Ask for an invitation
Menu

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: 1

The body is one event object, as compact UTF-8 JSON with no trailing newline, at most 256 KB.

Table: The request you receive
FieldMeaning
idThe event's id. The same on every retry and resend. Use it to drop duplicates.
typeThe event type (the event list).
api_versionThe version data.object is rendered in: the endpoint's.
createdWhen the thing happened.
livemodeLive or test.
data.objectThe whole object the event is about, as it was at that moment.
data.previous_attributesFor a change: the old values of exactly the fields that changed. Absent on .created events.
requestThe API request that caused it (id, idempotency_key), or nulls when a payer, the chain, a timer or the dashboard caused it.
pending_webhooksHow 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.

  1. Read the raw body bytes before any JSON parsing.
  2. Check the signature and the timestamp. On failure answer 400 and do nothing else.
  3. Check whether you have already handled this id. If so answer 200 and stop.
  4. Record the event, answer any 2xx within 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.
  5. Treat the event as a prompt, and where money or goods depend on it, act on the state: data.object.status is paid or overpaid. 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 with GET /v1/orders/{id} and your secret key, and ship only if its status is paid or overpaid.

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

Table: The signature
HeaderOrrerie-Signature: t=<unix seconds>,v1=<hex>[,v1=<hex>]
AlgorithmHMAC with SHA-256
KeyThe endpoint's signing secret, the whole string including whsec_, as UTF-8 bytes
Signed messageThe decimal timestamp, a full stop, then the raw request body: <t>.<body>
Output64 lower-case hex characters
Several v1While a secret is being rolled, one signature per valid secret. Accept if any v1 matches.
Other schemesIgnore any element that is not t or v1. A later scheme would be v2 and would be announced as a new API version.
Timestamp tolerance300 seconds either way
ComparisonConstant-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=a6e57ea3d029c6fd580984a9926230d06425882512aff3d5dc7abf650b481107

Check 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:

  1. The timestamp is inside the signature, so nobody can alter it. Reject a delivery whose t is more than 300 seconds from your clock. Keep your clock synchronised.
  2. 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.
  3. Each attempt is signed afresh. A retry or a resend carries a new t and a new signature over the same body, so a genuine late retry is never rejected as stale.

Rolling a signing secret

POST /v1/webhook_endpoints/{id}/roll_secret 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).

Table: Retries
AttemptSent after the previous failureTime since the event
1At once0
21 min1 min
35 min6 min
415 min21 min
51 h1 h 21 min
62 h3 h 21 min
74 h7 h 21 min
88 h15 h 21 min
912 h27 h 21 min
1012 h39 h 21 min
1116 h55 h 21 min
1216 h71 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.updated with 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: true with failing_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 disabled with disabled_reason: "consecutive_failures", the Owners and Developers are emailed, and webhook_endpoint.disabled goes to your other endpoints. Enable it again with PATCH; fetch events from the gap with GET /v1/events and resend them with POST /v1/events/{id}/resend.
  • Catching up without webhooks. GET /v1/events?created[gte]=<the last time you were sure of> 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 (a customer.* event for a key without customers:read, for example) comes with its object cut to id and object and with redacted naming 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; ship

The 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.