# @okyc/node

> The server SDK for Node 18+, Bun, Deno and edge runtimes — typed, retried, never logs bodies.

Source: https://docs.okyc.stream/backend/node-sdk

## Install

```bash
npm install @okyc/node
```

```ts
import { Okyc } from '@okyc/node';

const okyc = new Okyc(process.env.OKYC_SECRET_KEY!); // sk_test_… or sk_live_…
```

| Option       | Default                   |                                                                                              |
| ------------ | ------------------------- | -------------------------------------------------------------------------------------------- |
| `baseUrl`    | `https://api.okyc.stream` | API origin, for a dedicated or self-hosted deployment. Must be https except on `localhost`.  |
| `apiVersion` | —                         | Pins the `OKYC-Version` header.                                                              |
| `timeoutMs`  | `30000`                   | Per request.                                                                                 |
| `maxRetries` | `3`                       | Network errors, `408`, `429`, `5xx`; exponential backoff with jitter, honours `Retry-After`. |
| `fetch`      | global `fetch`            | Custom fetch implementation.                                                                 |
| `logger`     | —                         | Receives method, path, status, latency, attempt and request id — never bodies.               |

The constructor refuses anything that isn't a secret key, so a client token can't be used by mistake.

## Sessions

```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' });
  }
});
```

| Method                                                                   | Endpoint                                                            |
| ------------------------------------------------------------------------ | ------------------------------------------------------------------- |
| `sessions.create(params, { idempotencyKey })`                            | `POST /v1/sessions` → `{ id, status, url, clientToken, expiresAt }` |
| `sessions.retrieve(id, { expand: ['checks', 'applicant', 'business'] })` | `GET /v1/sessions/:id`                                              |
| `sessions.list(params)`                                                  | async iterator over every page (`firstPage()` for one page)         |
| `sessions.listPage(params)`                                              | one page: `{ data, has_more }`                                      |
| `sessions.decide(id, { outcome, reasonCode, note, steps })`              | `POST /v1/sessions/:id/decision`                                    |
| `sessions.cancel(id)`                                                    | `POST /v1/sessions/:id/cancel`                                      |
| `sessions.createClientToken(id)`                                         | `POST /v1/sessions/:id/client_token`                                |
| `sessions.evidenceUrl(id, evidenceId)`                                   | `GET /v1/sessions/:id/evidence/:evidence_id`                        |
| `sessions.erasePii(id)`                                                  | `DELETE /v1/sessions/:id/pii`                                       |

`create` takes camelCase (`workflowId`, `referenceId`, `redirectUrl`, `expiresIn`, `prefill.fullName`);
retrieved sessions are returned as the API sends them (snake\_case).

## Webhooks

```js title="server.mjs"
// Webhooks: verify against the RAW body, dedupe on event.id, then fetch details from the API.
const seen = new Set();
app.post('/okyc/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
  let event;
  try {
    event = await okyc.webhooks.constructEvent(
      req.body,
      req.header('okyc-signature'),
      process.env.OKYC_WEBHOOK_SECRET,
    );
  } catch {
    return res.sendStatus(400);
  }
  if (seen.has(event.id)) return res.sendStatus(200);
  seen.add(event.id);
  if (event.type === 'session.approved' || event.type === 'session.declined') {
    const session = await okyc.sessions.retrieve(event.data.session.id);
    console.log(`user ${session.reference_id} → ${session.status}`);
    // mark the user verified / rejected in your database
  }
  res.sendStatus(200);
});
```

`webhooks.constructEvent(rawBody, header, secret, { toleranceSec })` throws `WebhookSignatureError` when the
header is missing, malformed, too old or wrong, or when the body isn't JSON. A valid event can arrive more than
once within the tolerance, so de-duplicate on its `id` ([webhooks](https://docs.okyc.stream/backend/webhooks.md)).
`generateTestHeader(secret, body)` signs a body for your tests.
Both use Web Crypto, so they work in edge runtimes.

## Checks and other resources

| Method                                                                                    |                                                             |
| ----------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| `checks.run(key, input, { async })` / `checks.retrieve(id)`                               | Direct checks, e.g. `checks.run('in.pan.verify', { pan })`. |
| `in.pan.verify`, `in.gstin.verify`, `in.bank.verify`, `in.mca.company`, `in.udyam.verify` | Typed shortcuts for India checks.                           |
| `screening.sanctions({ name, dob, entity_type })`                                         | Sanctions screening without a session.                      |
| `workflows.list()` / `workflows.retrieve(id)`                                             | Published workflows.                                        |
| `webhookEndpoints.create/list/del`                                                        | Manage endpoints (the secret is returned once by `create`). |
| `events.list({ type, limit })`                                                            | Recent events, for reconciliation.                          |
| `capabilities.list({ country })`                                                          | What a country supports.                                    |

Errors are typed — see [errors](https://docs.okyc.stream/backend/errors.md).
