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

# Settlement Webhooks

> Get pushed a notification when a swap reaches a terminal state, on top of polling /status.

Register a URL and RAVN posts to it whenever a swap made with your key settles, gets refunded,
or fails. This is a convenience notification layer on top of [`/status`](/api-reference/status),
not a replacement for it, see [What this isn't](#what-this-isn't) before you build on it.

## Register a URL

Set `webhookUrl` on [`PATCH /v1/keys`](/api-reference/keys). It must be `https://`.

```bash theme={null}
curl -s -X PATCH https://app.ravn.exchange/api/v1/keys \
  -H 'x-api-key: rvn_live_your_key_here' \
  -H 'content-type: application/json' \
  -d '{ "webhookUrl": "https://your-service.example/ravn-webhook" }'
```

```json theme={null}
{ "data": { "webhookUrl": "https://your-service.example/ravn-webhook", "webhookSecret": "a1b2c3..." } }
```

<Warning>
  **Save `webhookSecret` immediately.** It's only ever returned from this exact call, there's no
  separate endpoint to retrieve it later. Losing it means you can no longer verify deliveries and
  have to register a new URL to get a fresh one, see below.
</Warning>

Re-registering a different `webhookUrl` on the same key keeps the same `webhookSecret`; only the
first registration generates one. Enterprise keys don't support webhooks yet and return `404
NOT_FOUND` on this field.

## What gets sent

When a swap executed with your `x-api-key` reaches a terminal state, RAVN `POST`s this to your
URL:

```json theme={null}
{
  "event": "swap.settled",
  "requestId": "...",
  "partnerId": "...",
  "venue": "mayan",
  "deliveredOut": "...",
  "slippageBps": 10,
  "ts": 1234567890
}
```

`event` is one of `swap.settled`, `swap.refunded`, or `swap.failed`. `ts` is the delivery
timestamp, epoch milliseconds. `deliveredOut` and `slippageBps` can be `null` on a venue that
doesn't report them.

## Verify the signature

Every request carries an `x-ravn-signature` header: `sha256=<hex>`, an HMAC-SHA256 of the exact
raw JSON body using your `webhookSecret`. Verify it against the raw bytes, before you parse the
body, not against a re-serialized copy of it.

<CodeGroup>
  ```js Node theme={null}
  import { createHmac, timingSafeEqual } from "node:crypto";

  function isValidSignature(rawBody, signatureHeader, secret) {
    const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
    const a = Buffer.from(expected);
    const b = Buffer.from(signatureHeader ?? "");
    return a.length === b.length && timingSafeEqual(a, b);
  }
  ```

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

  def is_valid_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
      expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
      return hmac.compare_digest(expected, signature_header or "")
  ```
</CodeGroup>

## What this isn't

<Warning>
  **This is not a reliable, real-time push system, treat it as a convenience on top of polling.**

  * Deliveries fire from a background settlement job that runs roughly every 5 minutes, so a
    webhook can lag up to that long behind what [`/status`](/api-reference/status) would already
    show you.
  * Delivery is best-effort and fired once. If your endpoint is down, times out, or the request
    otherwise fails, there is no retry and no way to recover that specific notification, it's
    gone.

  [`/status`](/api-reference/status) polling remains the only way to get a guaranteed final
  result. Use webhooks to avoid polling every swap constantly, not as your source of truth for
  whether one actually completed.
</Warning>
