> ## Documentation Index
> Fetch the complete documentation index at: https://docs.didit.me/llms.txt
> Use this file to discover all available pages before exploring further.

# API Full Flow

> End-to-end Didit REST API integration: get credentials, create a session, share the link, handle webhooks, and retrieve KYC decisions in minutes.

This guide walks you through the complete flow for integrating Didit identity verification via the REST API: get credentials, create a session, present it to the user, and receive results.

***

## Step 1: Get Your Credentials

You have two ways to obtain an `api_key`:

### Option A: Programmatic registration (no browser)

Best for CI/CD and AI agents. Two API calls — `POST /programmatic/register/` then `POST /programmatic/verify-email/` — return the `api_key` inline.

→ See [Programmatic Registration](/integration/programmatic-registration) for the full flow, password rules, JWT lifetime, and lockout policy.

### Option B: From the Didit Console

1. Go to the [Didit Console](https://business.didit.me).
2. Create or select your organization.
3. Navigate to **Settings** → **API & Webhooks**.
4. Copy:
   * **API Key** — used as the `x-api-key` header on every verification call.
   * **Webhook Secret Key** — used to verify webhook HMAC signatures.

### Environment variables

```bash theme={null}
# Required
DIDIT_API_KEY=your_api_key_here
DIDIT_WEBHOOK_SECRET=your_webhook_secret_here

# Your workflow ID (from the Console)
DIDIT_WORKFLOW_ID=your_workflow_id_here
```

***

## Step 2: Create a Workflow

Before creating sessions, you need a verification workflow. Workflows define what verification steps users go through.

### Create in Console

1. Go to [Didit Console](https://business.didit.me) → **Workflows**
2. Click **Create Workflow**
3. Choose a base template:

| Template                      | Best For                                    |
| ----------------------------- | ------------------------------------------- |
| **KYC**                       | Full identity verification with ID document |
| **Adaptive Age Verification** | Age verification with optional ID backup    |
| **Biometric Authentication**  | Re-verifying returning users                |
| **Address Verification**      | Proof of address verification               |

4. Add optional features:

| Feature              | Description                         |
| -------------------- | ----------------------------------- |
| NFC Verification     | Read passport/ID chip data          |
| Liveness Detection   | Prevent spoofing with selfie video  |
| Face Matching        | Match selfie to document photo      |
| Phone Verification   | Verify phone number via SMS         |
| Email Verification   | Verify email address                |
| AML Screening        | Check against sanctions/PEP lists   |
| Database Validation  | Verify against government databases |
| Device & IP Analysis | Detect VPNs, proxies, geolocation   |

5. Copy your **Workflow ID**

<Note>
  See [Workflows Documentation](/console/workflows) for detailed
  configuration options.
</Note>

***

## Step 3: Create a Verification Session

Call the API to create a session for your user.

### Request

```bash theme={null}
curl -X POST https://verification.didit.me/v3/session/ \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{
    "workflow_id": "your-workflow-id",
    "callback": "https://yourapp.com/verification-complete",
    "vendor_data": "user-123"
  }'
```

### Request Parameters

| Parameter          | Required    | Description                                                                                                                                                                                                                       |
| ------------------ | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `workflow_id`      | Yes         | Your workflow UUID from the Console.                                                                                                                                                                                              |
| `callback`         | No          | URL the user is sent to after verification. Didit appends `verificationSessionId` and `status` as query parameters. Supports custom schemes (e.g. `myapp://`). Falls back to the workflow's configured callback URL when omitted. |
| `callback_method`  | No          | Which device handles the redirect: `initiator`, `completer`, or `both`. Default: `initiator`.                                                                                                                                     |
| `vendor_data`      | No          | Your internal user identifier — echoed in webhooks for correlation.                                                                                                                                                               |
| `metadata`         | No          | Arbitrary JSON object stored with the session and echoed back in webhooks.                                                                                                                                                        |
| `language`         | No          | UI language hint (ISO 639-1, e.g. `en`, `es`). Browser language is auto-detected when omitted.                                                                                                                                    |
| `contact_details`  | No          | User's email/phone for prefill and email notifications.                                                                                                                                                                           |
| `expected_details` | No          | Expected user details for cross-validation against verified data.                                                                                                                                                                 |
| `portrait_image`   | Conditional | Base64-encoded face image (max 2MB; JPEG, PNG, WebP, or TIFF). Required for Biometric Authentication / Face-Match-first workflows.                                                                                                |
| `sandbox_scenario` | No          | Sandbox-only scenario slug (e.g. `approve`, `decline_aml_hit`) that auto-populates the session with magic inputs.                                                                                                                 |

### Full Example with All Options

```bash theme={null}
curl -X POST https://verification.didit.me/v3/session/ \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{
    "workflow_id": "11111111-2222-3333-4444-555555555555",
    "callback": "https://yourapp.com/verification-complete",
    "vendor_data": "user-123",
    "metadata": {
      "plan": "premium",
      "signup_source": "mobile-app"
    },
    "contact_details": {
      "email": "alex.sample@example.com",
      "email_lang": "en",
      "send_notification_emails": true,
      "phone": "+15550101000"
    },
    "expected_details": {
      "first_name": "John",
      "last_name": "Doe",
      "date_of_birth": "1990-01-01",
      "expected_document_types": ["P", "ID"]
    }
  }'
```

<Note>
  `expected_details.expected_document_types` restricts the ID verification step
  to specific document types — the document selection screen only shows the
  requested types. Allowed values: `P` (passport), `ID` (national ID), `DL`
  (driver's licence), `RP` (residence permit), `HIC` (health insurance card),
  `TC` (tax card), `SSC` (social security card). Values are case-insensitive
  and deduplicated; unknown values return `400`. See the
  [Create Session reference](/sessions-api/create-session) for the full schema.
</Note>

### Response

`201 Created`:

```json theme={null}
{
  "session_id": "11111111-2222-3333-4444-555555555555",
  "session_number": 43762,
  "session_token": "3FaJ9wLqX2Mz",
  "url": "https://verify.didit.me/session/3FaJ9wLqX2Mz",
  "vendor_data": "user-123",
  "metadata": {
    "plan": "premium",
    "signup_source": "mobile-app"
  },
  "status": "Not Started",
  "workflow_id": "11111111-2222-3333-4444-555555555555",
  "workflow_version": 3,
  "callback": "https://yourapp.com/verification-complete"
}
```

### Key Response Fields

| Field              | Description                                                                                                                                                                                                                                                     |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `session_id`       | Unique UUID for this session. Use it for `GET /v3/session/{sessionId}/decision/` and webhooks.                                                                                                                                                                  |
| `session_number`   | Human-readable sequence number shown in the Didit console.                                                                                                                                                                                                      |
| `session_token`    | 12-character URL-safe token that authorizes the end user to open the hosted flow, and initialises the native SDKs (the web SDK takes `url` instead). Treat it as a secret.                                                                                      |
| `url`              | Hosted verification URL to redirect the user to, or embed in an iframe. (Field name is `url`, not `verification_url`.) When the request includes `language`, the URL contains a language path segment — e.g. `https://verify.didit.me/en/session/3FaJ9wLqX2Mz`. |
| `status`           | Current status — `"Not Started"` for a newly created session.                                                                                                                                                                                                   |
| `workflow_id`      | Stable workflow identifier the session runs on.                                                                                                                                                                                                                 |
| `workflow_version` | Published workflow version the session was pinned to at creation.                                                                                                                                                                                               |

<Note>
  **Idempotency.** When `vendor_data` is provided and an unfinished session
  (`Not Started`, `In Progress`, `Resubmitted`, or `Awaiting User`) with the
  same `vendor_data` already exists on the workflow's latest published version,
  that existing session is returned (still `201`) instead of creating a
  duplicate — with its `callback` and `metadata` updated to the new values, and
  `status` reflecting the existing session.
</Note>

<Note>
  **Auth errors are `403`, never `401`.** A missing, malformed, or expired
  `x-api-key` returns `403` with
  `{"detail": "You do not have permission to perform this action."}` — the
  same body as a valid key without permission. Insufficient credits return
  `400`. See [Create Session API Reference](/sessions-api/create-session) for
  complete documentation.
</Note>

***

## Step 4: Present Verification to User

Choose how to present the verification flow to your user:

### Option A: Native SDK (recommended for mobile)

Use the `session_token` with the native SDKs for camera, NFC, and biometric integration.

→ [iOS SDK](/integration/native-sdks/ios-sdk) · [Android SDK](/integration/native-sdks/android-sdk) · [React Native](/integration/native-sdks/react-native-sdk) · [Flutter](/integration/native-sdks/flutter-sdk)

### Option B: InContext iframe (recommended for web)

Embed the verification in your page using the `url` returned in Step 3.

```html theme={null}
<iframe
  src="{url}"
  style="width: 100%; height: 700px; border: none;"
  allow="camera; microphone; fullscreen; autoplay; encrypted-media"
></iframe>
```

→ [InContext iframe](/integration/web-sdks/incontext-iframe)

### Option C: Redirect

Redirect the user to the verification URL.

```javascript theme={null}
window.location.href = session.url;
```

→ [Web redirect](/integration/web-sdks/web-redirect)

### Option D: Mobile WebView

For mobile apps without a native SDK.

```jsx theme={null}
// React Native
<WebView source={{ uri: session.url }} />
```

→ [WebView in iOS/Android](/integration/web-sdks/webview-in-ios-android)

***

## Step 5: Receive Results

### Via Webhook (Recommended)

Webhooks notify your server in real-time when verification status changes.

**Setup:**

1. Create a webhook destination in the [Console](https://business.didit.me) (**API & Webhooks**) or via [`POST /v3/webhook/destinations/`](/management-api/webhook/create-destination) with `webhook_version: "v3"` and a `subscribed_events` array (e.g. `["status.updated"]`)
2. Store the `secret_shared_key` returned on creation — it is the HMAC secret for all signature variants
3. Implement a webhook endpoint on your server

**Example webhook payload:**

Inside `decision`, every per-feature result is delivered as a **plural array** (`id_verifications[]`, `nfc_verifications[]`, `liveness_checks[]`, `face_matches[]`, …) — one entry per node in the workflow. This is the V3 contract and is sacred: never code against a singular `nfc` / `id_verification` / `liveness` key.

```json theme={null}
{
  "event_id": "9c0c8b8a-1111-4222-9333-444444444444",
  "webhook_type": "status.updated",
  "timestamp": 1774970000,
  "created_at": 1774969994,
  "application_id": "11111111-2222-3333-4444-555555555555",
  "environment": "live",
  "session_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
  "status": "Approved",
  "workflow_id": "22222222-3333-4444-5555-666666666666",
  "workflow_version": 4,
  "vendor_data": "user-123",
  "metadata": { "plan": "premium" },
  "decision": {
    "session_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
    "status": "Approved",
    "id_verifications": [
      { "node_id": "id_verification_1", "status": "Approved", "document_type": "Passport", "warnings": [] }
    ],
    "liveness_checks": [
      { "node_id": "liveness_1", "status": "Approved", "method": "PASSIVE", "score": 95.4, "warnings": [] }
    ],
    "face_matches": [
      { "node_id": "face_match_1", "status": "Approved", "score": 96.1, "warnings": [] }
    ],
    "reviews": []
  }
}
```

The `decision` object is only present when `status` is `Approved`, `Declined`, `In Review`, or `Abandoned` (a `Resubmitted` event carries `resubmit_info` instead). Didit reuses the same `event_id` across retries and fan-out destinations — key your idempotency on it. See [Data models](/reference/data-models) for every plural array and field.

**Verify the signature (X-Signature-V2, recommended):**

`X-Signature-V2` is HMAC-SHA256 over a canonical re-encoding of the JSON body: whole-valued floats shortened to ints, keys sorted recursively, compact separators, Unicode preserved.

```javascript theme={null}
const crypto = require("crypto");

// Match Didit's float normalisation: whole-valued floats serialise as ints.
function shortenFloats(data) {
  if (Array.isArray(data)) return data.map(shortenFloats);
  if (data !== null && typeof data === "object") {
    return Object.fromEntries(
      Object.entries(data).map(([key, value]) => [key, shortenFloats(value)])
    );
  }
  return data;
}

// Sort object keys recursively before re-stringifying.
function sortKeys(obj) {
  if (Array.isArray(obj)) return obj.map(sortKeys);
  if (obj !== null && typeof obj === "object") {
    return Object.keys(obj).sort().reduce((acc, key) => {
      acc[key] = sortKeys(obj[key]);
      return acc;
    }, {});
  }
  return obj;
}

function verifyWebhook(rawBody, signatureV2, timestamp, secret) {
  // 1. Reject anything older than 5 minutes
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp, 10)) > 300) return false;

  // 2. Recompute HMAC-SHA256 against the canonical JSON Didit signs
  const canonical = JSON.stringify(sortKeys(shortenFloats(JSON.parse(rawBody))));
  const expected = crypto.createHmac("sha256", secret).update(canonical, "utf8").digest("hex");

  // 3. Constant-time compare
  const a = Buffer.from(expected, "utf8");
  const b = Buffer.from(signatureV2, "utf8");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
```

→ [Full webhooks documentation](/integration/webhooks), including the raw-bytes `X-Signature`, the envelope-only `X-Signature-Simple`, and how to pick. Didit retries failed deliveries (`5xx`, `404`, timeout) up to 2 times (\~1 min, then \~4 min) and times out after **5 seconds** — return `2xx` fast.

### Via API (optional)

Fetch the same decision payload at any time:

```bash theme={null}
curl https://verification.didit.me/v3/session/{session_id}/decision/ \
  -H "x-api-key: YOUR_API_KEY"
```

The response shape (plural arrays) is identical to the `decision` object inside the webhook payload, and the endpoint can be called at any point in the session lifecycle. Use webhooks for real-time updates and reserve API calls for backfill, audit, and rebuilds — the `/decision/` endpoint is rate-limited to 600 GET requests/min per API key (see [Rate limiting](/integration/rate-limiting)). Error semantics: an unknown `session_id` returns `404` with `{"detail": "Not found."}`; authentication failures surface as `403` — this endpoint never returns `401`. Media URLs in the response are short-lived presigned links — fetch them promptly rather than persisting them.

→ [Retrieve session API reference](/sessions-api/retrieve-session)

***

## Complete Code Examples

### Node.js / Express

```javascript theme={null}
const express = require("express");
const crypto = require("crypto");
const app = express();

// Create verification session
app.post("/api/verify", async (req, res) => {
  const { userId } = req.body;

  const response = await fetch("https://verification.didit.me/v3/session/", {
    method: "POST",
    headers: {
      "x-api-key": process.env.DIDIT_API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      workflow_id: process.env.DIDIT_WORKFLOW_ID,
      callback: `${process.env.APP_URL}/verification-complete`,
      vendor_data: userId,
    }),
  });

  const session = await response.json();

  // Store session_id associated with user
  await db.users.update({
    where: { id: userId },
    data: { verificationSessionId: session.session_id },
  });

  res.json({
    verificationUrl: session.url,             // hosted verification URL
    sessionToken: session.session_token,      // for native/web SDKs
  });
});

// Handle webhook — uses verifyWebhook / shortenFloats / sortKeys from Step 5
app.post(
  "/api/webhook",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const signature = req.get("X-Signature-V2");
    const timestamp = req.get("X-Timestamp");

    if (
      !verifyWebhook(
        req.body.toString("utf8"),
        signature,
        timestamp,
        process.env.DIDIT_WEBHOOK_SECRET,
      )
    ) {
      return res.status(401).json({ error: "Invalid signature" });
    }

    const payload = JSON.parse(req.body);
    const { event_id, session_id, status, vendor_data } = payload;

    // Idempotency: the same event_id is reused on retries and fan-out.
    // Skip events you have already processed (e.g. a processed_events table).

    // Update user's verification status (status values are case-sensitive strings)
    if (status === "Approved") {
      db.users.update({
        where: { id: vendor_data },
        data: { isVerified: true, verifiedAt: new Date() },
      });
    }

    // Return 2xx quickly (Didit's delivery timeout is 5s); do heavy work asynchronously
    res.json({ received: true });
  },
);

app.listen(3000);
```

### Python / FastAPI

```python theme={null}
from fastapi import FastAPI, Request, HTTPException
import httpx
import hashlib
import hmac
import json
import os
import time

app = FastAPI()

@app.post("/api/verify")
async def create_verification(user_id: str):
    async with httpx.AsyncClient() as client:
        response = await client.post(
            "https://verification.didit.me/v3/session/",
            headers={
                "x-api-key": os.environ["DIDIT_API_KEY"],
                "Content-Type": "application/json"
            },
            json={
                "workflow_id": os.environ["DIDIT_WORKFLOW_ID"],
                "callback": f"{os.environ['APP_URL']}/verification-complete",
                "vendor_data": user_id
            }
        )

    session = response.json()
    return {
        "verification_url": session["url"],            # hosted verification URL
        "session_token": session["session_token"],     # for native/web SDKs
    }

def shorten_floats(data):
    """Match Didit: whole-valued floats serialise as ints."""
    if isinstance(data, dict):
        return {k: shorten_floats(v) for k, v in data.items()}
    if isinstance(data, list):
        return [shorten_floats(x) for x in data]
    if isinstance(data, float) and data.is_integer():
        return int(data)
    return data

@app.post("/api/webhook")
async def handle_webhook(request: Request):
    payload = await request.json()
    signature = request.headers.get("X-Signature-V2")
    timestamp = request.headers.get("X-Timestamp")

    # Verify timestamp freshness
    if not signature or not timestamp or abs(int(time.time()) - int(timestamp)) > 300:
        raise HTTPException(status_code=401, detail="Request too old")

    # Recompute Didit's V2 canonical JSON: shortened floats, sorted keys,
    # compact separators, Unicode preserved (ensure_ascii=False)
    canonical = json.dumps(
        shorten_floats(payload),
        sort_keys=True,
        separators=(",", ":"),
        ensure_ascii=False,
    )
    expected = hmac.new(
        os.environ["DIDIT_WEBHOOK_SECRET"].encode("utf-8"),
        canonical.encode("utf-8"),
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(signature, expected):
        raise HTTPException(status_code=401, detail="Invalid signature")

    # Idempotency: dedupe on payload["event_id"] (reused on retries)

    if payload["status"] == "Approved":
        # Update user in database
        pass

    return {"received": True}
```

***

## Session statuses

These are the 10 possible values of `status` — exact, case-sensitive strings (note `Kyc Expired` uses a single capital `K`):

| Status          | Description                                                   |
| --------------- | ------------------------------------------------------------- |
| `Not Started`   | Session created but user hasn't opened the link yet.          |
| `In Progress`   | User is currently completing verification.                    |
| `In Review`     | Verification flagged for human review.                        |
| `Approved`      | All checks passed.                                            |
| `Declined`      | One or more checks failed.                                    |
| `Resubmitted`   | Reviewer requested specific steps to be redone.               |
| `Awaiting User` | KYB parent session waiting for child KYC parties to finish.   |
| `Abandoned`     | User started but didn't finish in the allotted time.          |
| `Expired`       | Session timed out before the user opened the link.            |
| `Kyc Expired`   | Previously approved session passed its KYC expiration policy. |

See [Verification statuses](/integration/verification-statuses) for the full lifecycle, transitions, and Mermaid diagram.

***

## Next steps

1. **Choose your integration method**:
   * Mobile apps → [Native SDKs](/integration/native-sdks/overview)
   * Web apps → [Web SDKs](/integration/web-sdks/overview)
2. **Configure webhooks**: [Webhooks](/integration/webhooks)
3. **Plan capacity**: [Rate limiting](/integration/rate-limiting)
4. **Handle every status**: [Verification statuses](/integration/verification-statuses)
5. **Customize your workflow**: [Workflows](/console/workflows)
6. **Verify the API is reachable**: [Healthcheck](/api-reference/healthcheck)

## Need help?

* **Support**: [support@didit.me](mailto:support@didit.me)
* **Programmatic onboarding**: [Programmatic registration](/integration/programmatic-registration)
