> 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/business-verification/kyb-webhooks.md).

# KYB webhooks

Receive KYB status updates and reconcile the current case.

KYB events use the same Svix headers, raw-body verification, delivery ID, and retry behavior described in [Webhooks](/docs/kyc-api/webhooks.md).

## Events

| Event                  | Payload status | Meaning                                       |
| ---------------------- | -------------- | --------------------------------------------- |
| `kyb.case.in_progress` | `IN_PROGRESS`  | The applicant started the case                |
| `kyb.case.in_review`   | `IN_REVIEW`    | The applicant submitted the case for review   |
| `kyb.case.completed`   | `COMPLETED`    | Review completed under the configured process |
| `kyb.case.rejected`    | `REJECTED`     | Review rejected the case                      |

There is no public `kyb.case.created` event. Persist the `POST /kyb/case` response as the creation record. `NOT_STARTED` does not emit a status event.

## Representative payloads

### In review

```json
{
  "refId": "business-7f3d",
  "status": "IN_REVIEW",
  "previousStatus": "IN_PROGRESS"
}
```

### Completed

```json
{
  "refId": "business-7f3d",
  "status": "COMPLETED",
  "previousStatus": "IN_REVIEW"
}
```

### Rejected

```json
{
  "refId": "business-7f3d",
  "status": "REJECTED",
  "previousStatus": "IN_REVIEW",
  "reason": "DOCUMENT_MISMATCH"
}
```

Fields other than `refId` and `status` depend on the transition. Tolerate additive fields.

## Payload type

```typescript
type KybWebhookStatus =
  | 'IN_PROGRESS'
  | 'IN_REVIEW'
  | 'COMPLETED'
  | 'REJECTED';

type KybWebhookPayload = {
  refId: string;
  status: KybWebhookStatus | string;
  previousStatus?: string;
  reason?: string;
  [field: string]: unknown;
};
```

## Node.js handler

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

const app = express();

app.post('/webhooks/vove/kyb', express.raw({ type: 'application/json' }), async (req, res) => {
  let event;

  try {
    event = new Webhook(process.env.VOVE_WEBHOOK_SECRET).verify(req.body, {
      'svix-id': req.header('svix-id'),
      'svix-timestamp': req.header('svix-timestamp'),
      'svix-signature': req.header('svix-signature'),
    });
  } catch {
    return res.sendStatus(400);
  }

  await storeOnceAndEnqueue(req.header('svix-id'), event);
  return res.sendStatus(204);
});
```

Register the route before global JSON middleware. Deduplicate on `svix-id`, not `refId`.

## Processing example

```javascript
async function processKybEvent(event) {
  const { refId, status } = event;

  switch (status) {
    case 'IN_PROGRESS':
    case 'IN_REVIEW':
      await updateLocalCase(refId, { status });
      return;
    case 'COMPLETED':
    case 'REJECTED': {
      const currentCase = await retrieveKybCase(refId);
      await applyReviewedBusinessDecision(currentCase);
      return;
    }
    default:
      await flagUnknownKybStatus(refId, status);
  }
}
```

For `COMPLETED` and `REJECTED`, retrieve `GET /kyb/case/{refId}/details` before applying your business decision.

## Idempotency and ordering

* Store every `svix-id` with a unique constraint.
* Expect duplicate and out-of-order deliveries.
* Do not overwrite a newer local state with an older event.
* Use the current case resource as the final authority.
* Return `2xx` after durable storage and before slow business processing.

## Testing

Use a fictional Sandbox case to verify:

1. A valid event returns `204`.
2. A changed body or invalid signature returns `400`.
3. Replaying the same delivery does not repeat a business action.
4. An out-of-order event does not move the case backwards.
5. A terminal event triggers case-detail retrieval.

## Troubleshooting

| Problem                           | Check                                                             |
| --------------------------------- | ----------------------------------------------------------------- |
| Signature verification fails      | Raw body, matching environment secret, and all three Svix headers |
| Event is delivered repeatedly     | Endpoint response status and processing time                      |
| Case cannot be correlated         | The original `refId` persisted from case creation                 |
| Payload lacks optional fields     | Branch only on documented required fields and retrieve the case   |
| Local and webhook statuses differ | Retrieve current case details and prefer the latest server state  |
