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

# Webhooks

> Get notified when cards, cardholders, and transactions change

Webhooks push events to your server as they happen, so you don't have to poll. Register an endpoint, subscribe to the events you care about, and Agentcard delivers a signed JSON payload for each one — a card being used, a transaction settling, a balance running low.

Events fired by sandbox activity carry `livemode: false`; live activity carries `livemode: true`. **A destination belongs to one mode**: it's created in the mode you're viewing and receives only that mode's events — register one in each mode to run both.

## Register an endpoint

Webhook destinations live in the dashboard: open your organization → **Webhooks** → **Add destination**, pick the events, and paste your endpoint URL. Subscribe to **All events**, a whole family like `transaction.*`, or individual events — and change the selection any time.

Creating a destination reveals its **signing secret** (prefixed `whsec_`) — store it, you use it to verify every delivery's signature. You can reveal it again from the destination's page, and rotate it there too (for example after a suspected leak). The same page sends **test events** so you can prove your handler before any real traffic, pauses or deletes the destination, and shows a live view of recent deliveries with their response codes.

## The event payload

Every delivery is a POST with this envelope. Dedupe on `id` — the same event can be delivered more than once.

```json theme={null}
{
  "id": "evt_…",
  "type": "transaction.authorized",
  "created": 1751976000,
  "livemode": true,
  "data": {
    "id": "…",
    "card_id": "card_…",
    "last4": "4242",
    "amount_cents": 675,
    "merchant": "COFFEE SHOP #42 SAN FRANCISCO",
    "status": "PENDING",
    "balance_cents": 1825
  }
}
```

| Field      | Meaning                                                |
| ---------- | ------------------------------------------------------ |
| `id`       | Stable, idempotent event id (`evt_…`). Dedupe on this. |
| `type`     | The event type (see the catalog below).                |
| `created`  | Unix epoch seconds.                                    |
| `livemode` | `false` for sandbox/test events, `true` for live.      |
| `data`     | Event-specific payload; its shape depends on `type`.   |

## Verify the signature

Every delivery carries an `AgentCard-Signature` header so you can confirm it came from us and wasn't replayed. The header looks like:

```
AgentCard-Signature: t=1751976000,v1=<hex-hmac>
```

`v1` is an HMAC-SHA256 of `"{t}.{raw_request_body}"` keyed with your endpoint secret. Verify it against the **raw** body (before any JSON parsing), and reject timestamps that are too old to blunt replays.

<CodeGroup>
  ```js Node theme={null}
  import crypto from "node:crypto";

  // `rawBody` must be the exact bytes we sent — do not re-serialize parsed JSON.
  // Returns false (never throws) on a missing or malformed header, so a random
  // probe to your endpoint can't turn verification into a 500.
  export function verify(rawBody, header, secret, toleranceSeconds = 300) {
    if (typeof header !== "string" || !header) return false;
    const parts = Object.fromEntries(
      header.split(",").map((kv) => {
        const i = kv.indexOf("=");
        return i === -1 ? [kv, ""] : [kv.slice(0, i), kv.slice(i + 1)];
      }),
    );
    const t = Number(parts.t);
    if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
    const expected = crypto
      .createHmac("sha256", secret)
      .update(`${t}.${rawBody}`)
      .digest("hex");
    const given = Buffer.from(String(parts.v1 ?? ""), "utf8");
    const want = Buffer.from(expected, "utf8");
    return given.length === want.length && crypto.timingSafeEqual(given, want);
  }
  ```

  ```python Python theme={null}
  import hashlib, hmac, time

  def verify(raw_body: bytes, header: str, secret: str, tolerance=300) -> bool:
      parts = dict(kv.split("=", 1) for kv in header.split(","))
      t = int(parts.get("t", 0))
      if not t or abs(time.time() - t) > tolerance:
          return False
      expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
      return hmac.compare_digest(expected, parts.get("v1", ""))
  ```
</CodeGroup>

