
# Create a session

```
POST /api/kyc/sessions
```

Mints a hosted verification link for **one applicant** from one of your published [workflows](https://trust.myaza.co/documentation/workflows/markdown), entirely server to server. You get back a URL that is theirs alone: send it by email, SMS, WhatsApp, or render it as a QR, and the applicant completes the flow on our hosted page with no SDK integration on your side.

The session **is** the link. It carries one verification, it is traceable to your own reference for the applicant, and once it is submitted it is spent. Forwarding it to a second person does not work. This is the API twin of the dashboard's "Create session" action; the two accept the same request and mint the same thing.

**Authentication:** `Authorization: Bearer sk_…` (**secret key required**). The response contains a live verification credential and the request may carry applicant details, so this endpoint is backend-only. A publishable key receives `403 secret_key_required`.
**Content type:** `application/json`.
**Production note:** `sk_live_` keys require an [approved business](https://trust.myaza.co/documentation/environments/markdown#production).

Looking for one URL many people can open, each visit starting its own verification? That is a workflow's [hosted link](https://trust.myaza.co/documentation/workflows/markdown#2-share-a-hosted-link). This endpoint is the opposite shape: you know the applicant, and you want a link that is theirs.

## Request body

| Field | Type | Required | Description |
|---|---|---|---|
| `workflowId` | string | yes | The **published** workflow to run, e.g. `wf_AbC123dEf456`. Drafts and archived workflows are refused. |
| `externalUserId` | string | no | **Your** reference for the subject: your user id on an individual flow, the business on a KYB one. It is what links the resulting verification and [entity](https://trust.myaza.co/documentation/identity-hub/markdown) back to your records, and what a returning applicant resumes by. Strongly recommended. |
| `metadata` | object | no | Your own correlation fields for this applicant (a loan id, an account id). Bounded (16 KiB, four levels, 100 keys), stored on the session, inherited by the verification it produces and carried on every [webhook](https://trust.myaza.co/documentation/webhooks/markdown#your-reference-on-every-event) about it. |
| `userData` | object | no | What you already know about the subject, to prefill the flow: `firstName`, `lastName`, `dateOfBirth`, and for KYB flows `businessName`. Prefill, never assertion: the applicant confirms or corrects every value, so none of it is treated as verified. `email` is the applicant's address: nothing in the flow asks for it, and it is where we write when you have the applicant [emailed about a decision](https://trust.myaza.co/documentation/api-review-verification/markdown#emailing-the-applicant). When you leave it out, the `email` you send the link to stands in for it. |
| `business` | object | no | For KYB flows, the company where you already know it: `country` (ISO-2), `registrationNumber`, `registrationName`. The applicant lands on a picked-company card instead of a search box, and the register remains the source of truth. |
| `email` | string | no | Email the link to the applicant on your behalf. Optional: deliver it your own way and omit this. |
| `faceReferenceMediaId` | string | see note | Biometric-authentication workflows only: the mediaId of a reference face photo (uploaded via [`POST /api/kyc/upload`](https://trust.myaza.co/documentation/api-upload/markdown) with type `auth_reference`). The applicant's live selfie is matched against this photo instead of a stored enrolment, so you can authenticate somebody you never enrolled through KYC. |

A **biometric-authentication** workflow must name its subject at mint time: pass `externalUserId` for a user with an active biometric enrolment, or `faceReferenceMediaId` with a reference photo. Neither present is refused with `422 user_reference_required`; a reference without an enrolment with `422 not_enrolled`. A **biometric-enrolment** workflow always requires `externalUserId` (the template binds a face to a named record). This is checked when the session is created, so a link that could only fail at submit is never minted.

## Request

```bash
curl "https://trust.myaza.app/api/kyc/sessions" \
  -H "Authorization: Bearer $MYAZA_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "workflowId": "wf_AbC123dEf456",
    "externalUserId": "user_42",
    "metadata": { "loanId": "loan_20191" },
    "userData": { "firstName": "John", "lastName": "Doe" },
    "email": "john@example.com"
  }'
```

## Response `201 Created`

```json
{
  "session": {
    "sessionId": "cmt1gfjce05g547grqbmjsiqf",
    "url": "https://trust.myaza.co/verify/pXk3...",
    "shortCode": "7K9F-2QHM",
    "expiresAt": "2026-08-21T12:00:00.000Z",
    "kind": "individual",
    "externalUserId": "user_42",
    "metadata": { "loanId": "loan_20191" },
    "workflow": { "id": "wf_AbC123dEf456", "name": "Identity verification", "version": 3 }
  },
  "delivery": { "sent": true }
}
```

| Field | Description |
|---|---|
| `session.url` | The applicant's link. Treat it like a credential: whoever opens it verifies as this applicant. |
| `session.shortCode` | A human-typable code for when a camera cannot scan a QR. Display only, never a credential. |
| `session.expiresAt` | When the link stops accepting a **new** start: 24 hours for individual flows, 7 days for business (KYB) ones, unless the workflow sets its own session lifetime. A submitted verification is unaffected by expiry. |
| `session.kind` | `individual` or `business`, following the workflow's subject type. |
| `session.externalUserId`, `session.metadata` | Your reference and metadata, echoed exactly as stored. The verification this session produces, and every webhook about it, carry the same values. |
| `delivery` | `null` when no `email` was given; `{ "sent": true }` or `{ "sent": false, "reason": "send_failed" }` otherwise. A failed send never fails the mint; the link in `session.url` works regardless. |

The config the applicant walks is the workflow's published snapshot, frozen at mint time. Editing or re-publishing the workflow never changes a link that is already in someone's inbox.

## Tracking the session

The `sessionId` doubles as the verification's id once the applicant submits, so the reference you hold from the start resolves from the start:

- [`GET /status/:id`](https://trust.myaza.co/documentation/api-verification-status/markdown) reports the attempt live: `in_progress` with the current step and captured slots, then the full verification lifecycle after submission, and `abandoned` or `expired` if the applicant never finishes.
- [Session webhooks](https://trust.myaza.co/documentation/webhooks/markdown) (`session.started`, `session.resumed`, `session.abandoned`, `session.expired`) push the same lifecycle to your backend, and the verification events follow once they submit.
- Opening the `url` in a WebView or an iframe? The page tells its host what the applicant is doing, step by step, with nothing to install: see [Hosted link events](https://trust.myaza.co/documentation/hosted-events/markdown).
- Sending the `url` yourself over WhatsApp, SMS or email? It previews as your request: your organisation's name and logo, the workflow's primary colour and "Verify your identity with {Your organisation}" in the title, the same way a [hosted link](https://trust.myaza.co/documentation/workflows/markdown) does. A spent or expired session previews as a neutral Myaza Trust card.

## Ending a session early

A session you no longer want opened can be expired with `POST /api/kyc/sessions/:sessionId/expire`: the link stops working at once and you receive `session.expired`. See [Delete records & expire sessions](https://trust.myaza.co/documentation/api-manage-records/markdown).

## Errors

| Status | `error` | Meaning |
|---|---|---|
| `400` | `invalid_input` | The body failed validation; `message` names the first problem. |
| `403` | `secret_key_required` | A publishable key was used. Call this from your backend with a secret key. |
| `403` | `business_not_approved` | `sk_live_` before [KYB approval](https://trust.myaza.co/documentation/environments/markdown#production). Sandbox keys work from day one. |
| `422` | `workflow_not_available` | The workflow is not published (or is suspended), so it cannot start a verification. |
| `429` | `rate_limited` | Too many sessions minted this hour. Retry later, or contact support to raise your limit. |
