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

# Webhooks

> Inbound provider callbacks and outbound Apocor events.

## Inbound — Core API (target)

Provider webhooks are received by the **Apocor Core API**. Register these HTTPS URLs in each vendor dashboard (use your environment host).

| Provider      | Path                          | Sandbox                                           | Production                                     |
| ------------- | ----------------------------- | ------------------------------------------------- | ---------------------------------------------- |
| **Sumsub**    | `POST /v1/webhooks/sumsub`    | `https://sandbox.apocor.ai/v1/webhooks/sumsub`    | `https://api.apocor.com/v1/webhooks/sumsub`    |
| **Interlace** | `POST /v1/webhooks/interlace` | `https://sandbox.apocor.ai/v1/webhooks/interlace` | `https://api.apocor.com/v1/webhooks/interlace` |
| **Dynamic**   | `POST /v1/webhooks/dynamic`   | `https://sandbox.apocor.ai/v1/webhooks/dynamic`   | `https://api.apocor.com/v1/webhooks/dynamic`   |

| Env var                      | Purpose                                                                |
| ---------------------------- | ---------------------------------------------------------------------- |
| `APOCOR_PUBLIC_API_URL`      | Public Core API base (drives `sumsubWebhookUrl` when mode is `apocor`) |
| `SUMSUB_WEBHOOK_MODE=apocor` | Route Sumsub callbacks to Core API (required for cutover)              |
| `SUMSUB_WEBHOOK_SECRET`      | HMAC verification for Sumsub                                           |
| `INTERLACE_WEBHOOK_SECRET`   | Signature verification for Interlace                                   |
| `DYNAMIC_WEBHOOK_SECRET`     | HMAC verification for Dynamic (`x-dynamic-signature-256`)              |

`GET /v1/kyc/config` returns `sumsubWebhookUrl` and `sumsubWebhookMode` for your environment — use that URL when registering Sumsub.

After hosted Sumsub verification, you can still call **`POST /v1/applicants/{id}/submit`** to sync tenant KYC and trigger issuer handoff. Poll **`GET /v1/applicants/{id}/kyc-status`** for issuer progress — see [KYC integration](/guides/kyc-integration).

Cutover steps (dashboards + Railway env): see `scripts/aws/WEBHOOK_CUTOVER.md` in the monorepo.

***

## Legacy — seismic-cards (migration only)

Older deployments used the **seismic-cards** Railway service with `SUMSUB_WEBHOOK_MODE=seismic` and path `/webhooks/sumsub` (no `/v1`). That path is **legacy** — do not register new Sumsub webhooks there. Keep seismic only until the Sumsub dashboard points at Core API, then set `SUMSUB_WEBHOOK_MODE=apocor`.

|          | Legacy (do not use for new setup)                            |
| -------- | ------------------------------------------------------------ |
| **URL**  | `https://api-production-XXXX.up.railway.app/webhooks/sumsub` |
| **Mode** | `SUMSUB_WEBHOOK_MODE=seismic` + `SEISMIC_SUMSUB_WEBHOOK_URL` |

***

## Outbound — Apocor → your app

Apocor will send **outbound webhooks** to your endpoint so you can react to events without polling — an applicant is approved, a card changes state, or a credit purchase settles.

<Note>
  Outbound delivery is not yet available on all endpoints (`POST /v1/webhooks` returns not implemented). The sections below describe the planned contract.
</Note>

## Delivering events

You register an HTTPS endpoint and the events you care about. Apocor `POST`s a JSON body to that URL for each matching event.

```json Example event theme={null}
{
  "id": "evt_abc123",
  "type": "card.activated",
  "createdAt": "2026-07-07T10:15:00.000Z",
  "data": { "id": "card_abc123", "status": "ACTIVE" }
}
```

## Verifying signatures

Each delivery is signed with the secret from your webhook subscription. Compute an HMAC-SHA256 over the raw request body and compare it, in constant time, to the signature header.

```ts theme={null}
import crypto from "node:crypto";

function verify(rawBody: string, signature: string, secret: string): boolean {
  const digest = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(digest);
  const b = Buffer.from(signature);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
```

<Warning>
  Always verify against the **raw** request body before parsing JSON. Reject any request whose signature does not match.
</Warning>

## Best practices

<CardGroup cols={2}>
  <Card title="Respond fast" icon="bolt">
    Return `2xx` immediately and process asynchronously. Slow handlers risk timeouts and retries.
  </Card>

  <Card title="Be idempotent" icon="arrows-rotate">
    Deliveries may repeat. De-duplicate on the event `id`.
  </Card>

  <Card title="Verify every time" icon="shield-check">
    Check the signature on every request, not just the first.
  </Card>

  <Card title="Use HTTPS" icon="lock">
    Only register TLS endpoints so payloads stay encrypted in transit.
  </Card>
</CardGroup>
