> For the complete documentation index, see [llms.txt](https://docs.voveid.com/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.voveid.com/docs/kyc-api/webhooks.md).

# Webhooks

Receive and process VOVE ID webhooks with Svix signature verification.

VOVE ID sends HTTPS webhook requests when verification, KYB, or AML resources change. Verify every request before parsing or processing it.

## Configure an endpoint

1. Open **Webhooks** in the matching Sandbox or Production dashboard.
2. Add a publicly reachable HTTPS endpoint.
3. Select the event types required by your integration.
4. Store the generated signing secret in your server secret manager.
5. Send or replay a fictional Sandbox event.

Sandbox and Production endpoints use separate signing secrets.

## Request headers

| Header           | Type   | Description                                        |
| ---------------- | ------ | -------------------------------------------------- |
| `svix-id`        | string | Unique delivery identifier; use it for idempotency |
| `svix-timestamp` | string | Signed delivery timestamp                          |
| `svix-signature` | string | One or more versioned signatures                   |
| `content-type`   | string | `application/json`                                 |

Header names are case-insensitive. Frameworks commonly expose them in lowercase.

## Representative KYC event

```http
POST /webhooks/vove HTTP/1.1
Host: example.test
Content-Type: application/json
Svix-Id: msg_example_01
Svix-Timestamp: 1788775200
Svix-Signature: v1,example_signature

{
  "status": "successful",
  "refId": "customer-7f3d",
  "userId": "customer-7f3d"
}
```

> **Deprecated:** `userId` is retained only for backward compatibility and will be removed in a future version. Use `refId` to identify and correlate users.

## Verify the raw request body

Install the official Svix package:

```bash
npm install svix
```

### Node.js and Express

```javascript
import express from 'express';
import { Webhook } from 'svix';

const app = express();

// Register this route before app.use(express.json()).
app.post('/webhooks/vove', express.raw({ type: 'application/json' }), async (req, res) => {
  const headers = {
    'svix-id': req.header('svix-id'),
    'svix-timestamp': req.header('svix-timestamp'),
    'svix-signature': req.header('svix-signature'),
  };

  let event;

  try {
    event = new Webhook(process.env.VOVE_WEBHOOK_SECRET).verify(req.body, headers);
  } catch {
    return res.sendStatus(400);
  }

  const deliveryId = headers['svix-id'];
  await persistAndEnqueueOnce(deliveryId, event);

  return res.sendStatus(204);
});
```

The verifier must receive the original bytes. `express.json()` changes the body representation, which makes signature verification fail.

### Python and Flask

```python
import os

from flask import Flask, request
from svix.webhooks import Webhook, WebhookVerificationError

app = Flask(__name__)

@app.post("/webhooks/vove")
def receive_vove_webhook():
    payload = request.get_data()
    headers = {
        "svix-id": request.headers.get("svix-id"),
        "svix-timestamp": request.headers.get("svix-timestamp"),
        "svix-signature": request.headers.get("svix-signature"),
    }

    try:
        event = Webhook(os.environ["VOVE_WEBHOOK_SECRET"]).verify(
            payload,
            headers,
        )
    except WebhookVerificationError:
        return "", 400

    persist_and_enqueue_once(headers["svix-id"], event)
    return "", 204
```

## Processing contract

1. Read the unmodified request bytes.
2. Verify the timestamp and signature with the official library.
3. Deduplicate on `svix-id` using durable storage and a unique constraint.
4. Store the verified event and enqueue work in one transaction where possible.
5. Return `200` or `204` quickly.
6. Process asynchronously.
7. Tolerate duplicates and out-of-order delivery.
8. Retrieve the current API resource before applying a consequential decision.

Do not deduplicate on `refId` because one subject can produce multiple legitimate events.

## Event payload types

```typescript
type KycWebhookPayload = {
  refId: string;
  status:
    | 'in_progress'
    | 'pending'
    | 'successful'
    | 'suspected'
    | string;
  userId?: string;
  [field: string]: unknown;
};

type KybWebhookPayload = {
  refId: string;
  status:
    | 'IN_PROGRESS'
    | 'IN_REVIEW'
    | 'COMPLETED'
    | 'REJECTED'
    | string;
  previousStatus?: string;
  reason?: string;
  [field: string]: unknown;
};
```

See [Statuses](/docs/api-fundamentals/statuses.md) and [KYB webhooks](/docs/business-verification/kyb-webhooks.md) for exact event mappings.

## Retries and ordering

Webhook delivery can be retried and replayed. Your handler must be safe when:

* The same delivery arrives more than once.
* A newer resource state arrives before an older event.
* Your worker fails after the endpoint has acknowledged the request.
* The endpoint times out after persisting the event.

Do not depend on a fixed retry schedule. Use the delivery log for diagnosis and retrieve the current API resource during reconciliation.

## Test the endpoint

Test all of these cases in Sandbox:

| Test                         | Expected result                      |
| ---------------------------- | ------------------------------------ |
| Valid event                  | `200` or `204` and one queued action |
| Altered body                 | `400`                                |
| Missing or invalid signature | `400`                                |
| Same `svix-id` twice         | One business action                  |
| Worker failure               | Event remains available for retry    |
| Out-of-order status          | Current API resource wins            |

For local testing, expose the server through an HTTPS tunnel, register the temporary URL in Sandbox, and use the dashboard's send or replay function. Never use a Production signing secret in local tooling.

## Safe logging

Log event type, `svix-id`, `refId`, processing result, latency, and your correlation ID. Do not log the signing secret, signature, session token, raw payload, documents, biometrics, or unredacted personal or match data.

## Troubleshooting

| Problem                        | Likely cause                                   | Resolution                                                 |
| ------------------------------ | ---------------------------------------------- | ---------------------------------------------------------- |
| Every signature fails          | Body parsed before verification                | Configure a raw-body route before JSON middleware          |
| Some signatures fail           | Wrong environment secret or altered proxy body | Check endpoint configuration and proxy behavior            |
| Repeated deliveries            | Non-`2xx` response or timeout                  | Acknowledge after durable enqueueing                       |
| Duplicate business action      | No durable `svix-id` uniqueness                | Add a unique constraint and transactional enqueue          |
| Status appears to go backwards | Out-of-order delivery                          | Retrieve the current resource and compare timestamps/state |
