Skip to main content
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 for the full flow, password rules, JWT lifetime, and lockout policy.

Option B: From the Didit Console

  1. Go to the Didit Console.
  2. Create or select your organization.
  3. Navigate to SettingsAPI & 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

# 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 ConsoleWorkflows
  2. Click Create Workflow
  3. Choose a base template:
TemplateBest For
KYCFull identity verification with ID document
Adaptive Age VerificationAge verification with optional ID backup
Biometric AuthenticationRe-verifying returning users
Address VerificationProof of address verification
  1. Add optional features:
FeatureDescription
NFC VerificationRead passport/ID chip data
Liveness DetectionPrevent spoofing with selfie video
Face MatchingMatch selfie to document photo
Phone VerificationVerify phone number via SMS
Email VerificationVerify email address
AML ScreeningCheck against sanctions/PEP lists
Database ValidationVerify against government databases
Device & IP AnalysisDetect VPNs, proxies, geolocation
  1. Copy your Workflow ID
See Workflows Documentation for detailed configuration options.

Step 3: Create a Verification Session

Call the API to create a session for your user.

Request

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

ParameterRequiredDescription
workflow_idYesYour workflow UUID from the Console.
callbackNoURL 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_methodNoWhich device handles the redirect: initiator, completer, or both. Default: initiator.
vendor_dataNoYour internal user identifier — echoed in webhooks for correlation.
metadataNoArbitrary JSON object stored with the session and echoed back in webhooks.
languageNoUI language hint (ISO 639-1, e.g. en, es). Browser language is auto-detected when omitted.
contact_detailsNoUser’s email/phone for prefill and email notifications.
expected_detailsNoExpected user details for cross-validation against verified data.
portrait_imageConditionalBase64-encoded face image (max 2MB; JPEG, PNG, WebP, or TIFF). Required for Biometric Authentication / Face-Match-first workflows.
sandbox_scenarioNoSandbox-only scenario slug (e.g. approve, decline_aml_hit) that auto-populates the session with magic inputs.

Full Example with All Options

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"]
    }
  }'
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 for the full schema.

Response

201 Created:
{
  "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

FieldDescription
session_idUnique UUID for this session. Use it for GET /v3/session/{sessionId}/decision/ and webhooks.
session_numberHuman-readable sequence number shown in the Didit console.
session_token12-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.
urlHosted 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.
statusCurrent status — "Not Started" for a newly created session.
workflow_idStable workflow identifier the session runs on.
workflow_versionPublished workflow version the session was pinned to at creation.
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.
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 for complete documentation.

Step 4: Present Verification to User

Choose how to present the verification flow to your user: Use the session_token with the native SDKs for camera, NFC, and biometric integration. iOS SDK · Android SDK · React Native · Flutter Embed the verification in your page using the url returned in Step 3.
<iframe
  src="{url}"
  style="width: 100%; height: 700px; border: none;"
  allow="camera; microphone; fullscreen; autoplay; encrypted-media"
></iframe>
InContext iframe

Option C: Redirect

Redirect the user to the verification URL.
window.location.href = session.url;
Web redirect

Option D: Mobile WebView

For mobile apps without a native SDK.
// React Native
<WebView source={{ uri: session.url }} />
WebView in iOS/Android

Step 5: Receive Results

Webhooks notify your server in real-time when verification status changes. Setup:
  1. Create a webhook destination in the Console (API & Webhooks) or via POST /v3/webhook/destinations/ 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.
{
  "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 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.
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, 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:
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). 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

Complete Code Examples

Node.js / Express

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

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):
StatusDescription
Not StartedSession created but user hasn’t opened the link yet.
In ProgressUser is currently completing verification.
In ReviewVerification flagged for human review.
ApprovedAll checks passed.
DeclinedOne or more checks failed.
ResubmittedReviewer requested specific steps to be redone.
Awaiting UserKYB parent session waiting for child KYC parties to finish.
AbandonedUser started but didn’t finish in the allotted time.
ExpiredSession timed out before the user opened the link.
Kyc ExpiredPreviously approved session passed its KYC expiration policy.
See Verification statuses for the full lifecycle, transitions, and Mermaid diagram.

Next steps

  1. Choose your integration method:
  2. Configure webhooks: Webhooks
  3. Plan capacity: Rate limiting
  4. Handle every status: Verification statuses
  5. Customize your workflow: Workflows
  6. Verify the API is reachable: Healthcheck

Need help?