> ## 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.

# KYB Integration Guide

> Production-ready KYB integration: architecture, sessions, hosted flows, webhooks, polling, and decisions. Pay-per-call $2.00 business verification.

export const AgentPromptAccordion = ({prompt, title = "AI Agent Integration Prompt"}) => {
  const [copied, setCopied] = React.useState(false);
  const handleCopy = e => {
    e.stopPropagation();
    if (!prompt) return;
    navigator.clipboard.writeText(prompt.trim()).then(() => {
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    });
  };
  const agents = ["Claude Code", "Codex", "Cursor", "Devin", "Windsurf", "GitHub Copilot"];
  return <div className="didit-agent-card">
      {}
      <div className="didit-agent-titlebar">
        <div className="didit-agent-dots" aria-hidden="true">
          <span className="didit-agent-dot didit-agent-dot-red"></span>
          <span className="didit-agent-dot didit-agent-dot-yellow"></span>
          <span className="didit-agent-dot didit-agent-dot-green"></span>
        </div>
        <span className="didit-agent-filename">{title}</span>
        <button type="button" className={`didit-agent-copy ${copied ? "didit-agent-copy-copied" : ""}`} onClick={handleCopy} title="Copy prompt to clipboard" aria-label={copied ? "Copied!" : "Copy prompt to clipboard"}>
          {copied ? <>
              <svg width="13" height="13" viewBox="0 0 16 16" fill="none">
                <path d="M3 8.5l3.5 3.5L13 4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
              </svg>
              <span>Copied</span>
            </> : <>
              <svg width="13" height="13" viewBox="0 0 16 16" fill="none">
                <rect x="5" y="5" width="9" height="9" rx="1.5" stroke="currentColor" strokeWidth="1.5" />
                <path d="M11 5V3.5A1.5 1.5 0 0 0 9.5 2h-6A1.5 1.5 0 0 0 2 3.5v6A1.5 1.5 0 0 0 3.5 11H5" stroke="currentColor" strokeWidth="1.5" />
              </svg>
              <span>Copy</span>
            </>}
        </button>
      </div>

      {}
      <pre className="didit-agent-body"><code>{prompt.trim()}</code></pre>

      {}
      <div className="didit-agent-footer">
        <span className="didit-agent-footer-label">Paste into</span>
        <div className="didit-agent-chips">
          {agents.map(name => <span key={name} className="didit-agent-chip">{name}</span>)}
        </div>
      </div>
    </div>;
};

