# Webhooks

> Ownrecord POSTs your brand's events to the endpoints you choose, signed with Ownrecord-Signature, and retries failed deliveries for 3 days.

A **webhook endpoint** is a URL of yours that receives a brand's events as they happen: a customer was created, a checkout completed, an invoice was paid. Each delivery is an HTTP `POST` with a JSON body, signed so you can check it came from Ownrecord.

Endpoints belong to one brand and one mode. Test mode and live mode have separate endpoints, and an endpoint only ever receives events of its own brand and mode.

Can't receive webhooks, or need the legal entity's compliance events too? Read the [event feed](https://ownrecord.io/docs/concepts/events.md#the-pull-feed) instead: the same events with a cursor, and long-polling.

## Creating an endpoint

In the console, open the brand and choose **Developers: Webhooks**, or call [`webhook_endpoints.create`](https://ownrecord.io/docs/reference/webhook-endpoints-create.md):

- `url`: a public `https` URL. Ownrecord never calls localhost, private networks (10/8, 172.16/12, 192.168/16, `fc00::/7`, `.local` or `.internal` names) or link-local addresses, and it checks every address the host name resolves to when it delivers. In development and test (dev gate open), `http://localhost` works, for example the dev webhook sink.
- `events`: the event types to receive, or `["*"]` for every event of the brand, including types added later. Endpoints receive the brand's events that the [event catalogue](https://ownrecord.io/docs/reference/events.md) delivers to webhooks.

The response contains the endpoint's **signing secret** (`whsec_…`) in `secret`. It's shown only once: store it in your secret manager. A brand can have up to 16 endpoints per mode.

## What a delivery looks like

```json
{
  "id": "evt_01m4tx5nj8emwbyf7y9vcf47z4",
  "type": "customer.created",
  "created": 1791979200,
  "livemode": false,
  "data": { "object": { "id": "cus_01m4tx5nj8emwbyf7y9vcf47z6", "object": "customer" } }
}
```

- `id` is the event's ID. Retries and redeliveries send the same event again, so **deduplicate on `id`**.
- `created` is when the event happened, in Unix seconds. Deliveries aren't guaranteed to arrive in order: use `created`, and fetch the object again when you need its current state.
- `data.object` is the object as the API returns it, with amounts as integer micros in strings. Updates also carry `previous_attributes` with the old values of the fields that changed.

The request also has `Content-Type: application/json` and `Ownrecord-Delivery-Id` (the delivery's `whd_…` ID, useful when you contact support).

## Verifying the signature

Every delivery carries the header:

```text
Ownrecord-Signature: t=1791979205,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
```

`t` is when the request was signed (Unix seconds) and `v1` is the hex HMAC-SHA256 of `t + "." + body`, keyed with your whole signing secret. To verify a request:

1. Take the **raw body**, exactly as received, before parsing the JSON.
2. Compute the HMAC of `t.body` with your secret and compare it with each `v1` in constant time. Ignore schemes you don't know (`v0`, …).
3. Refuse the request if `t` is more than 5 minutes from your clock: this stops replays.

The TypeScript SDK does all of this:

```ts
import { webhooks } from "@ownrecord/sdk";

const event = await webhooks.verify(rawBody, request.headers.get("Ownrecord-Signature"), secret);
// Throws WebhookSignatureError (code: missing_header, malformed_header, timestamp_outside_tolerance,
// no_matching_signature or invalid_payload): answer 400 and ignore the request.
```

Answer with any `2xx` status within 10 seconds, ideally before doing slow work. Redirects aren't followed and count as failures.

## Retries and automatic disabling

A delivery that doesn't get a `2xx` within 10 seconds is retried after 1 minute, 5 minutes, 15 minutes, 30 minutes, 1 hour, 2, 4, 6 and 8 hours, then every 10 hours: 15 attempts over about 3 days. The delivery log shows each attempt with its status code and the start of your response.

If an endpoint's deliveries keep failing for 3 days, Ownrecord **disables** it and raises the `WEBHOOK_FAILING` alert. Fix your receiver, enable the endpoint again (console, or [`webhook_endpoints.update`](https://ownrecord.io/docs/reference/webhook-endpoints-update.md) with `enabled: true`) and redeliver what it missed. The alert closes when a delivery succeeds after that. While an endpoint is disabled, nothing is sent to it and waiting retries are canceled.

## Testing and redelivering

- [`webhook_endpoints.test`](https://ownrecord.io/docs/reference/webhook-endpoints-test.md) sends a `webhook_endpoint.test` event to one endpoint, signed like any delivery. It's attempted once; read the result with [`webhook_deliveries.get`](https://ownrecord.io/docs/reference/webhook-deliveries-get.md). Test events aren't in the event feed, and endpoints can't subscribe to them.
- [`webhook_deliveries.redeliver`](https://ownrecord.io/docs/reference/webhook-deliveries-redeliver.md) sends a finished delivery's event again, once, as a new delivery. Use it after fixing a receiver.
- [`webhook_deliveries.list`](https://ownrecord.io/docs/reference/webhook-deliveries-list.md) is the delivery log, filterable by endpoint, event and status.

In development, point an endpoint at `http://localhost:3000/api/dev/webhook-sink/<channel>`: the dev webhook sink keeps what it receives, and `GET` on the same URL lists the requests with their headers and raw body.

## Rotating the signing secret

[`webhook_endpoints.rotate_secret`](https://ownrecord.io/docs/reference/webhook-endpoints-rotate-secret.md) returns a new secret, once. By default the old secret stops at once. With `overlapSeconds` (up to 24 hours) the old secret keeps signing next to the new one: each delivery then carries two `v1=` signatures, and your receiver accepts it when either matches. Deploy the new secret, then let the overlap run out.

## Reference

- Operations: [`webhook_endpoints.create`](https://ownrecord.io/docs/reference/webhook-endpoints-create.md), [`webhook_endpoints.list`](https://ownrecord.io/docs/reference/webhook-endpoints-list.md), [`webhook_endpoints.get`](https://ownrecord.io/docs/reference/webhook-endpoints-get.md), [`webhook_endpoints.update`](https://ownrecord.io/docs/reference/webhook-endpoints-update.md), [`webhook_endpoints.delete`](https://ownrecord.io/docs/reference/webhook-endpoints-delete.md), [`webhook_endpoints.rotate_secret`](https://ownrecord.io/docs/reference/webhook-endpoints-rotate-secret.md), [`webhook_endpoints.test`](https://ownrecord.io/docs/reference/webhook-endpoints-test.md), [`webhook_deliveries.list`](https://ownrecord.io/docs/reference/webhook-deliveries-list.md), [`webhook_deliveries.get`](https://ownrecord.io/docs/reference/webhook-deliveries-get.md), [`webhook_deliveries.redeliver`](https://ownrecord.io/docs/reference/webhook-deliveries-redeliver.md).
- Events: `webhook_endpoint.created`, `webhook_endpoint.updated`, `webhook_endpoint.deleted` (internal), `webhook_endpoint.test` ([events](https://ownrecord.io/docs/reference/events.md)).
- Errors: `webhook_url_not_allowed`, `webhook_event_type_invalid`, `webhook_endpoint_limit_reached`, `webhook_delivery_in_progress` ([errors](https://ownrecord.io/docs/reference/errors.md)).
