# Sessions

> Create a verification session for each applicant, hand its URL to your frontend, and read the result.

Source: https://docs.okyc.stream/backend/sessions

A session is one applicant going through one [workflow](https://docs.okyc.stream/concepts/workflows.md). Create it on your backend with a
secret key; everything the frontend needs is in the response.

## Create a session

```js title="server.mjs"
// Your frontend calls this, then passes { clientToken, sessionUrl } to @okyc/web or @okyc/react.
app.post('/api/kyc/session', express.json(), async (req, res) => {
  const userId = req.header('x-user-id') ?? 'demo-user'; // use your own auth here
  try {
    const session = await okyc.sessions.create(
      {
        workflowId: process.env.OKYC_WORKFLOW_ID,
        referenceId: userId,
        redirectUrl: process.env.REDIRECT_URL,
      },
      { idempotencyKey: `kyc-${userId}-${new Date().toISOString().slice(0, 10)}` },
    );
    res.json({ clientToken: session.clientToken, sessionUrl: session.url });
  } catch (e) {
    res.status(e.statusCode ?? 500).json({ error: e.code ?? 'error' });
  }
});
```

| Field          | Required | Notes                                                                                   |
| -------------- | -------- | --------------------------------------------------------------------------------------- |
| `workflow_id`  | yes      | A published workflow. New sessions use its current version.                             |
| `reference_id` |          | Your user's id (max 128 characters). Use it to find sessions and in webhooks.           |
| `country`      |          | Two-letter code; defaults to the workflow's country.                                    |
| `prefill`      |          | `phone`, `email`, `full_name` shown pre-filled to the applicant.                        |
| `locale`       |          | Flow language, one of the workflow's languages (`en`, `hi`).                            |
| `redirect_url` |          | Where `redirect` mode returns (with `?session_id=&status=`). Must be an allowed origin. |
| `metadata`     |          | String key/value pairs (values up to 500 characters) returned with the session.         |
| `expires_in`   |          | Seconds, 300 to 2,592,000 (30 days). Default: the workflow's TTL (7 days).              |

The full schema is in the [API reference](https://docs.okyc.stream/reference/api/sessions.md#post-v1sessions). Send an
[`Idempotency-Key`](https://docs.okyc.stream/backend/idempotency.md) so a retried request doesn't create a second session.

The response:

```json
{
  "id": "ses_…",
  "status": "created",
  "url": "https://verify.okyc.stream/s/ses_…#t=st_…",
  "client_token": "st_…",
  "expires_at": "2026-10-08T09:30:00.000Z"
}
```

* Pass `url` to a [mobile SDK](https://docs.okyc.stream/mobile.md), or `client_token` + `url` to [`@okyc/web`](https://docs.okyc.stream/web/web-sdk.md) /
  [`@okyc/react`](https://docs.okyc.stream/web/react.md).
* The client token is valid for 30 minutes, sliding while the applicant is active. To resume later, issue a new
  one with `POST /v1/sessions/:id/client_token`; the flow continues at the first unfinished step.
* Don't log the `url`: it carries the token.

## Read the result

```bash
curl "https://api.okyc.stream/v1/sessions/ses_…?expand=checks,applicant" \
  -H "Authorization: Bearer $OKYC_SECRET_KEY"
```

`status` is the session's state ([statuses](https://docs.okyc.stream/concepts/statuses.md)); `decision` and `decision_source` (`auto` or
`manual`) say how it was decided. `expand=checks` adds every check with its result; `expand=applicant` (KYC) or
`expand=business` (KYB) adds the verified profile.

Fetch a session when a [webhook](https://docs.okyc.stream/backend/webhooks.md) arrives rather than polling.

## List sessions

`GET /v1/sessions?status=review&limit=50` returns a page (`data`, `has_more`). Pass the last id as
`starting_after` for the next page, or filter by `reference_id`. `@okyc/node` iterates every page for you:
`for await (const s of okyc.sessions.list({ status: 'review' }))`.

## Other actions

| Endpoint                                     | Use                                                                                              |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `POST /v1/sessions/:id/decision`             | Decide a session in `review` (`approved`, `declined`, `resubmission_requested` + `reason_code`). |
| `POST /v1/sessions/:id/cancel`               | Cancel an active session.                                                                        |
| `POST /v1/sessions/:id/client_token`         | New client token to resume in the frontend.                                                      |
| `GET /v1/sessions/:id/evidence/:evidence_id` | Short-lived URL for an evidence file (audited).                                                  |
| `DELETE /v1/sessions/:id/pii`                | Erase personal data (DPDP erasure); the decision skeleton is kept.                               |

Evidence files are served for download: images (JPEG, PNG, WebP) inline, everything else — PDFs, ZIPs — as an
attachment with `Content-Type: application/octet-stream` and a sandboxing `Content-Security-Policy`, so a
malicious document can never run in a browser. Every upload is virus-scanned before it is stored.

## Direct checks (no hosted flow)

Run a single check server-to-server, for example to re-verify a PAN you already hold:

```bash
curl https://api.okyc.stream/v1/checks/in.pan.verify \
  -H "Authorization: Bearer $OKYC_SECRET_KEY" -H "Content-Type: application/json" \
  -d '{ "pan": "ABCPE1234F" }'
```

Use a country key (`in.pan.verify`) or a generic key with `country` (`tax_id.verify` + `"country": "IN"`).
`?async=true` returns `202` and sends `check.completed` when done. `GET /v1/capabilities?country=IN` lists
what a country supports.
