
# Workflows

A **workflow** is a verification flow you build once in the dashboard (the capture steps, the countries and ID types, the branding and copy, any extra checks, and an optional [decisioning](https://trust.myaza.co/documentation/decisioning/markdown) graph), and then reuse without touching your code. Instead of passing a dozen props to the [SDK](https://trust.myaza.co/documentation/sdks/markdown), you reference a workflow by its id (`wf_…`) and the server hands the SDK the whole configuration.

Because the configuration lives on the server, you can change the flow (add a step, tighten a rule, tweak the welcome copy) by **re-publishing in the dashboard**. Live integrations pick up the new version on their next verification. No SDK upgrade, no redeploy.

Build workflows under **Dashboard → Workflows**. Each is scoped to one [environment](https://trust.myaza.co/documentation/environments/markdown) (a sandbox workflow doesn't exist in production) and has a draft you edit and a published snapshot your integrations read.

## Three ways to run a workflow

### 1. Embed it in the SDK (`workflowId`)

Mount the [Web](https://trust.myaza.co/documentation/sdk-react/markdown), [React Native](https://trust.myaza.co/documentation/sdk-react-native/markdown), or [Flutter](https://trust.myaza.co/documentation/sdk-flutter/markdown) SDK with a `workflowId` instead of spelling out `country`, `idTypes`, and the step toggles:

> **Minimum SDK versions for `workflowId`.** Web `@myazahq/kyc-sdk-react` **≥ 2.2.0** · React Native `@myazahq/kyc-sdk-react-native` **≥ 2.1.0** · Flutter `kyc_sdk_flutter` **≥ 2.2.0**. Earlier versions can't resolve a workflow ID: the web and React Native SDKs silently ignore it (the flow runs on props alone), and Flutter below 2.2.0 won't compile a workflow mount without a placeholder `country`. If a workflow embed seems to "do nothing", check the installed SDK version first.

```tsx
<MyazaKYC
  apiKey="pk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  workflowId="wf_AbC123dEf456"
  userId="user_42"
  metadata={{ requestId: "order_1001" }}
  onSubmit={(s) => console.log(s.verificationId)}
/>
```

The SDK resolves the workflow on mount and uses it as the source of truth: **the workflow's configuration wins over any overlapping props** (country, ID types, step toggles, appearance, copy). Runtime data (`userId`, `userData`, `metadata`, and your callbacks) always comes from your code. With a `workflowId`, `country` becomes optional (the workflow carries it).

An unknown, unpublished, or wrong-environment workflow surfaces as a blocking error via `onError`, never a silently different flow.

### 2. Share a hosted link

Every published workflow can also be turned into a **hosted link**: a public URL you send directly to a person (WhatsApp, email, SMS) with zero integration:

```
https://trust.myaza.co/verify/workflow/<linkToken>
```

The person opens the link, completes the flow in their browser, and you receive the result by [webhook](https://trust.myaza.co/documentation/webhooks/markdown) exactly as you would from an embedded SDK. Desktop visitors are offered a "continue on your phone" QR hand-off for the camera steps. Enable, disable, or regenerate the link from the workflow's page. Regenerating immediately kills the old URL. A link opened inside a WebView or an iframe reports the applicant's progress to its host as [hosted link events](https://trust.myaza.co/documentation/hosted-events/markdown).

**How the link previews when shared.** Pasted into WhatsApp, iMessage, Slack or an email client, the link unfurls as your request, not as Myaza's website: the title reads "Verify your identity with {Your organisation}" (or "Verify your business with", or the scope's own title for an address, contact or face check), the description is the Consent step's description when you have written one, and the preview card carries your logo (or your initial when none is uploaded), your organisation name and the workflow's primary colour from its Appearance settings, with a small "Secured by Myaza Trust" footer. A link that is no longer available previews as a neutral Myaza Trust card, so a preview never reveals whether a token exists. The pages stay out of search results regardless.

### 3. Mint a per-applicant session from your backend

When you know exactly who should verify, [`POST /api/kyc/sessions`](https://trust.myaza.co/documentation/api-create-session/markdown) (secret key) mints a hosted link that belongs to **one applicant**: single use, traceable to your `externalUserId`, optionally prefilled with what you already know, and optionally emailed on your behalf. A hosted link is one URL for many people; this is one URL for one person. The dashboard's "Create session" action mints the same thing by hand.

## How the SDK resolves a workflow

When mounted with a `workflowId`, the SDK calls one endpoint (instead of [`GET /config`](https://trust.myaza.co/documentation/api-config/markdown)):

```
GET /api/kyc/workflows/:workflowId
Authorization: Bearer pk_…
```

```json
{
  "workflow": { "id": "wf_AbC123dEf456", "name": "Standard KYC", "version": 3 },
  "config": { "country": "NG", "idTypes": ["bvn", "passport"], "enableLiveness": true },
  "environment": "PRODUCTION",
  "idTypes": [ { "country": "NG", "idType": "bvn", "features": { "govDbCheck": true } } ],
  "branding": { "companyName": "Acme Inc.", "logo": "https://…", "primaryColor": "#5645F5" }
}
```

The workflow must belong to the API key's organisation **and** environment and be **published**: anything else is an indistinguishable `404 workflow_not_found`, so ids can't be probed across organisations. The `config` is org-authored template data and contains no PII, so a publishable (`pk_`) key is enough. You normally never call this yourself; the SDK does.

## Capture add-ons

A workflow can turn on extra capture steps and checks beyond the core document + selfie + liveness flow. Each one is a step you toggle in the builder; most feed a soft signal into [decisioning](https://trust.myaza.co/documentation/decisioning/markdown) without ever changing the verification's own pass/fail status.

| Add-on | What it does |
|---|---|
| **Consent screen** | The welcome screen every flow opens on, where the person agrees to the verification. Its copy is editable, and the screen can be switched off on the Consent step (or by default for new workflows under Workflow Settings) when your own app has already asked: the flow then opens on its first real step. Consent remains your organisation's attestation to Myaza whichever way the switch is set. |
| **Contact verification** | An email and/or phone OTP right after consent: a cheap possession check that filters fake contacts before any gov-DB or liveness spend. Branch on `email.disposable`, `phone.countryMismatch`, and the like. |
| **Proof of Address** | Collects a utility bill / bank statement / tenancy agreement / government-issued document, then checks recency and, by the workflow's name rule, whether the applicant's name appears on it. The rule (Required / Optional / Not needed) is set per workflow with per-country, per-document exceptions, for markets where a bill names the meter or the landlord rather than the tenant. Branch on `verification.poaStatus` and `verification.poaNameMatch`. |
| **[Address Intelligence](https://trust.myaza.co/documentation/address-intelligence/markdown)** | A map pin (+ optional entrance photo and directions) corroborated against the government record, the PoA document and the IP country. On KYB flows the pin is the business premises. Branch on `address.tier` and the other `address.*` fields. |
| **NFC chip read** | Reads the eMRTD chip in a passport (mobile only) and runs server-side passive authentication, the strongest assurance tier. Branch on `verification.nfcStatus` / `verification.assuranceLevel`. |
| **Questionnaire** | Extra compliance declarations (source of funds, expected volume, …) asked before submission. Answers ride the webhook and can be branched on as `questionnaire.<key>`. A question can map onto canonical profile fields via `mapsTo`, including `walletAddress`, which appends the answer to the entity's wallet set and enrols [WALLET screening](https://trust.myaza.co/documentation/screening/markdown#types). |
| **Device Intelligence** | Device + IP fraud analysis (multi-accounting, emulators, datacentre IPs, velocity), **on by default**. Branch on `device.*` and `ip.*`. |
| **Liveness mode** | Choose gesture challenges, the screen-flash sequence, or **both** for the strongest presence check. |
| **Multi-region** | List several countries; the flow opens with a country-select step and applies that country's ID types and validation rules. |
| **Multiple IDs** | Verify two or three IDs in one run (e.g. NIN **and** BVN): the applicant picks each from what you allow for their country, one selfie covers them all, and it lands as **one verification** whose result follows your pass policy (all must pass, at least two, or any one). Works across regions, each country choosing which IDs its verifications offer. Branch on the `multiId.*` fields for finer routing. |

The web SDK accepts these as props too, but a workflow is the recommended path: you compose the checks once in the dashboard, and every embed and hosted link inherits them, and you change them by re-publishing, with no redeploy.

## Emailing the applicant

Switch on **Success → Applicant email → Email the applicant when approved** and, once a verification is approved, whether by the workflow or by a reviewer, we send the applicant a short email in your organisation's name. Declines are never emailed automatically; a reviewer can choose to email one as they decide it.

No address is set on the workflow. We only write to one the verification already holds, using the first of these that has one:

1. the email the applicant verified with a code, when the workflow runs email verification;
2. an email your integration passes, as `userData.email` on the SDK or on a [session you create](https://trust.myaza.co/documentation/api-create-session/markdown);
3. the email on the person's entity record.

When none of these holds an address, nothing is sent. Every email is added to the verification's timeline. The setting is not offered on biometric re-authentication workflows, which run every time somebody signs in.

## Identity details that do not match

Pass the name and date of birth you hold (`userData`), and every verification compares them with the ID: the government record for a number-only ID such as a BVN, and the document itself for a passport or licence. By default a mismatch fails the verification.

Under **ID Verification → Advanced → Identity details** you can set names and the date of birth, independently, to **record** instead. The comparison still runs and still lands on the result (`dataMatch: false`, with the field marked `enforced: false` in `dataValidationDetails`), but the verification can pass. Route those results with a decision rule on `verification.nameMatch` or `verification.dateOfBirthMatch`, for example sending them to review, or handle them in your webhook.

Keep face matching on when you record names. The face is then the only thing tying the person to the ID number they entered, so publishing warns you when face matching is off for any ID.

## Matching the selfie to the photo on a document

The selfie is compared with the strongest photo a verification holds: the photo on the government record, or the verified photo on the document's chip when the [NFC step](https://trust.myaza.co/documentation/nfc-chip/markdown) reads one. When neither is available, a document ID can use the photo printed on the document.

New workflows enable this for every supported document ID, including IDs added later. Manage the workflow-wide setting under **ID Verification → Advanced**. To exclude one ID, open its country settings under **ID Verification → Countries** and switch off **Match the selfie to the photo on the document**. Where an ID also shows **Facial match**, that setting must stay on.

Over the API, set `documentPhotoMatch: true` on the workflow config for every document ID, or on `countries[].idOptions[idType]` for one ID. The most specific value wins, so `false` on an ID leaves it out.

- It runs only when the verification has no government record photo and no verified chip photo.
- A mismatch fails the verification with `reasonCode: selfie_mismatch`, and the reason names the photo on the document.
- A printed photo is weaker evidence than a government or chip photo, so it never raises the [assurance level](https://trust.myaza.co/documentation/id-types/markdown).
- It is billed as a facial comparison.
- If no face can be read on the photo (glare, a thumb over it, a photo too small to see), nothing is compared. Under **ID Verification → Rules** choose what happens. **Decline**, the default, fails the verification with `reasonCode: document_photo_no_face` and asks the person to photograph the document again. **Review** keeps the result the checks produced, and a decision rule on `verification.documentPhotoNoFace` sends it to a person to approve or decline. The tab writes that rule for you, and publishing warns you if no rule reads the field. Either way the result carries `facialMatchSkipped: "no_face_on_document"`.

The result says which photo was used: `facialMatchSource` is `gov_record`, `chip` or `document`, and `null` when no comparison ran. It rides the result and the [webhook](https://trust.myaza.co/documentation/webhooks/markdown), and a decision rule on `verification.facialMatchSource` can, for example, send document-photo matches to review.

## Age restrictions

Under **ID Verification → Age** you can limit a workflow to people of a certain age in each country. Once it is on, every verification compares the holder's age with the range for their country, using the date of birth the check established: the government record, the document, or its chip. A date of birth the applicant typed is never used for this.

- **Minimum age** follows each country's legal age of majority by default: 18 in most countries, between 16 and 21 elsewhere. The tab shows it beside every country, with a note where the age differs inside a country. You can set one minimum for every country instead.
- **Maximum age** is off by default, because no country sets a legal maximum. Set one if your policy needs it.
- **A country's own limits** override the defaults for that country alone.

Choose what happens to someone outside the range, separately for **under the minimum** and **over the maximum**:

- **Decline**, the default: the verification fails with `reasonCode: age_restricted`.
- **Review**: the verification keeps the result its checks produced, and a decision rule on `verification.ageLimit` (`under_minimum` or `over_maximum`) sends it to a person to approve or decline. The tab writes that rule for you, and publishing warns you if no rule reads the field.

A verification whose date of birth could not be read always fails with `age_unverified`, so an unreadable document is never waved through. Neither reason names an age. Test scenarios 22 and 23 exercise both against your own limits (see [Sandbox testing](https://trust.myaza.co/documentation/sandbox-testing/markdown)).

## Templates

New workflows start from a **template**: a pre-built flow you can use as-is or customise. Templates are country-agnostic (you pick the country and ID types in the builder) and ship with a sensible decision graph. The catalogue:

**Onboarding**: everyday signup shapes.

- **Standard KYC**: full capture, auto-approve on pass.
- **Low-friction signup**: number-only IDs (BVN/NIN) + selfie; failures go to review, not decline.
- **Hosted link collection**: tuned for sharing; can't be dismissed mid-way, desktop→phone hand-off.
- **Re-verification**: a light refresh for customers due for renewal.
- **High-assurance biometric**: camera-only, gesture + flash liveness, NFC chip read; chip-verified identities auto-approve, softer matches go to review.
- **Contact-verified onboarding**: email + phone OTP before capture; throwaway emails routed to review.

**Compliance**: screening, strict regimes, human-in-the-loop.

- **KYC + AML screening**: screen every verified user; watchlist hits open a case.
- **Strict compliance**: auto-decline confirmed matches, escalate the rest.
- **Manual review everything**: no auto-approvals; a human signs off each customer.
- **Geographic risk routing**: extra scrutiny for high-risk nationalities or IP-country mismatches.
- **Risk-tiered onboarding**: let the monitoring risk tier set the friction.
- **Crypto exchange onboarding**: full KYC plus AML screening that covers the customer's crypto wallet addresses; watchlist or wallet hits open a case, network-flagged identities are declined.

**Specialised**: targeted checks.

- **Age-gated service (18+)**: verify identity and age together.
- **Expired-document guard**: decline expired IDs, flag soon-to-expire ones.
- **Network trust fast-track**: trust identities already verified elsewhere on the Myaza network.
- **Strict data & face match**: both checks must explicitly pass.
- **Failure triage & retry**: give recoverable capture failures a second chance.
- **Device & IP fraud guard**: route multi-accounting, emulator, and bot signals to review.
- **Address-verified onboarding**: identity plus a proof-of-address check.
- **Presence-verified address**: smart-address capture plus a multi-day presence watch the decision holds on.
- **Address-only verification**: no identity step at all; the address capture is the whole flow, for books verified elsewhere. See [Address Intelligence](https://trust.myaza.co/documentation/address-intelligence/markdown#address-only-workflows-standalone-no-identity-step).
- **Biometric enrolment** and **Face re-authentication**: a liveness selfie into, then against, the customer's biometric template; the workflow-shaped face check with contact-code companions and a decision on the outcome.
- **Declarations refresh**: a questionnaire alone, for periodic attestations and source-of-funds updates.
- **Contact re-verification**: one-time codes to the email and phone on file, nothing else.

These five are **scoped workflows**: `scope` on the config names the one thing the flow verifies (`address`, `biometric-authentication`, `biometric-enrollment`, `questionnaire`, `contact`), publish rejects any stronger evidence, submissions carry the scope's marker `idType` and require the published workflow, and the capture lands on your existing customer record by `externalUserId` without ever touching its KYC status.

A workflow's scope is chosen with the **Scope** control at the top of the builder rail and is **editable until the first publish, then locked**: verifications, hosted links and embeds already reference what that workflow ID verifies, so a mid-life change would rewrite their meaning (the API returns `scope_locked`). To get a different scope from a live workflow, use **Scoped copy** on the workflows list. It creates a fresh draft under a new workflow ID that carries the parts that mean the same thing under any scope (branding, country, contact steps, session policy) and none of the identity evidence, seeded so the copy opens ready to publish.

**Business (KYB)**: registry lookups with due diligence.

- **Standard KYB**: registry lookup with a customer due-diligence gate for key people.
- **KYB + AML screening**: screen every verified business before approval.
- **Enhanced KYB with declarations**: registry check plus a compliance questionnaire.

## Business (KYB) workflows

A workflow can verify a **business** instead of a person. It looks the company up in the official registry, optionally collects company documents and declarations, and runs **customer due diligence on the people who own or control it**, screening directors, beneficial owners (UBOs), and the applicant, and optionally requiring each to complete their own KYC via a per-person invite link.

Live business submissions **require** a published KYB workflow; the server rejects a business verification with no workflow. See **[Business verification (KYB)](https://trust.myaza.co/documentation/business-verification/markdown)** for the full guide (products, the application layers, the result shape, and decisioning), [Key people & ownership](https://trust.myaza.co/documentation/key-people/markdown) for the due-diligence layer (roles, thresholds, corporate shareholders, ownership chains), and [Supported countries](https://trust.myaza.co/documentation/countries/markdown#business-verification-kyb) for registry coverage.

## Versioning

Editing a published workflow only changes its **draft**: live integrations keep seeing the last published snapshot until you publish again. Every publish bumps the version and appends an immutable history entry you can inspect or restore from. Each verification records the exact `workflowId` and version that drove it, and the [`data.workflowId`](https://trust.myaza.co/documentation/webhooks/markdown) field appears on every verification webhook so you can attribute results back to a flow.

## Next steps

- [Decisioning](https://trust.myaza.co/documentation/decisioning/markdown): the rules engine that turns verification data into approve / decline / review.
- [Webhooks](https://trust.myaza.co/documentation/webhooks/markdown): [`verification.started`](https://trust.myaza.co/documentation/webhooks/markdown#when-the-applicant-submits) when an applicant submits your flow, the `workflow.run.completed` event, and workflow attribution on results.
- [Web SDK](https://trust.myaza.co/documentation/sdk-react/markdown): mounting with a `workflowId`.
