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 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:
url: a publichttpsURL. Ownrecord never calls localhost, private networks (10/8, 172.16/12, 192.168/16,fc00::/7,.localor.internalnames) 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://localhostworks, 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 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
{
"id": "evt_01m4tx5nj8emwbyf7y9vcf47z4",
"type": "customer.created",
"created": 1791979200,
"livemode": false,
"data": { "object": { "id": "cus_01m4tx5nj8emwbyf7y9vcf47z6", "object": "customer" } }
}
idis the event's ID. Retries and redeliveries send the same event again, so deduplicate onid.createdis when the event happened, in Unix seconds. Deliveries aren't guaranteed to arrive in order: usecreated, and fetch the object again when you need its current state.data.objectis the object as the API returns it, with amounts as integer micros in strings. Updates also carryprevious_attributeswith 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:
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:
- Take the raw body, exactly as received, before parsing the JSON.
- Compute the HMAC of
t.bodywith your secret and compare it with eachv1in constant time. Ignore schemes you don't know (v0, …). - Refuse the request if
tis more than 5 minutes from your clock: this stops replays.
The TypeScript SDK does all of this:
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 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.testsends awebhook_endpoint.testevent to one endpoint, signed like any delivery. It's attempted once; read the result withwebhook_deliveries.get. Test events aren't in the event feed, and endpoints can't subscribe to them.webhook_deliveries.redeliversends a finished delivery's event again, once, as a new delivery. Use it after fixing a receiver.webhook_deliveries.listis 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 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,webhook_endpoints.list,webhook_endpoints.get,webhook_endpoints.update,webhook_endpoints.delete,webhook_endpoints.rotate_secret,webhook_endpoints.test,webhook_deliveries.list,webhook_deliveries.get,webhook_deliveries.redeliver. - Events:
webhook_endpoint.created,webhook_endpoint.updated,webhook_endpoint.deleted(internal),webhook_endpoint.test(events). - Errors:
webhook_url_not_allowed,webhook_event_type_invalid,webhook_endpoint_limit_reached,webhook_delivery_in_progress(errors).