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

# Webhooks

> Event catalogue, signature verification, retries, and testing

Webhooks let your backend react asynchronously instead of polling. You register
an HTTPS endpoint; Safariat POSTs a signed JSON event to it.

## Event catalogue

| Event type              | Emitted when                                                         |
| ----------------------- | -------------------------------------------------------------------- |
| `booking.confirmed`     | A `POST /bookings` succeeded.                                        |
| `booking.cancelled`     | A booking was cancelled (by you or by Safariat).                     |
| `booking.completed`     | The travel date passed and the booking is fulfilled.                 |
| `settlement.issued`     | A monthly [settlement](/api-concepts/settlements) moved to `ISSUED`. |
| `settlement.paid`       | A settlement was marked `PAID` (funds reconciled).                   |
| `adventure.unavailable` | A previously bookable adventure/slot is no longer sellable.          |

## Event envelope

```json theme={"theme":"github-dark-dimmed"}
{
  "id": "8c3f9a40-3b5d-4d52-9a2e-cc1f4b7d8e2a",
  "type": "booking.confirmed",
  "created_at": "2026-07-12T10:05:00Z",
  "test": false,
  "data": { "booking_number": "MV-AB7X92", "partner_reference": "YBANK-BOOK-001" }
}
```

* `id` — idempotent event ID. **Deduplicate on it** (the same event may be delivered more than once).
* `test` — `true` when the event comes from an `sk_test_*` key or from
  `POST /webhooks/{id}/test`. Your production handler must ignore or clearly label these.

## Delivery headers

| Header                   | Purpose                                                                                                                 |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
| `X-SafarApi-Signature`   | HMAC-SHA256 of the **raw body** with the endpoint's `signing_secret`.                                                   |
| `X-SafarApi-Event-Id`    | Same UUID as `id` — use for deduplication.                                                                              |
| `X-SafarApi-Delivery-Id` | Unique per **delivery attempt**. The same `Event-Id` can arrive under several `Delivery-Id`s (retries / re-deliveries). |
| `X-SafarApi-Event-Type`  | The event type, for routing before parsing.                                                                             |

The `signing_secret` is returned **once**, in the `POST /webhooks` response
(`WebhookEndpointWithSecret`). It is never displayable again — store it securely.

## Verifying the signature

Compare in constant time. Reject on mismatch.

<CodeGroup>
  ```javascript Node.js theme={"theme":"github-dark-dimmed"}
  import crypto from 'node:crypto';

  function verify(rawBody, header, signingSecret) {
    const expected = crypto.createHmac('sha256', signingSecret)
      .update(rawBody).digest('hex');
    return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header));
  }
  ```

  ```python Python theme={"theme":"github-dark-dimmed"}
  import hmac, hashlib

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

<Warning>
  Verify against the **exact bytes received**, before any JSON re-serialization.
  Reformatting the body breaks the signature.
</Warning>

## Retries

A delivery is successful on a `2xx` response within the timeout. Otherwise
Safariat retries up to **5 attempts** with backoff: **5 min → 15 min → 1 h →
6 h → 24 h**. After the last attempt the delivery is dead-lettered. Because
retries happen, your handler must be **idempotent on `X-SafarApi-Event-Id`**.

Respond `2xx` immediately and process asynchronously — slow handlers cause
timeouts and unnecessary retries.

## Delivery semantics

SafarAPI guarantees **at-least-once** delivery, not exactly-once. Two rules follow:

* **Deduplicate.** The same event (`X-SafarApi-Event-Id`) may be delivered more
  than once — on retry, and in a narrow window if a delivery is received but our
  status write is interrupted. Treat the first successful processing as
  authoritative and make re-processing a no-op (upsert / dedup table keyed by
  `Event-Id`).
* **Do not rely on ordering.** Events are dispatched concurrently and retried
  with backoff, so they can arrive **out of order** (e.g. `booking.cancelled`
  before its `booking.confirmed`, or a retried event after a newer one). Drive
  your state from the event's `created_at` and your own state machine — never
  assume the arrival order reflects the real sequence.

<Info>
  Missed or dead-lettered a delivery? List and inspect attempts with
  `GET /webhooks/deliveries`, and re-drive one with
  `POST /webhooks/deliveries/{id}/replay`. You can also reconcile from the
  authoritative REST resources (`GET /bookings/{n}`, `GET /settlements`) at any time —
  webhooks are an optimization over polling, not the source of truth.
</Info>

## Testing

`POST /webhooks/{id}/test` sends a synthetic event to your endpoint and returns
the HTTP status and response time it observed — use it to validate connectivity
and your signature check before going live.