<AgentPromptAccordion
  title="KYB Integration Prompt"
  prompt={`# Goal — ship a production-ready KYB (business verification) integration

KYB runs through the same \`/v3/session/\` endpoint as KYC. The session's workflow determines whether it's a KYC or KYB session — pass a KYB workflow id and Didit creates a business session automatically (\`session_kind: "business"\`).

## End-to-end flow

\`\`\`
You: POST /v3/session/ (KYB workflow_id, vendor_data) → session_id, url, session_token
You: deliver url to the business contact (email / your UI / iframe)
Biz: completes the hosted flow (registry confirm → Key People → documents → AML)
Didit → Webhook: status.updated with session_kind: "business"
You: GET /v3/session/{session_id}/decision/ → full KYB decision
You: branch on decision.status (APPROVED / DECLINED / IN_REVIEW / RESUBMITTED)
\`\`\`

## Create a KYB session

\`\`\`bash
curl -X POST https://verification.didit.me/v3/session/ \\
-H "x-api-key: $DIDIT_API_KEY" \\
-H "Content-Type: application/json" \\
-d '{
"workflow_id": "$KYB_WORKFLOW_ID",
"vendor_data": "biz-acme",
"callback": "https://myapp.com/kyb/return",
"metadata": {"plan": "growth", "country": "GBR"}
}'
\`\`\`

Recommended fields:

- \`workflow_id\` — references your KYB workflow.
- \`vendor_data\` — your business id; binds the session to a [Business entity](/entities/businesses/overview).
- \`callback\` — where to redirect the user after completion.
- \`metadata\` — JSON attached to the session for your own reporting.

## Webhook events for KYB

| Event | Trigger |
|---|---|
| \`status.updated\` (with \`data.session_kind === "business"\`) | KYB session status changed |
| \`data.updated\` (with \`data.session_kind === "business"\`) | KYB session data changed (registry refresh, Key People submission, document upload, ongoing AML) |
| \`business.status.updated\` | Linked Business entity status changed |
| \`business.data.updated\` | Linked Business entity profile / counters changed |

Subscribe via \`POST /v3/webhook/destinations/\` with \`webhook_version: "v3"\`. Verify the \`X-Signature-V2\` header (HMAC-SHA256 of canonical JSON). De-dupe on \`event_id\`. Respond within 5 seconds.

## KYB decision shape (V3 plural arrays)

\`\`\`bash
curl https://verification.didit.me/v3/session/$SESSION_ID/decision/ \\
-H "x-api-key: $DIDIT_API_KEY"
\`\`\`

Key V3 arrays you'll iterate:

| Array | Holds |
|---|---|
| \`key_people_checks[]\` | UBOs, shareholders, directors, officers — bucketed into \`registry\` (from the registry lookup) and \`submitted\` (what the business confirmed). Plus a \`ubo_kyc_summary\` rollup. |
| \`document_verifications[]\` | Each item has \`status\`, \`node_id\`, \`items[]\` (signed \`file_url\` + OCR extract), \`groups\`, \`required_groups\`. |
| \`aml_screenings[]\` | Company AML and per-person AML hits. |

The KYB-specific features are \`kyb_registry\`, \`kyb_company_aml\`, \`kyb_documents\`, \`kyb_key_people\`. Statuses: \`NOT_FINISHED\`, \`APPROVED\`, \`DECLINED\`, \`IN_REVIEW\`, \`RESUB_REQUESTED\`.

## Apply-decision logic

\`\`\`ts
switch (decision.status) {
case "Approved":   await onboardBusiness(decision); break;
case "Declined":   await logRejection(decision.decision_reason_code, decision); break;
case "In Review":  await markPending(decision); break; // wait for next webhook
case "Resubmitted":await notifyBusiness(decision); break; // business needs to re-upload
case "Awaiting User": await pingChildPersonForKyc(decision); break; // child Key People KYC pending
}
\`\`\`

See /business-verification/statuses for the full state machine.

## Resubmission flow

When a session transitions to \`RESUB_REQUESTED\` (or a feature is marked for resubmission), re-deliver the same \`url\` — the hosted flow resumes at the resubmission step. To re-verify a specific Key Person, resubmit that person's child KYC session; the parent \`kyb_key_people\` re-aggregates automatically.

## Key People child KYC

If your KYB workflow has KYC required for some roles, each person triggers a child KYC session automatically. While any child is in progress, the parent's \`kyb_key_people\` status is \`Awaiting User\`. Configure per-role required/skippable behaviour at *Workflows → [KYB workflow] → Key People*.

## Failure modes

| Scenario | Response |
|---|---|
| Invalid \`workflow_id\` | \`400 Bad Request\` |
| Wrong workflow type (KYC instead of KYB) | Session is created but runs KYC steps — verify the workflow's \`workflow_type\` is \`"kyb"\` |
| Duplicate \`vendor_data\` | A new session is created — that's by design for re-KYB |
| Dangerous country | Session auto-\`DECLINED\` with reason \`blocked_country\` |
| Blocklisted business | Session auto-\`DECLINED\` with reason \`blocked_business\` |
| Rate limit | \`429\` with \`Retry-After\` header |

## Sources of truth

- /sessions-api/create-session — request body schema
- /business-verification/response-schema — KYB decision payload
- /business-verification/statuses — feature state machine
- /business-verification/webhooks — KYB-specific event catalog
- /entities/businesses/data-model — Business entity schema
- /integration/integration-prompt — canonical end-to-end prompt
`}
/>

This guide is the companion to the [quickstart](/business-verification/quickstart). It covers the architecture, every integration decision, and the operational patterns that make KYB reliable in production.

## Architecture

```mermaid theme={null}
sequenceDiagram
    participant You as Your platform
    participant Didit as Didit API
    participant Biz as Business contact
    participant Webhook as Your webhook endpoint

    You->>Didit: POST /v3/session/ (workflow_id, vendor_data)
    Didit-->>You: session_id, url, token
    You->>Biz: Deliver hosted verification URL (your email/flow)
    Biz->>Didit: Completes KYB hosted flow
    Didit->>Didit: Registry lookup, AML screening, OCR
    Didit->>Webhook: status.updated (session_kind: business)
    You->>Didit: GET /v3/session/{id}/decision/
    Didit-->>You: Full KYB decision payload
    You->>You: Onboard / reject / escalate based on status
```

## Authentication

All requests require the `x-api-key` header with your application's API key. See [API authentication](/getting-started/api-authentication) for key rotation, environment separation, and scopes.

## Session creation

Single endpoint: `POST /v3/session/`. The workflow's type determines whether the session is KYC or KYB — no explicit "business" flag is needed.

**Recommended fields on creation:**

| Field              | Purpose                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `workflow_id`      | Required — references your KYB workflow.                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `vendor_data`      | Your identifier for the business. Binds the session to a [Business entity](/entities/businesses/overview).                                                                                                                                                                                                                                                                                                                                                                |
| `callback_url`     | Optional — where to redirect the user after completion.                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `expected_details` | Optional — `{ company_name, registry_country, registration_number }`. Pre-fills the company registry search: the country of incorporation is pre-selected and locked (`registry_country` as `XX`, or `XX-YY` for state-level registries, e.g. `US-CA`), the company-name and registration-number fields are pre-filled, and the asserted search runs automatically — combining both identifiers when both are present. Prefill only — mismatches do not produce warnings. |
| `metadata`         | Optional JSON attached to the session.                                                                                                                                                                                                                                                                                                                                                                                                                                    |

