Webhooks
Receiving webhooks
A webhook is an HTTPS POST from Orrerie to a URL on your server, telling it that something happened. It is how your shop learns that an order was paid.
The request you receive
HTTP request
POST /webhooks/orrerie HTTP/1.1
Host: acme.example
Content-Type: application/json; charset=utf-8
User-Agent: Orrerie-Webhooks/1.0 (+https://merchant.orrerie.com/docs/webhooks)
Orrerie-Signature: t=1791727795,v1=a6e57ea3d029c6fd580984a9926230d06425882512aff3d5dc7abf650b481107
Orrerie-Event-Id: evt_…
Orrerie-Event-Type: order.paid
Orrerie-Delivery-Id: whd_…
Orrerie-Delivery-Attempt: 1The body is one event object, as compact UTF-8 JSON with no trailing newline, at most 256 KB.
| Field | Meaning |
|---|---|
id | The event's id. The same on every retry and resend. Use it to drop duplicates. |
type | The event type (the event list). |
api_version | The version data.object is rendered in: the endpoint's. |
created | When the thing happened. |
livemode | Live or test. |
data. | The whole object the event is about, as it was at that moment. |
data. | For a change: the old values of exactly the fields that changed. Absent on .created events. |
request | The API request that caused it (id, idempotency_key), or nulls when a payer, the chain, a timer or the dashboard caused it. |
pending_webhooks | How many deliveries of this event are not yet done. |
Events carry the whole object, so most handlers need no second call. Every event has a page with its complete example payload.
What your endpoint must do
The first rule
Verify the signature, then fetch the order by id with your secret key before you ship. Anyone can post to your endpoint; only the signature says a delivery is from Orrerie, and only the order you read yourself says it is paid.
- Read the raw body bytes before any JSON parsing.
- Check the signature and the timestamp. On failure answer
400and do nothing else. - Check whether you have already handled this
id. If so answer200and stop. - Record the event, answer any
2xxwithin 10 seconds, and do slow work (emails, stock, shipping) afterwards. The response body is ignored; its first 512 bytes are kept in the delivery log to help you debug. - Treat the event as a prompt, and where money or goods depend on it, act on the state:
data.object.statusispaidoroverpaid. A signature proves the event is from Orrerie; it does not protect you from a stolen signing secret. Before you ship, read the order again withGET /v1/orders/{id}and your secret key, and ship only if itsstatusispaidoroverpaid.
A redirect is not followed and counts as a failure. 410 Gone disables the endpoint at once. Your endpoint must not need cookies, a login or a CSRF token.
What an endpoint is sent
An endpoint is sent no more than whoever pointed it at its address may read. An endpoint added, or given a new URL, by an Owner, an Admin or a secret key is sent every event whole. One added by a restricted key is sent each event as List events answers that key: an event whose object the key's scopes do not read has its object cut to its id and object, with redacted naming the scope that reads it whole. One added by a Developer is sent orders and checkouts without the customer's name, email and address, as the dashboard shows them to a Developer. Read what you need with your own secret key: the event is a prompt, as above.
Working on your own computer
An endpoint, test or live, needs a public https address on port 443. Orrerie refuses localhost, private and loopback addresses, plain http and every other port, with url_invalid, and checks the address again before each delivery. To receive deliveries while you build, put your local server behind an HTTPS tunnel of your choice and add the tunnel's address as a test endpoint.
Without an endpoint, List events with created[gte] and Retrieve an event give you every event of the last 30 days, and Developers, then Events, in the dashboard shows them. With one, Resend an event sends a stored event to it again. Check your signature code against the test vector and the checker below, which works with no endpoint and sends nothing anywhere.
The signature
| Header | Orrerie-Signature: t=<unix seconds>,v1=<hex>[,v1=<hex>] |
|---|---|
| Algorithm | HMAC with SHA-256 |
| Key | The endpoint's signing secret, the whole string including whsec_, as UTF-8 bytes |
| Signed message | The decimal timestamp, a full stop, then the raw request body: <t>.<body> |
| Output | 64 lower-case hex characters |
Several v1 | While a secret is being rolled, one signature per valid secret. Accept if any v1 matches. |
| Other schemes | Ignore any element that is not t or v1. A later scheme would be v2 and would be announced as a new API version. |
| Timestamp tolerance | 300 seconds either way |
| Comparison | Constant-time |
Test vector. Use it to check your code before you have an endpoint. Checking a signature has the function in JavaScript, Python, PHP and Ruby.
Test vector
secret whsec_EXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAk3Qw
timestamp 1791727795
body {"id":"evt_9s2e3tbehv3qd6zp7jjg1gbx","object":"event","type":"ping","api_version":"2026-10-11","created":"2026-10-11T14:09:55.000Z","livemode":false,"data":{"object":{"object":"ping","endpoint":"we_tx7j2vmn3r2px2vpwbtz5h7b"}},"request":{"id":null,"idempotency_key":null},"pending_webhooks":1}
header Orrerie-Signature: t=1791727795,v1=a6e57ea3d029c6fd580984a9926230d06425882512aff3d5dc7abf650b481107Check a signature in this page
Paste a payload, the signing secret and the Orrerie-Signature header. The check runs in your browser and sends nothing anywhere.
The checker needs JavaScript
The checker is added to this page by its script and works only with JavaScript on. Nothing typed into it leaves your browser. With JavaScript off, run one of the four functions on the test vector above.
Replay protection
Three things together, and you need all three:
- The timestamp is inside the signature, so nobody can alter it. Reject a delivery whose
tis more than 300 seconds from your clock. Keep your clock synchronised. - Each event has one id for ever. Store the ids you have handled and ignore a repeat. This also covers Orrerie's own retries and resends, which are legitimate duplicates. Keep ids for at least 30 days, the time an event can be resent.
- Each attempt is signed afresh. A retry or a resend carries a new
tand a new signature over the same body, so a genuine late retry is never rejected as stale.
Rolling a signing secret
POST / returns a new secret once (in live mode, roll it in the dashboard, with a passkey). For expires_in seconds (24 hours by default, and at most 24 hours) every delivery carries two v1 values, one per secret, so you can deploy the new secret at any moment in that window without losing an event. Accept a delivery if any v1 matches.
Retries
A delivery fails when the connection cannot be made, TLS fails (TLS 1.2 or later with a valid certificate is required), the answer is not 2xx, or no answer arrives within 10 seconds (5 seconds to connect).
| Attempt | Sent after the previous failure | Time since the event |
|---|---|---|
| 1 | At once | 0 |
| 2 | 1 min | 1 min |
| 3 | 5 min | 6 min |
| 4 | 15 min | 21 min |
| 5 | 1 h | 1 h 21 min |
| 6 | 2 h | 3 h 21 min |
| 7 | 4 h | 7 h 21 min |
| 8 | 8 h | 15 h 21 min |
| 9 | 12 h | 27 h 21 min |
| 10 | 12 h | 39 h 21 min |
| 11 | 16 h | 55 h 21 min |
| 12 | 16 h | 71 h 21 min |
Twelve attempts over three days, each delay varied by up to 10% either way. After the twelfth failure the delivery is failed and is not tried again by itself; the event stays readable and resendable for 30 days. In test mode there are three attempts: at once, after 1 minute, after 5 minutes.
- Order is not guaranteed. A retry of an old event can arrive after a newer event about the same object. Compare
data.object.updatedwith what you hold, or read the object again. Up to 4 deliveries run at once per endpoint. - At least once. The same event can arrive twice. Use the event id.
- Failing. After 24 hours of consecutive failures the endpoint reads
failing: truewithfailing_since, the dashboard labels it "Failing" and the Owners, Admins and Developers are emailed. This is a label, not a state: deliveries go on. - Switching off. If every attempt to an endpoint has failed for 7 days running, and there were at least 10 attempts, the endpoint becomes
disabledwithdisabled_, the Owners and Developers are emailed, andreason: "consecutive_ failures" webhook_endpoint.disabledgoes to your other endpoints. Enable it again withPATCH; fetch events from the gap withGET /v1/eventsand resend them withPOST /v1/events/{id}/resend. - Catching up without webhooks.
GET /returns everything, 30 days back, whether or not it was ever delivered. A robust integration runs this once an hour as a safety net. An event whose object your key may not read on its own (av1/ events?created[gte]=<the last time you were sure of> customer.*event for a key withoutcustomers:read, for example) comes with its object cut toidandobjectand withredactednaming the scope that reads it whole: read the object with a key that has it. - Where deliveries come from. A fixed list of sending IP addresses is not promised. The signature is the proof of origin; do not rely on an IP list alone.
- What Orrerie will not send to. A URL that resolves to a private, loopback or link-local address is refused at creation and again at every delivery.
A handler that is correct
Outline
on POST:
raw = read body bytes
if not verify(raw, header, secret): return 400
event = parse(raw)
begin transaction
insert event.id into handled_events -- unique key; a duplicate aborts the transaction
if event.type == "order.paid":
order = your order where reference = event.data.object.reference
if order is not yet fulfilled and event.data.object.status in ("paid", "overpaid"):
queue fulfilment of order, with event.data.object.id
commit
return 200
fulfilment, for each queued order:
current = GET /v1/orders/{id} with your secret key
if current.status in ("paid", "overpaid"):
mark order paid; shipThe answer goes back within 10 seconds, and nothing ships until the order read with your own key says it is paid (rule 5 above).
Read next: How an order becomes Paid. The path from one API call to the webhook, and who has the money at each moment.
The same in brief, without code: Crypto payments API and webhooks.