<Note>
  A legacy `X-AgentCard-Signature: sha256=<hex-hmac>` header (an HMAC of the body alone, no timestamp) is also sent for backward compatibility. New integrations should verify `AgentCard-Signature` — the timestamp gives you replay protection.
</Note>

Respond `2xx` to acknowledge. Any other status (or a timeout) is treated as a failure and retried.

## Delivery and retries

We attempt each delivery up to **5 times** with backoff on any non-`2xx` response, timeout, or network error. Requests time out after 10 seconds. The destination's page in the dashboard shows every recent delivery — status, attempt count, the response code we got, and when the next retry is scheduled.

An endpoint that keeps failing is eventually disabled; fix your handler, then re-enable the destination from its page.

## Event catalog

Subscribe to any of these by name (or `"*"` / a `"family.*"` prefix). Events are grouped below by the flow they belong to — each one shows a full example delivery. The envelope is always the same; only `data` changes shape.

| Event                                                                                 | Fires when                                                                                                             |
| ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| [`card.closed`](#card-closed)                                                         | A card is closed (one-time-use spend, manual close, or expiry).                                                        |
| [`card.created`](#card-created)                                                       | A card is issued.                                                                                                      |
| [`card.updated`](#card-updated)                                                       | A card's balance or status changes.                                                                                    |
| [`cardholder.created`](#cardholder-created)                                           | A cardholder is created.                                                                                               |
| [`cardholder.updated`](#cardholder-updated)                                           | A cardholder's details change.                                                                                         |
| [`cardholder_onboarding_session.completed`](#cardholder-onboarding-session-completed) | A hosted onboarding session finishes.                                                                                  |
| [`platform_connect_session.completed`](#platform-connect-session-completed)           | A company finished your hosted [platform connect](/companies/guides/connect-for-platforms) flow.                       |
| [`balance.low`](#balance-low)                                                         | A card's remaining balance drops into the low range.                                                                   |
| [`transaction.authorized`](#transaction-authorized)                                   | A charge is authorized (funds held).                                                                                   |
| [`transaction.cleared`](#transaction-cleared)                                         | A charge settles.                                                                                                      |
| [`transaction.declined`](#transaction-declined)                                       | A charge is declined.                                                                                                  |
| [`transaction.voided`](#transaction-voided)                                           | An authorization is reversed.                                                                                          |
| [`merchant.connected`](#merchant-connected)                                           | A cardholder links a merchant (e.g. DoorDash).                                                                         |
| [`reward.earned`](#reward-earned)                                                     | Your routed reward share granted tokens to a connected user.                                                           |
| [`card_flow.failed`](#card-flow-failed)                                               | The mint failed after funds moved; the allocation stays as user spending power.                                        |
| [`card_flow.started`](#card-flow-started)                                             | A user's company-funded card creation reserved your pool ([company wallet](/companies/wallet-funding/company-wallet)). |
| [`transfer.approved`](#transfer-approved)                                             | You (or auto-approve) confirmed collection for a company-funded card.                                                  |
| [`transfer.completed`](#transfer-completed)                                           | The funds became the user's spending power. Capture your customer here.                                                |
| [`transfer.failed`](#transfer-failed)                                                 | Company funding aborted before the card was created.                                                                   |
| [`transfer.initiated`](#transfer-initiated)                                           | The pool-to-user transfer was submitted on-chain.                                                                      |
| [`transfer.released`](#transfer-released)                                             | A company-funded card closed with unused balance.                                                                      |
| [`wallet.balance.low`](#wallet-balance-low)                                           | Your company wallet dropped under its threshold.                                                                       |
| [`wallet.funded`](#wallet-funded)                                                     | A deposit landed in your company wallet.                                                                               |
| [`identity.verification.updated`](#identity-verification-updated)                     | A connected user's identity verification status changes.                                                               |

## Card events

The lifecycle of a card: issued, balance or status changes, closed.

### card.closed

A card was closed. `reason` tells you why: `manual` (closed via the API), `single_use_spent` (a single-use card made its purchase), `balance_exhausted` (a multi-use card spent its full limit), or `provider_closed` (closed upstream by the card provider).

```json theme={null}
{
  "id": "evt_3d9e5f7a9b1c3d5e7f9a1b2c",
  "type": "card.closed",
  "created": 1751976100,
  "livemode": true,
  "data": {
    "id": "cmcx1a2b30001s8f9vwxyz012",
    "cardholder_id": "cmcw9z8y70004s8f9stuv6789",
    "last4": "4242",
    "status": "CLOSED",
    "reason": "single_use_spent"
  }
}
```

### card.created

A card was issued — via the API, or minted at the end of a company-funded flow. When the card was company-funded, `transfer_id` carries the transfer it was minted for so you can correlate it to your collection; it's `null` for direct issuance.

```json theme={null}
{
  "id": "evt_6b9c1a4d2e8f5a7b3c0d9e1f",
  "type": "card.created",
  "created": 1751976000,
  "livemode": true,
  "data": {
    "id": "cmcx1a2b30001s8f9vwxyz012",
    "cardholder_id": "cmcw9z8y70004s8f9stuv6789",
    "last4": "4242",
    "expiry": "07/29",
    "spend_limit_cents": 2500,
    "balance_cents": 2500,
    "status": "ACTIVE",
    "created_at": "2026-07-08T12:00:00.000Z",
    "transfer_id": "owt_9f8e7d6c5b4a3f2e1d0c9b8a"
  }
}
```

### card.updated

A card's balance or status changed: the balance moving after a transaction, or a multi-use card being paused, resumed, or given a new spend limit (`status` is `PAUSED` while the card is paused). Not fired when the change is the card closing; that transition is covered by `card.closed`.

```json theme={null}
{
  "id": "evt_2c8d4e6f8a0b2c4d6e8f0a1b",
  "type": "card.updated",
  "created": 1751976040,
  "livemode": true,
  "data": {
    "id": "cmcx1a2b30001s8f9vwxyz012",
    "cardholder_id": "cmcw9z8y70004s8f9stuv6789",
    "last4": "4242",
    "status": "ACTIVE",
    "balance_cents": 1825,
    "spend_limit_cents": 2500
  }
}
```

## Cardholder and onboarding events

The people you issue cards to — created directly via the API or through a hosted onboarding session.

### cardholder.created

A cardholder was created — via `POST /api/v1/cardholders`, or by a hosted onboarding session that created a brand-new cardholder (claims of an existing one don't re-fire this).

```json theme={null}
{
  "id": "evt_4e0f6a8b0c2d4e6f8a0b2c3d",
  "type": "cardholder.created",
  "created": 1751976000,
  "livemode": true,
  "data": {
    "id": "cmcw9z8y70004s8f9stuv6789",
    "first_name": "Ada",
    "last_name": "Lovelace",
    "email": "ada@example.com",
    "date_of_birth": "1994-03-15",
    "phone_number": "+14155550123",
    "created_at": "2026-07-08T11:58:00.000Z"
  }
}
```

### cardholder.updated

A cardholder's details changed. `updated_fields` lists exactly which fields the update touched.

```json theme={null}
{
  "id": "evt_5f1a7b9c1d3e5f7a9b1c3d4e",
  "type": "cardholder.updated",
  "created": 1751976200,
  "livemode": true,
  "data": {
    "id": "cmcw9z8y70004s8f9stuv6789",
    "first_name": "Ada",
    "last_name": "Lovelace",
    "email": "ada@newdomain.com",
    "date_of_birth": "1994-03-15",
    "phone_number": "+14155550123",
    "updated_fields": ["email"],
    "updated_at": "2026-07-08T12:03:20.000Z"
  }
}
```

### cardholder\_onboarding\_session.completed

A hosted onboarding session finished. `cardholder_id` is the resulting cardholder, `external_user_id` is your id for the user, and `state` echoes the opaque value you passed when creating the session. `claimed_existing_cardholder` is `true` when the session attached to a cardholder already in your org's books instead of creating a new one.

```json theme={null}
{
  "id": "evt_6a2b8c0d2e4f6a8b0c2d4e5f",
  "type": "cardholder_onboarding_session.completed",
  "created": 1751975880,
  "livemode": true,
  "data": {
    "id": "cos_1b2c3d4e5f6a7b8c9d0e1f2a",
    "status": "completed",
    "cardholder_id": "cmcw9z8y70004s8f9stuv6789",
    "external_user_id": "user_8842",
    "email": "ada@example.com",
    "claimed_existing_cardholder": false,
    "state": "sess-a1b2c3",
    "completed_at": "2026-07-08T11:58:00.000Z"
  }
}
```

### platform\_connect\_session.completed

A company finished your hosted [platform connect](/companies/guides/connect-for-platforms) flow. The payload never carries credentials — treat it as a nudge, verify `state`, and [exchange the code](/companies/api/reference/platform-connect-exchange) from the redirect server-side (or poll the session by id).

```json theme={null}
{
  "id": "evt_7b3c9d1e3f5a7b9c1d3e5f6a",
  "type": "platform_connect_session.completed",
  "created": 1753305480,
  "livemode": true,
  "data": {
    "session_id": "pcs_2c3d4e5f6a7b8c9d0e1f2a3b",
    "state": "af0ifjsldkj",
    "connected_organization_id": "cmcw9z8y70004s8f9stuv6789"
  }
}
```

## Transaction events

The life of a charge: authorized (funds held) → cleared (settled), or declined / voided. `balance.low` rides along when a charge leaves the card nearly empty.

### balance.low

A transaction left the card's remaining balance at \$10 or less (but above zero). Fires alongside the transaction event, so you can top up or warn the user before the next charge declines.

```json theme={null}
{
  "id": "evt_1f7a3b5c7d9e1f3a5b7c9d0e",
  "type": "balance.low",
  "created": 1751976000,
  "livemode": true,
  "data": {
    "card_id": "cmcx1a2b30001s8f9vwxyz012",
    "last4": "4242",
    "balance_cents": 850
  }
}
```

### transaction.authorized

A charge was authorized on a card — the funds are held. `id` is the provider's transaction id (dedupe transactions on it; it stays stable through clearing), and `balance_cents` is the card's remaining balance after the hold.

```json theme={null}
{
  "id": "evt_7b3c9d1e3f5a7b9c1d3e5f6a",
  "type": "transaction.authorized",
  "created": 1751976000,
  "livemode": true,
  "data": {
    "id": "txn_01HZY3V8KQ4W",
    "card_id": "cmcx1a2b30001s8f9vwxyz012",
    "last4": "4242",
    "amount_cents": 675,
    "merchant": "COFFEE SHOP #42 SAN FRANCISCO",
    "mcc": "5814",
    "status": "PENDING",
    "balance_cents": 1825
  }
}
```

### transaction.cleared

An authorized charge settled. Same shape as `transaction.authorized`, with `status` now final.

```json theme={null}
{
  "id": "evt_8c4d0e2f4a6b8c0d2e4f6a7b",
  "type": "transaction.cleared",
  "created": 1752062400,
  "livemode": true,
  "data": {
    "id": "txn_01HZY3V8KQ4W",
    "card_id": "cmcx1a2b30001s8f9vwxyz012",
    "last4": "4242",
    "amount_cents": 675,
    "merchant": "COFFEE SHOP #42 SAN FRANCISCO",
    "mcc": "5814",
    "status": "SETTLED",
    "balance_cents": 1825
  }
}
```

### transaction.declined

A charge was declined — no funds moved. `decline_reason` says why (only present on declines).

```json theme={null}
{
  "id": "evt_9d5e1f3a5b7c9d1e3f5a7b8c",
  "type": "transaction.declined",
  "created": 1751976300,
  "livemode": true,
  "data": {
    "id": "txn_01HZY4M2XR7P",
    "card_id": "cmcx1a2b30001s8f9vwxyz012",
    "last4": "4242",
    "amount_cents": 129900,
    "merchant": "ELECTRONICS WAREHOUSE",
    "mcc": "5732",
    "status": "DECLINED",
    "balance_cents": 1825,
    "decline_reason": "INSUFFICIENT_FUNDS"
  }
}
```

### transaction.voided

An authorization was reversed before settling — the held funds return to the card's balance.

```json theme={null}
{
  "id": "evt_0e6f2a4b6c8d0e2f4a6b8c9d",
  "type": "transaction.voided",
  "created": 1751977000,
  "livemode": true,
  "data": {
    "id": "txn_01HZY3V8KQ4W",
    "card_id": "cmcx1a2b30001s8f9vwxyz012",
    "last4": "4242",
    "amount_cents": 675,
    "merchant": "COFFEE SHOP #42 SAN FRANCISCO",
    "mcc": "5814",
    "status": "VOIDED",
    "balance_cents": 2500
  }
}
```

## Merchant connection events

Cardholders linking merchant accounts for agent-driven shopping.

### merchant.connected

A cardholder linked a merchant account (e.g. DoorDash) — useful to proactively tell the user their connection is live. Fires on live activity only.

```json theme={null}
{
  "id": "evt_2a8b4c6d8e0f2a4b6c8d0e1f",
  "type": "merchant.connected",
  "created": 1751976500,
  "livemode": true,
  "data": {
    "userId": "cmcv7x6w50002s8f9qrst4567",
    "clientId": "client_3f2e1d0c9b8a",
    "merchantSlug": "doordash",
    "merchantAccountRef": "ada@example.com"
  }
}
```

## Reward events

[Tokenback](/personal/concepts/rewards) your organization routes to its users. Configure the routed share with `rewardShareBps` (0 to 10000 basis points of your revenue share) on `PATCH /orgs/:orgId/revenue-config`.

### reward.earned

A settled charge granted tokens to a connected user out of your routed reward share. `tokens` counts only the tokens your share funded (1 token = 1 cent of credit value), `user_id` is the user who earned them, and `card_id` is the card whose settlement triggered the grant.

```json theme={null}
{
  "id": "evt_9e5f1a3b5c7d9e1f3a5b7c8d",
  "type": "reward.earned",
  "created": 1752076800,
  "livemode": true,
  "data": {
    "tokens": 120,
    "merchant": "OPENAI *CHATGPT SUBSCR",
    "card_id": "cmcx1a2b30001s8f9vwxyz012",
    "user_id": "cmcv7x6w50002s8f9qrst4567"
  }
}
```

## Company wallet events

The [company wallet](/companies/wallet-funding/company-wallet) funding flow, in firing order: `card_flow.started` (hold your customer) → `transfer.approved` → `transfer.initiated` → `transfer.completed` (capture your customer) → `card.created`. The failure and release paths (`transfer.failed`, `card_flow.failed`, `transfer.released`) and the pool treasury events (`wallet.funded`, `wallet.balance.low`) round out the family.

### card\_flow\.failed

The card mint failed after funds had already moved. You keep your capture: `funds_status: released_to_headroom` means the money is real spending power for that cardholder and automatically nets against their next company-funded card.

```json theme={null}
{
  "id": "evt_9b5c1d3e5f7a9b1c3d5e7f8a",
  "type": "card_flow.failed",
  "created": 1751976120,
  "livemode": true,
  "data": {
    "transfer_id": "owt_9f8e7d6c5b4a3f2e1d0c9b8a",
    "cardholder_id": "cmcw9z8y70004s8f9stuv6789",
    "amount_cents": 2500,
    "reason": "mint_failed",
    "funds_status": "released_to_headroom"
  }
}
```

### card\_flow\.started

A user's company-funded card creation reserved funds in your pool — your moment to collect from your customer ([company wallet](/companies/wallet-funding/company-wallet)). When `ack_required` is `true`, confirm collection via `POST /api/v1/wallet/transfers/:id/collection_succeeded` before `ack_deadline`; with auto-approve it's informational.

```json theme={null}
{
  "id": "evt_3b9c5d7e9f1a3b5c7d9e1f2a",
  "type": "card_flow.started",
  "created": 1751976000,
  "livemode": true,
  "data": {
    "transfer_id": "owt_9f8e7d6c5b4a3f2e1d0c9b8a",
    "cardholder_id": "cmcw9z8y70004s8f9stuv6789",
    "external_user_id": "user_8842",
    "amount_cents": 2500,
    "fee_cents": 25,
    "transfer_cents": 2525,
    "ack_required": true,
    "ack_deadline": "2026-07-08T12:05:00.000Z",
    "client_id": "client_3f2e1d0c9b8a"
  }
}
```

### transfer.approved

Collection was confirmed for a company-funded card — the pool transfer proceeds. `approved_via` says how it was confirmed.

```json theme={null}
{
  "id": "evt_4c0d6e8f0a2b4c6d8e0f2a3b",
  "type": "transfer.approved",
  "created": 1751976030,
  "livemode": true,
  "data": {
    "transfer_id": "owt_9f8e7d6c5b4a3f2e1d0c9b8a",
    "approved_via": "api"
  }
}
```

### transfer.completed

The funds became the user's spending power — the funding is final from here. **Capture your customer's payment on this event.**

```json theme={null}
{
  "id": "evt_6e2f8a0b2c4d6e8f0a2b4c5d",
  "type": "transfer.completed",
  "created": 1751976070,
  "livemode": true,
  "data": {
    "transfer_id": "owt_9f8e7d6c5b4a3f2e1d0c9b8a",
    "cardholder_id": "cmcw9z8y70004s8f9stuv6789",
    "amount_cents": 2500,
    "transferred_cents": 2525,
    "tx_hash": "0x7f3a9b2c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8"
  }
}
```

### transfer.failed

Company funding aborted before the card was created — release your hold on the customer. `reason` is one of `insufficient_org_funds`, `ack_timeout`, `ack_denied`, or `transfer_failed`.

```json theme={null}
{
  "id": "evt_7f3a9b1c3d5e7f9a1b3c5d6e",
  "type": "transfer.failed",
  "created": 1751976310,
  "livemode": true,
  "data": {
    "transfer_id": "owt_9f8e7d6c5b4a3f2e1d0c9b8a",
    "cardholder_id": "cmcw9z8y70004s8f9stuv6789",
    "amount_cents": 2500,
    "reason": "ack_timeout"
  }
}
```

### transfer.initiated

The pool-to-user transfer was submitted on-chain. `tx_hash` is the transaction hash when known — `null` in sandbox and for fully headroom-netted transfers where nothing moves on-chain.

```json theme={null}
{
  "id": "evt_5d1e7f9a1b3c5d7e9f1a3b4c",
  "type": "transfer.initiated",
  "created": 1751976035,
  "livemode": true,
  "data": {
    "transfer_id": "owt_9f8e7d6c5b4a3f2e1d0c9b8a",
    "cardholder_id": "cmcw9z8y70004s8f9stuv6789",
    "amount_cents": 2500,
    "transferred_cents": 2525,
    "tx_hash": "0x7f3a9b2c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8"
  }
}
```

### transfer.released

A company-funded card closed with unused balance — you can release or refund your own hold for `released_cents`. The unused amount stays as the user's spending power and offsets their next company-funded card.

```json theme={null}
{
  "id": "evt_8a4b0c2d4e6f8a0b2c4d6e7f",
  "type": "transfer.released",
  "created": 1752148800,
  "livemode": true,
  "data": {
    "transfer_id": "owt_9f8e7d6c5b4a3f2e1d0c9b8a",
    "card_id": "cmcx1a2b30001s8f9vwxyz012",
    "released_cents": 675,
    "reason": "card_closed_unused"
  }
}
```

### wallet.balance.low

Your company wallet dropped under the low-balance threshold you configured. Sent at most once per alert window, not on every check — top up before card creations start failing.

```json theme={null}
{
  "id": "evt_1d7e3f5a7b9c1d3e5f7a9b0c",
  "type": "wallet.balance.low",
  "created": 1751976600,
  "livemode": true,
  "data": {
    "balance_cents": 8450,
    "threshold_cents": 10000
  }
}
```

### wallet.funded

A USDC deposit landed in your company wallet and was booked. `amount_cents` is the deposit, `balance_cents` the pool's on-chain balance after it.

```json theme={null}
{
  "id": "evt_0c6d2e4f6a8b0c2d4e6f8a9b",
  "type": "wallet.funded",
  "created": 1751975000,
  "livemode": true,
  "data": {
    "amount_cents": 500000,
    "balance_cents": 623450
  }
}
```

## Identity verification events

The [identity verification](/companies/api/identity-verification) flow. One event follows a connected user's verification from start to finish — drive your UI from it instead of polling `GET /api/v2/kyc`.

### identity.verification.updated

Fires whenever a connected user's verification status changes. `user_id` is the connected user. `status` is one of `awaiting_documents`, `needs_information`, `requires_verification`, `pending`, `approved`, or `rejected`. When the status is `needs_information`, `required_fields` lists exactly what to collect next; when it's `requires_verification`, `iframe_url` is the face-scan URL to show the user (short-lived — always render the freshest one, don't store it). Non-terminal statuses (and `rejected`) may also carry `reason` — an end-user-safe sentence explaining what the review asked for, safe to show verbatim.

Statuses are not one-way: a review can send the user back — `pending` may return to `needs_information` (a detail didn't match the document; re-collect and resubmit the listed fields) or to `awaiting_documents` (unusable images; upload both sides again). Drive your UI from the current status of every event — see [when a review sends the user back](/companies/api/identity-verification#when-a-review-sends-the-user-back).

A `rejected` event can additionally carry `reason_code: "duplicate_identity"`: the person is already verified on another Agentcard account. That rejection is not fixed by new documents; the user resolves it by linking the other account from the verification page, and the same verification then reads `approved` on the next poll. See [duplicate documents](/companies/api/identity-verification#duplicate-documents).

To exercise this event end to end with a test-mode credential, [simulate an outcome](/companies/api/reference/kyc-simulate) — the simulated verdict fires the same event (with `livemode: false`) to your test-mode endpoints.

```json theme={null}
{
  "id": "evt_1a2b3c4d5e6f7a8b9c0d1e2f",
  "type": "identity.verification.updated",
  "created": 1751976800,
  "livemode": true,
  "data": {
    "user_id": "user_8842",
    "status": "approved"
  }
}
```

When more details are needed, the same event carries the fields to collect —
and, when the review asked for a correction, a `reason` to show the user:

```json theme={null}
{
  "id": "evt_2b3c4d5e6f7a8b9c0d1e2f3a",
  "type": "identity.verification.updated",
  "created": 1751976500,
  "livemode": true,
  "data": {
    "user_id": "user_8842",
    "status": "needs_information",
    "required_fields": ["first_name", "last_name"],
    "reason": "Enter your full name exactly as it appears on your identity document."
  }
}
```

And when the user is ready for the face scan, it carries the URL to show them:

```json theme={null}
{
  "id": "evt_3c4d5e6f7a8b9c0d1e2f3a4b",
  "type": "identity.verification.updated",
  "created": 1751976600,
  "livemode": true,
  "data": {
    "user_id": "user_8842",
    "status": "requires_verification",
    "iframe_url": "https://…"
  }
}
```

A rejection with a machine-readable class carries `reason_code`:

```json theme={null}
{
  "id": "evt_4d5e6f7a8b9c0d1e2f3a4b5c",
  "type": "identity.verification.updated",
  "created": 1751976700,
  "livemode": true,
  "data": {
    "user_id": "user_8842",
    "status": "rejected",
    "reason": "These documents are already verified on another Agentcard account.",
    "reason_code": "duplicate_identity"
  }
}
```