Full schema: [create session](/sessions-api/create-session).

## Delivering the hosted link

The response `url` is a hosted verification link. Options for delivery:

* Email it from your own platform.
* Embed it in your onboarding UI as a link.
* Open it in an in-app webview (iOS / Android / React Native).

You can **customize** the hosted experience — logo, color, domain, email templates — via [White-label](/console/white-label).

## Polling vs webhooks

Webhooks are the recommended pattern. Didit POSTs completion events to your subscribed endpoint as soon as processing finishes.

Polling is supported for development or when your webhook endpoint is unreachable:

* Poll `GET /v3/session/{id}/decision/` with exponential backoff (every 10s initially, up to 60s).
* Stop polling when `status` is one of `APPROVED`, `DECLINED`, `IN_REVIEW`.

<Warning>
  Polling at high frequency may hit [rate limits](/integration/rate-limiting). Always prefer webhooks in production.
</Warning>

## Webhook handling

Subscribe to the events you care about — the same events cover both KYC and KYB, with `session_kind: "business"` inside the payload to identify business-session events:

* `status.updated` — session status changed. Filter on `data.session_kind === "business"` for KYB sessions.
* `data.updated` — session data changed (registry refresh, key-people submission, documents, ongoing AML). Same `session_kind` filter applies.
* `business.status.updated` / `business.data.updated` — the linked [Business entity](/entities/businesses/overview) changed.

Full event catalog: [KYB webhooks](/business-verification/webhooks). Signature verification and retry semantics: [webhooks reference](/integration/webhooks).

## Handling decisions

```typescript theme={null}
switch (decision.status) {
  case 'APPROVED':
    await onboardBusiness(decision);
    break;
  case 'DECLINED':
    await logRejection(decision.decision_reason_code, decision);
    break;
  case 'IN_REVIEW':
    // Wait for analyst decision — another webhook will fire
    await markPending(decision);
    break;
  case 'RESUBMITTED':
    // The business was asked to re-upload a document
    await notifyBusiness(decision);
    break;
}
```

See [KYB statuses](/business-verification/statuses) for the full state machine and reason codes.

## Resubmission flow

When a Business Verification (KYB) session transitions to `RESUB_REQUESTED` (or a feature is marked for resubmission), the business needs to upload corrected or additional data.

You can:

* **Re-deliver** the same `url` — the hosted flow picks up where resubmission is needed.
* **Open a [case](/transaction-monitoring/cases)** in the Business Console to track the back-and-forth internally.

## Business profile aggregation

If you pass `vendor_data` on session creation, Didit aggregates all sessions for that business into a single [Business entity](/entities/businesses/overview). Use the entity's `features` map to answer "is this business fully verified right now?" without iterating sessions.

Benefits:

* Periodic re-KYB flows append to the same profile.
* Transactions monitored against the same `vendor_data` carry the business's risk context.
* UBOs and officers linked to User entities populate cross-entity relationships.

## Best practices

| Practice                            | Why                                                               |
| ----------------------------------- | ----------------------------------------------------------------- |
| Always pass `vendor_data`           | Without it, the session is orphaned and can't be aggregated.      |
| Use webhooks, not polling           | Lower latency, no rate limit pressure.                            |
| Verify webhook signatures           | Prevents spoofed events.                                          |
| Store the `session_id` on your side | Needed to retrieve the decision and generate PDFs.                |
| Tag sessions with `metadata`        | Simplifies reporting and debugging.                               |
| Set up a test workflow              | Use a sandbox workflow for integration testing before production. |

## Error handling

| Scenario                                  | Response                                                                     |
| ----------------------------------------- | ---------------------------------------------------------------------------- |
| Invalid `workflow_id`                     | `400 Bad Request`                                                            |
| Duplicate session with same `vendor_data` | Creates a new session — that's by design, since re-verification is expected. |
| Dangerous country                         | Session is created but immediately `DECLINED` with reason `blocked_country`. |
| Blocklisted `vendor_data`                 | Session auto-declined with reason `blocked_business`.                        |
| Rate limit                                | `429 Too Many Requests` with `Retry-After` header.                           |

Full [rate limiting](/integration/rate-limiting) reference.

## Next steps

<CardGroup cols={3}>
  <Card title="Webhooks" icon="bolt" href="/business-verification/webhooks">
    Every KYB event and payload.
  </Card>

  <Card title="Statuses" icon="list-check" href="/business-verification/statuses">
    State machine reference.
  </Card>

  <Card title="Response schema" icon="file-lines" href="/business-verification/response-schema">
    Decoding the KYB decision.
  </Card>
</CardGroup>
