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

# Transaction Monitoring Webhooks

> Webhook events for transactions: triggers, payload shapes, and HMAC signature verification. Real-time delivery. Pay-per-call $0.02 per transaction.

Transaction Monitoring emits three webhook events that together cover the full transaction lifecycle. All ride on the standard Didit webhook infrastructure with HMAC-SHA256 signing - see the [webhooks reference](/integration/webhooks) for destinations, signing, and testing.

## Event catalog

| Event                        | When it fires                                                                                                                                                                                                                              |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `transaction.created`        | A new transaction was created and initial evaluation completed. Also fires for inbound transactions minted by an after-deposit [Travel Rule sunrise registration](/transaction-monitoring/travel-rule#after-deposit-sunrise-registration). |
| `transaction.status.updated` | The transaction's status changed - from analyst review, remediation session resolution, wallet-ownership completion, or a Travel Rule outcome applying your `timeout_outcome`.                                                             |
| `travel_rule.status.updated` | A [Travel Rule](/transaction-monitoring/travel-rule) exchange transitioned, on both the outbound and any `INTERNAL`-rail inbound leg.                                                                                                      |

Entity-level events (`user.status.updated`, `business.status.updated`) can also fire as a consequence of transaction activity - see [entity webhooks](/entities/webhooks).

<Note>
  Sandbox applications do not emit transaction or travel-rule webhooks - test deliveries against a live application, or use **API & Webhooks → Try Webhook** in the console.
</Note>

## Envelope

Didit webhooks use a **flat** body: the event fields sit at the top level next to the envelope fields - there is no nested `data` object.
Every delivery carries:

| Field                      | Description                                                                                   |
| -------------------------- | --------------------------------------------------------------------------------------------- |
| `webhook_type`             | The event name, e.g. `transaction.created`.                                                   |
| `event_id`                 | Stable identifier for the event - the same `event_id` is reused on retries, so de-dupe on it. |
| `timestamp` / `created_at` | Unix epoch seconds when the delivery was dispatched.                                          |
| `application_id`           | The application the event belongs to.                                                         |
| `environment`              | `live` or `sandbox`.                                                                          |

## Payload shapes

### `transaction.created` and `transaction.status.updated`

Both events share one payload - a compact summary plus the [`action_required`](/transaction-monitoring/sdk-transaction-submission#required-user-actions) block (or `null`):

```json theme={null}
{
  "webhook_type": "transaction.created",
  "event_id": "9c0c8b8a-1111-4222-9333-666666666666",
  "timestamp": 1774970000,
  "created_at": 1774970000,
  "application_id": "11111111-2222-3333-4444-555555555555",
  "environment": "live",
  "transaction_id": "9c7d2a30-...",
  "txn_id": "tx-0001",
  "status": "IN_REVIEW",
  "score": 65,
  "severity": "MEDIUM",
  "amount": "500.00",
  "currency": "EUR",
  "direction": "OUTBOUND",
  "action_required": null
}
```

The payload is deliberately thin: on receipt, fetch the full record with [`GET /v3/transactions/{transaction_id}/`](/management-api/transactions/get) - it returns the complete detail (parties, `rule_runs`, `provider_results`, `travel_rule`, `cost_breakdown`, `decision_reason_code`), including *why* the status changed.

### `travel_rule.status.updated`

```json theme={null}
{
  "webhook_type": "travel_rule.status.updated",
  "event_id": "9c0c8b8a-1111-4222-9333-777777777777",
  "timestamp": 1774970000,
  "created_at": 1774970000,
  "application_id": "11111111-2222-3333-4444-555555555555",
  "environment": "live",
  "transaction_id": "22222222-3333-4444-5555-666666666666",
  "txn_id": "trv-2026-07-05-001",
  "travel_rule_status": "COMPLETED",
  "rail": "INTERNAL",
  "direction": "OUTBOUND"
}
```

Fires on every non-terminal transition; once a transfer reaches `FINISHED`, `CANCELLED`, or `EXPIRED`, no further travel-rule events are sent for it.

## Example status journeys

### Simple approval

```
transaction.created (APPROVED, score 0)
```

### Review-then-approve

```
transaction.created (IN_REVIEW, score 65)
...analyst reviews...
transaction.status.updated (status: APPROVED)
```

### Step-up verification

```
transaction.created (AWAITING_USER, action_required.type: verification_session)
...user completes the biometric re-check...
transaction.status.updated (status: APPROVED, action_required: null)
```

### Travel Rule transfer

```
transaction.created (APPROVED)  +  travel_rule.status.updated (AWAITING_COUNTERPARTY, rail: EMAIL)
...counterparty accepts the pickup...
travel_rule.status.updated (COMPLETED)
...you broadcast on-chain and PATCH the hash...
travel_rule.status.updated (FINISHED)
```

## Signature verification

Every delivery is HMAC-SHA256 signed with the destination's shared secret and carries three signature headers plus a timestamp:

| Header               | Scheme                                                                           |
| -------------------- | -------------------------------------------------------------------------------- |
| `X-Signature-V2`     | HMAC over the canonical JSON (sorted keys, Unicode preserved) - **recommended**. |
| `X-Signature`        | HMAC over the ASCII-escaped canonical JSON (legacy).                             |
| `X-Signature-Simple` | HMAC over `"{timestamp}:{session_id}:{status}:{webhook_type}"`.                  |
| `X-Timestamp`        | Unix epoch seconds - reject deliveries older than 5 minutes.                     |

```typescript theme={null}
import { createHmac, timingSafeEqual } from 'crypto';

function verify(rawBody: string, signatureV2: string, secret: string) {
  const canonical = JSON.stringify(
    Object.keys(JSON.parse(rawBody)).sort()
      .reduce((acc, k) => ({ ...acc, [k]: JSON.parse(rawBody)[k] }), {})
  );
  const expected = createHmac('sha256', secret).update(canonical).digest('hex');
  return timingSafeEqual(Buffer.from(signatureV2), Buffer.from(expected));
}
```

See the [webhooks reference](/integration/webhooks#signature-verification) for the full verification code, including the timestamp freshness check.

## Retries and delivery guarantees

* On a `5xx`, `404`, timeout, or connection failure, Didit retries up to **2 times**: roughly 1 minute after the initial failure, then roughly 4 more minutes after that.
* Retried deliveries reuse the same `event_id` - de-dupe on it in your handler.
* After the final failure, delivery for that event stops and your organization is notified; destinations are never silently disabled.
* Return a `2xx` fast (enqueue, then process) - slow ACKs risk timeouts and duplicate deliveries.

## Idempotency

Process each webhook exactly once by storing `event_id` in your own dedupe store. Re-delivery of the same event is possible when your endpoint ACKs slowly or disconnects mid-request.

## Subscribing

```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": "TM events",
    "url": "https://yourapp.com/webhooks/didit-tm",
    "subscribed_events": [
      "transaction.created",
      "transaction.status.updated",
      "travel_rule.status.updated"
    ]
  }'
```

## Next steps

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

  <Card title="Statuses" icon="list-check" href="/transaction-monitoring/statuses">
    Full state machine.
  </Card>

  <Card title="Integration guide" icon="book" href="/transaction-monitoring/integration-guide">
    Handling webhooks in production.
  </Card>
</CardGroup>
