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

# Webhooks

> Receive real-time notifications for payments, payouts, and more.

Webhooks let Kori notify your server when events happen — most importantly, when an
asynchronous collection settles. Register endpoints with the `webhooks` scope
(dashboard → **Developer Portal → Webhooks**, or the API).

## Register an endpoint

```bash theme={null}
curl -X POST https://api.kori.ml/merchant/api/webhooks \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/webhooks/kori",
    "events": ["payment_received", "payout_completed"]
  }'
```

The response includes a `secret` **once** — store it securely; you'll use it to verify
signatures. You can register up to **5 endpoints** per merchant.

## Events

| Event               | Fired when                                      |
| ------------------- | ----------------------------------------------- |
| `payment_received`  | A collection (`/pay`) is confirmed and credited |
| `payout_completed`  | A payout finished successfully                  |
| `payout_failed`     | A payout could not be completed                 |
| `bulk_pay_done`     | A bulk-pay batch finished processing            |
| `payment_link_used` | A payment link received a successful payment    |
| `team_login`        | A team member signed in                         |
| `api_key_created`   | A new API key was created                       |
| `security_alert`    | A security-relevant event occurred              |

## Delivery format

Kori sends a `POST` with a JSON body and these headers:

| Header             | Description                                                               |
| ------------------ | ------------------------------------------------------------------------- |
| `X-Kori-Signature` | HMAC-SHA256 of the raw request body, keyed with your webhook secret (hex) |
| `X-Kori-Event`     | The event name                                                            |

```json Body theme={null}
{
  "event": "payment_received",
  "data": { "transaction_id": 88213, "reference": "order_1024", "amount": 5000 },
  "timestamp": "2026-08-08T12:00:00.000Z",
  "merchant_id": 42
}
```

Respond with a `2xx` status quickly. Non-2xx responses count as failures.

## Verify the signature

Always verify `X-Kori-Signature` before trusting a payload. Compute the HMAC over the
**raw** body (not a re-serialized object) using your webhook secret.

<CodeGroup>
  ```javascript Node.js theme={null}
  const crypto = require("crypto");

  function verifyKoriSignature(rawBody, signature, secret) {
    const expected = crypto
      .createHmac("sha256", secret)
      .update(rawBody)
      .digest("hex");
    return crypto.timingSafeEqual(
      Buffer.from(expected),
      Buffer.from(signature)
    );
  }

  // Express: capture the raw body
  app.post(
    "/webhooks/kori",
    express.raw({ type: "application/json" }),
    (req, res) => {
      const ok = verifyKoriSignature(
        req.body,                       // Buffer of the raw body
        req.header("X-Kori-Signature"),
        process.env.KORI_WEBHOOK_SECRET
      );
      if (!ok) return res.status(400).send("bad signature");

      const event = JSON.parse(req.body.toString());
      // handle event.event / event.data ...
      res.sendStatus(200);
    }
  );
  ```

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

  def verify_kori_signature(raw_body: bytes, signature: str, secret: str) -> bool:
      expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
      return hmac.compare_digest(expected, signature)
  ```
</CodeGroup>

<Warning>
  Verify against the **raw bytes** of the request body. Parsing to JSON and
  re-stringifying changes whitespace/key order and will break the signature.
</Warning>

## Retries and auto-disable

Failed deliveries increment a failure counter. After repeated consecutive failures, an
endpoint is automatically **disabled** (`is_active: false`). Fix your endpoint and
re-enable it via `PUT /merchant/api/webhooks/{id}`.

## Test an endpoint

Send a sample `test` event to confirm your handler works:

```bash theme={null}
curl -X POST https://api.kori.ml/merchant/api/webhooks/{id}/test \
  -H "Authorization: Bearer <token>"
```
