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

# Webhooks

> Get notified when bookings complete or fail: register a callback, verify the signature, handle the event.

Webhooks push booking lifecycle events to your server so you don't have to poll. Register an HTTPS endpoint and Jinko sends a signed `POST` the moment a booking is confirmed or fails.

The one-line version:

```
register endpoint → Jinko POSTs a signed event → you verify X-Jinko-Signature → you act
```

## Prerequisites

* A Jinko account and an API key (`jnk_...`). [Get one](https://dashboard.gojinko.com/developers/keys).
* An HTTPS endpoint that can receive a `POST` (must be `https://` and publicly reachable).

## 1) Register an endpoint

<Tabs>
  <Tab title="Dashboard">
    Go to [Dashboard → Webhooks](https://dashboard.gojinko.com/developers/webhooks), click **Add webhook**, paste your URL, pick the events, and **Create**. Copy the signing secret shown once. You'll need it to verify deliveries.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    curl -X POST https://api.gojinko.com/v1/webhooks \
      -H "X-API-Key: $JINKO_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "url": "https://your-app.com/webhooks/jinko",
        "events": ["booking.completed", "booking.failed"]
      }'
    ```

    The response contains the signing `secret` **once**, so store it securely:

    ```json theme={null}
    {
      "id": 42,
      "url": "https://your-app.com/webhooks/jinko",
      "events": ["booking.completed", "booking.failed"],
      "secret": "whsec_3f9a…",
      "status": "active",
      "created_at": "2026-06-02T08:00:00Z"
    }
    ```
  </Tab>
</Tabs>

## 2) Events

| Event               | When it fires                                             |
| ------------------- | --------------------------------------------------------- |
| `booking.completed` | The booking is fulfilled **and** paid (payment captured). |
| `booking.failed`    | The booking could not be fulfilled.                       |

More event types will be added over time. Treat the `event` field as an open enum and ignore events you don't handle.

## 3) Payload

Deliveries are intentionally **thin**: identifiers only, no traveler PII. Fetch full detail with [`get_booking`](/api/get-booking) using the `booking_ref`.

```json theme={null}
{
  "event": "booking.completed",
  "booking_ref": "JNK-A7B3X9",
  "status": "confirmed",
  "occurred_at": "2026-06-02T08:00:00Z",
  "event_id": "evt_01HX…",
  "livemode": true
}
```

Each request also carries these headers:

| Header                | Purpose                                                       |
| --------------------- | ------------------------------------------------------------- |
| `X-Jinko-Signature`   | `sha256=<hex>` HMAC of the request (see below).               |
| `X-Jinko-Timestamp`   | Unix seconds; part of the signed payload (replay protection). |
| `X-Jinko-Event-Id`    | Stable id for the business event; use it to dedupe.           |
| `X-Jinko-Delivery-Id` | Id of this specific delivery attempt.                         |

## 4) Verify the signature

Compute `HMAC-SHA256(secret, "<X-Jinko-Timestamp>.<raw request body>")` and compare it, in constant time, to the hex in `X-Jinko-Signature` (after the `sha256=` prefix). **Use the raw request body**: parsing and re-serializing the JSON will change the bytes and break the check.

<Tabs>
  <Tab title="Node.js">
    ```javascript theme={null}
    import crypto from 'node:crypto';

    // rawBody: the exact bytes Jinko sent (Buffer/string), NOT re-serialized JSON.
    function verifyJinkoWebhook(rawBody, headers, secret) {
      const ts = headers['x-jinko-timestamp'];
      const sig = headers['x-jinko-signature']; // "sha256=<hex>"
      const expected =
        'sha256=' +
        crypto.createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex');
      return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
    }
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import hashlib
    import hmac

    def verify_jinko_webhook(raw_body: bytes, headers: dict, secret: str) -> bool:
        ts = headers["X-Jinko-Timestamp"]
        sig = headers["X-Jinko-Signature"]  # "sha256=<hex>"
        signed = f"{ts}.".encode() + raw_body
        expected = "sha256=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
        return hmac.compare_digest(expected, sig)
    ```
  </Tab>
</Tabs>

Reject the request if the signature doesn't match, or if `X-Jinko-Timestamp` is older than your tolerance (e.g. 5 minutes) to guard against replays. Respond `2xx` once you've accepted the event.

## 5) Retries & idempotency

* A non-`2xx` response (or a timeout) is **retried** with exponential backoff, up to 8 attempts over several hours.
* Retries mean you may receive the **same event more than once**. Deduplicate on `X-Jinko-Event-Id` (a given business event always carries the same id).
* Return `2xx` as soon as you've durably recorded the event; do slow work asynchronously so you don't trip the delivery timeout.

## 6) Test it

Use **Send test** in the dashboard, or:

```bash theme={null}
curl -X POST https://api.gojinko.com/v1/webhooks/42/test \
  -H "X-API-Key: $JINKO_API_KEY"
```

This delivers a sample event with `"livemode": false` and `booking_ref: "JNK-TEST00"`, so you can confirm your signature handling end-to-end without a real booking.
