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

# Entity Webhooks

> Real-time webhooks for Didit User and Business entity changes — status transitions, profile updates, and activity log events. Pay-per-call.

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="Entity Webhooks Prompt"
  prompt={`# Goal — subscribe to entity-level webhooks and apply changes to my database

Entity webhooks fire whenever a User or Business entity changes — status flips, profile updates, or activity timeline rows. Use them to keep your local mirror of Didit entities in sync.

## Entity-level event catalog

| \`webhook_type\` | Trigger |
|---|---|
| \`user.status.updated\` | User entity status changed (\`ACTIVE\` / \`FLAGGED\` / \`BLOCKED\`). Payload includes \`previous_status\`. |
| \`user.data.updated\` | User entity profile / counters / metadata changed. Includes \`changed_fields\` and a \`changes\` map of \`{previous, current}\`. |
| \`business.status.updated\` | Business entity status changed. |
| \`business.data.updated\` | Business entity profile or aggregates changed. |
| \`activity.created\` | A row was added to the entity activity timeline. |

Session-level events (\`status.updated\`, \`data.updated\`) and transaction events (\`transaction.created\`, \`transaction.status.updated\`) live alongside but are separate — see /integration/webhooks for the full catalog.

## Register a destination

\`\`\`bash
curl -X POST https://verification.didit.me/v3/webhook/destinations/ \\
-H "x-api-key: $DIDIT_API_KEY" \\
-H "Content-Type: application/json" \\
-d '{
"label": "Entity sync",
"url": "https://yourapp.com/webhooks/didit",
"webhook_version": "v3",
"subscribed_events": [
  "user.status.updated",
  "user.data.updated",
  "business.status.updated",
  "business.data.updated",
  "activity.created"
]
}'
\`\`\`

Persist the returned destination \`secret\` — that's the HMAC signing key.

## Envelope shape

\`\`\`json
{
"webhook_type": "user.status.updated",
"event_id": "01HXXX...",
"application_id": "app_abc123",
"timestamp": "2026-04-16T10:00:00Z",
"data": {
"vendor_data": "user-42",
"uuid": "9c7d2a30-...",
"status": "BLOCKED",
"previous_status": "ACTIVE",
"reason": "session_declined"
}
}
\`\`\`

\`user.data.updated\` / \`business.data.updated\` payloads add \`changed_fields\` (array) and \`changes\` (\`{field: {previous, current}}\`). Use these to apply minimal diffs to your DB.

## Verify the signature

Canonical pipeline for X-Signature-V2: \`shortenFloats\` → \`sortKeys\` → \`JSON.stringify\` → HMAC-SHA256 with the destination secret → constant-time compare. See /integration/webhooks for code samples. Respond \`2xx\` within **5 seconds**; otherwise Didit retries with exponential backoff up to ~24 hours.

## Idempotency

Every delivery carries a unique \`event_id\`. De-dupe on it. Fallback compound key: \`(application_id, webhook_type, vendor_data, timestamp)\`.

## Failure modes

- Wrong \`webhook_version\` (e.g. v2) → singular fields and older event names; v3 is required for entity events plural-array contract.
- Slow handler → retried; budget < 5s; do heavy work async.
- Unknown event type → log and 200; new event types may be added.

## Apply-decision pseudocode

\`\`\`ts
switch (event.webhook_type) {
case "user.status.updated":
await db.users.update({ vendor_data: event.data.vendor_data }, { status: event.data.status });
break;
case "user.data.updated":
for (const field of event.data.changed_fields) {
  await db.users.update({ vendor_data: event.data.vendor_data }, { [field]: event.data.changes[field].current });
}
break;
case "business.status.updated":
case "business.data.updated": /* same shape, businesses table */ break;
case "activity.created":
await db.activities.insert(event.data);
break;
}
\`\`\`

## Sources of truth

- /integration/webhooks — destinations, signatures, retries, full event catalog
- /management-api/webhook/create-destination — endpoint schema
- /entities/users/operations — operations that emit user events
- /entities/businesses/operations — operations that emit business events
- /integration/integration-prompt — canonical end-to-end prompt
`}
/>

Every change to a User or Business entity emits a webhook so your system can react in real time. Use entity webhooks to sync profile data, propagate blocks to other systems, or trigger downstream compliance flows.

## Event catalog

| Event                     | Trigger                                                                                        |
| ------------------------- | ---------------------------------------------------------------------------------------------- |
| `user.status.updated`     | A User entity's status changes (`ACTIVE` ↔ `FLAGGED` ↔ `BLOCKED`)                              |
| `user.data.updated`       | A User entity's profile fields change (name, DOB, portrait, aggregate counters, metadata)      |
| `business.status.updated` | A Business entity's status changes                                                             |
| `business.data.updated`   | A Business entity's profile fields change (legal name, registration number, country, counters) |
| `activity.created`        | A new activity is logged against an entity (manual action, automated check, note)              |

Didit also emits session, transaction, and business-session events that reference entity fields. See:

* [KYB webhooks](/business-verification/webhooks) — business session events
* [Transaction Monitoring webhooks](/transaction-monitoring/webhooks) — transaction events
* [Webhooks reference](/integration/webhooks) — signature verification, retries, destinations

## Payload shapes

All entity webhooks share a common envelope:

```json theme={null}
{
  "event": "user.status.updated",
  "application_id": "app_abc123",
  "timestamp": "2026-04-16T10:00:00Z",
  "data": {
    "vendor_data": "user-42",
    "uuid": "9c7d2a30-2f98-4d4f-9d2d-1a7c4b1e5b6a",
    "status": "BLOCKED",
    "previous_status": "ACTIVE",
    "reason": "session_declined",
    "actor": "system",
    "metadata": { ... }
  }
}
```

### `user.status.updated` / `business.status.updated`

Fires on every status change. The `reason` field tells you why the change happened:

| Reason               | Description                                                               |
| -------------------- | ------------------------------------------------------------------------- |
| `manual`             | An analyst changed the status from the console                            |
| `api`                | The status was changed via `PATCH /v3/users/{vendor_data}/update-status/` |
| `session_declined`   | Linked session was declined and auto-block is enabled                     |
| `blocklist_match`    | Entity matched a blocklist entry (face, document, email, phone)           |
| `transaction_rule`   | A transaction rule action moved the entity to FLAGGED or BLOCKED          |
| `ongoing_monitoring` | Periodic AML rescan surfaced a new hit                                    |

### `user.data.updated` / `business.data.updated`

Fires when profile fields change. The payload includes the full updated entity:

```json theme={null}
{
  "event": "user.data.updated",
  "application_id": "app_abc123",
  "timestamp": "2026-04-16T10:00:00Z",
  "data": {
    "vendor_data": "user-42",
    "uuid": "9c7d2a30-2f98-4d4f-9d2d-1a7c4b1e5b6a",
    "display_name": "Jane Doe",
    "full_name": "John Doe",
    "date_of_birth": "1990-01-01",
    "status": "ACTIVE",
    "session_count": 3,
    "approved_count": 2,
    "declined_count": 0,
    "in_review_count": 1,
    "features": {
      "id_verification": "APPROVED",
      "liveness": "APPROVED",
      "face_match": "APPROVED",
      "aml": "IN_REVIEW"
    },
    "changed_fields": ["full_name", "session_count", "in_review_count", "features"]
  }
}
```

Use `changed_fields` to efficiently sync only the fields that changed.

### `activity.created`

Fires whenever an activity record is created about a User or Business entity. Activities form the entity's timeline — the backend writes one for every material event, across five source categories:

| Source         | Examples                                                                                |
| -------------- | --------------------------------------------------------------------------------------- |
| `CUSTOMER`     | End-user actions (profile edits, Key People submissions)                                |
| `SYSTEM`       | Backend automation (status transitions, ongoing-monitoring rescans, threshold triggers) |
| `VERIFICATION` | KYC / KYB session lifecycle (session created, feature finished, decision rendered)      |
| `TRANSACTION`  | Transaction monitoring events (rule triggers, severity shifts, case openings)           |
| `REVIEW`       | Console reviewer actions (notes, tag changes, status overrides)                         |

Example payload:

```json theme={null}
{
  "event": "activity.created",
  "application_id": "app_abc123",
  "timestamp": "2026-04-18T10:05:00Z",
  "data": {
    "subject_kind": "USER",
    "subject_vendor_data": "user-42",
    "activity_type": "note_added",
    "source": "REVIEW",
    "status": null,
    "title": "Analyst note",
    "description": "Requested additional proof of address",
    "metadata": {},
    "custom_values": {},
    "occurred_at": "2026-04-18T10:05:00Z",
    "session_id": null,
    "business_session_id": null,
    "transaction_id": null
  }
}
```

Key fields:

* `subject_kind` — `USER`, `BUSINESS`, or `EXTERNAL` (a counterparty not registered as one of your entities).
* `subject_vendor_data` — the entity's `vendor_data` when applicable.
* `source` — one of the five categories above.
* `activity_type` — machine-readable type tag (e.g. `note_added`, `status_changed`, `session_completed`, `rule_triggered`).
* `title` / `description` — human-readable summary; safe to render in your timeline UI.
* `session_id`, `business_session_id`, `transaction_id` — FK identifiers when the activity is tied to a specific session or transaction.

Use `activity.created` to mirror the Didit timeline into your own audit / compliance log, trigger follow-up workflows on specific activity types, or power a customer-facing timeline view.

## Delivery, retries, and signing

All entity webhooks follow the standard delivery contract:

* **Signed** with HMAC-SHA256 via the `X-Didit-Signature` header (secret shared key on your destination).
* **Retried** with exponential backoff on non-2xx responses (1s → 2s → 4s → ... up to 5 retries over \~24 hours).
* **Idempotent** — every webhook has a unique `event_id` in the payload; de-dupe on your side.

See [Webhooks reference](/integration/webhooks) for destination setup and signature verification code samples.

## Subscribing

Subscribe to entity events on a webhook destination via the Management API or Business Console:

```bash theme={null}
curl -X POST https://verification.didit.me/v3/webhook/destinations/ \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "Entity sync",
    "url": "https://yourapp.com/webhooks/didit",
    "subscribed_events": [
      "user.status.updated",
      "user.data.updated",
      "business.status.updated",
      "business.data.updated",
      "activity.created"
    ]
  }'
```

## Next steps

<CardGroup cols={3}>
  <Card title="Webhooks reference" icon="bolt" href="/integration/webhooks">
    Destinations, signatures, retries, and verification.
  </Card>

  <Card title="Create destination" icon="plus" href="/management-api/webhook/create-destination">
    Register a new webhook endpoint.
  </Card>

  <Card title="User operations" icon="user-pen" href="/entities/users/operations">
    Changes that trigger user webhooks.
  </Card>
</CardGroup>
