# Webhooks

Receive Kustom events by HTTPS POST instead of polling — the event types, the slim payload shape, Standard Webhooks signature verification, secret rotation and why your handler must be idempotent.

A webhook tells you that something happened in Kustom without you having to poll for it.
You register an HTTPS endpoint in the Kustom Portal, subscribe it to event types, and Kustom
`POST`s to it when one occurs. Payloads are deliberately slim — they identify the thing that
changed, not its full state — so a handler almost always ends by fetching the object from the
relevant API.

## What a webhook delivery contains

```json
{
    "id": "kevt_1YcaJdWrmlw9yp1GiiVgad",
    "merchant_id": "DM11917000",
    "timestamp": "2026-04-10T08:27:07.248645321Z",
    "type": "capture.created",
    "data": {
        "order_id": "25ed76ed-6477-46bb-8444-63945789ccfb",
        "capture_id": "070ddfa9-f7a8-4fb0-8291-b5b31a300a23",
        "created_at": "2026-04-10T08:27:02.648627Z"
    }
}
```

| Field | What it is |
| --- | --- |
| `id` | Unique ID for the event, stable across retry attempts. Use it for deduplication. |
| `merchant_id` | Your Kustom Merchant ID. |
| `type` | The event type, from the table below. |
| `timestamp` | ISO 8601 time the event was generated. |
| `data` | Identifiers for the affected objects. Contents vary by event type. |

Treat `data` as a pointer, not a snapshot. By the time you handle the event the object may
have moved on again, so read the current state through the API using the identifiers given.

## Event types Kustom sends

| Event type | Fires when |
| --- | --- |
| `order.created` | A new order has been created. |
| `capture.created` | A capture has been made on an order. |
| `refund.created` | A refund has been issued on an order. |
| `dispute.created` | A new dispute has been opened on an order. |
| `dispute.updated` | An existing dispute changes, for example its status. |
| `ipp.session.created` | An in-person payment session has been created. |
| `ipp.session.activated` | An in-person payment session has been activated. |
| `ipp.payment.failed` | An in-person payment attempt has failed. |
| `ipp.session.finalized` | An in-person payment session has been finalized. |
| `ipp.session.cancelled` | An in-person payment session has been cancelled. |

New event types are added over time. A handler that throws on an unrecognized `type` will
break the first time one appears — ignore what you haven't subscribed to and log it.

## Setting up an endpoint

1. **Expose an HTTPS endpoint** that accepts `POST`, returns a `2xx` promptly and does its
   real work asynchronously. Anything other than a `2xx`, or a timeout, is a failed delivery
   and will be retried.
2. **Register it in the Kustom Portal**, under Integrations → Webhooks, and choose the event
   types it should receive.
3. **Verify the signature** on every request before you act on the body.

Acknowledge first, process second. A handler that captures, refunds or writes to a slow
database *before* replying is a handler that will be sent duplicates.

## Verifying the signature

Kustom signs every payload following the [Standard
Webhooks](https://www.standardwebhooks.com/) specification, so an off-the-shelf Standard
Webhooks library will verify a Kustom delivery unmodified.

| Header | What it carries |
| --- | --- |
| `webhook-id` | Unique message ID, identical across retry attempts. Use for deduplication. |
| `webhook-timestamp` | Unix timestamp in seconds of when the message was sent. |
| `webhook-signature` | One or more space-delimited HMAC-SHA256 signatures. |

Your signing secret is generated when you configure the destination and has the form
`whsec_<base64-encoded-secret>`. Retrieve it from the Portal, store it as a secret and check
the `webhook-timestamp` against your own clock to reject replayed deliveries. Implementation
guidance is in the [Standard Webhooks
specification](https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md).

`webhook-signature` can hold more than one signature. Verify against *any* accepted secret,
not only the first — that's what makes rotation non-breaking.

## Rotating the signing secret

Rotate from the Portal at any time. During rotation Kustom signs with both the old and the
new secret, space-delimited in `webhook-signature`, so a verifier that accepts either keeps
working throughout. Rotation takes up to 10 minutes to propagate because of caching. Once
your code is verifying with the new secret, retire the old one.

## Retries and idempotent handling

If your endpoint doesn't return `2xx`, or times out, Kustom retries with exponential
backoff for **up to 3 days**.

Deliveries are therefore at-least-once, and your handler must be idempotent. Deduplicate on
`id` in the body, or equivalently on the `webhook-id` header — both are stable across retries
of the same event. Record the ID before doing the work and discard anything you've already
seen.

## Related

- [Fetching the object a webhook points at](/order-management/update-and-cancel/) — the read call a handler ends with
- [Retrying calls you make to Kustom](/order-management/idempotency-and-retries/) — the mirror image of this page
- [Reading an error body](/api/errors/) — when the follow-up read fails
- [Where events sit in the order lifecycle](/order-management/) — what `capture.created` and `refund.created` mean
