Designing resends, ordering, and idempotency
ReceiptRoller retries deliveries your server did not accept. As a result the same event can arrive more than once, events can arrive out of order, and occasionally one does not arrive at all. This article sets out the retry rules and how to build a receiver that copes with them.
Success and failure
- A
2xxresponse is a success - Anything else is a failure and is retried,
4xx(including401and404) is treated the same as5xx - No response within 10 seconds, or a DNS, TLS or connection error, is also a failure. The delivery log shows
httpStatus0for these
Retry timing
| Attempt | Sent |
|---|---|
| 1st | Right after the event |
| 2nd | 1 minute after the 1st fails |
| 3rd | 10 minutes after the 2nd fails |
| 4th | 1 hour after the 3rd fails |
- Four attempts at most; the last comes about 71 minutes after the first (plus up to 10 seconds of waiting per attempt)
- A retry has the same body, signature and
X-RR-Deliveryas the first attempt - If all four fail, the webhook is switched off (
active: false) and no further events are sent to it. Four failures of a single event are enough to stop the whole webhook - Events that happen while a webhook is switched off are not held back and do not arrive after you switch it back on. There is no way to resend a past event by hand
- Pausing a webhook (
active: false) does not stop retries that are already waiting
Retries are not stored
Pending retries are held on a ReceiptRoller server, not saved to a queue or database. If that server restarts while a retry is waiting (during a deployment, for example), the event's remaining retries do not happen and the webhook is not switched off either. The delivery log just ends with the last failed attempt.
So do not assume that retries mean every event arrives. Where a missed event matters, reconcile against the API on a schedule. For sales, the Transactions API returns them for a period you specify.
Order is not guaranteed
Each event is sent on its own and retries arrive later, so for example:
- An order's
order.updatedarrives before itsorder.created - A sale whose first attempt failed arrives after later sales
Where order matters, treat the webhook as a signal that something changed and fetch the current state from the API. The body's timestamp is when ReceiptRoller sent the event out and does not change on retries, so you can compare it to avoid overwriting an order's newer state with an older notification.
Dropping duplicates
An event mostly arrives twice when your server finished processing but did not return 2xx (it took longer than 10 seconds, or failed part-way). Every delivery has an X-RR-Delivery header that is unique per event and equal to the body's id; use it as the key.
async function handle(deliveryId, event) {
// already processed: just return 2xx
const inserted = await db.processedDeliveries.insertIfAbsent({
delivery_id: deliveryId,
received_at: new Date(),
});
if (!inserted) return;
await processEvent(event);
}
- Keep processed IDs for longer than the roughly 71 minutes retries can take; a few hours at least
- If you can record the ID and update your data in one transaction, a failure part-way cannot leave the ID recorded without the update
- If several of your webhooks subscribe to the same event, deliveries from all of them carry the same
X-RR-Delivery, so you process the event once
You can also make writes idempotent with a business key. For a sale, upsert by data.id (in the form {terminal_id}|{yyyy-MM}|{transaction_id}); for an order, by data.order_id. Then the same content arriving twice changes nothing.
A receiver that works well
- Verify the signature
- Check
X-RR-Delivery; if it was already processed, return200at once - Save the body to your own queue or table
- Return
200straight away (well within 10 seconds) - Do heavy work and calls to other services from the saved data in a separate process, retrying on your side
That way an outage in another service does not make deliveries fail, and you avoid four failures switching the webhook off.
Common mistakes
| Design | What happens |
|---|---|
| Responding only after heavy processing | It takes over 10 seconds, counts as a failure, and the event is sent again |
Returning 5xx on every temporary failure | Four failures of one event switch the webhook off, and all later events stop |
| Adding up quantities or amounts without dropping duplicates | A retry counts twice |
| Overwriting state in arrival order | An old notification overwrites a newer state |
| Relying on webhooks alone | Events from while the webhook was off, or lost in a server restart, go unnoticed |
Related guides
-
Receive new sales by webhook and post them to SlackRegister a receipt.created webhook, verify its signature and drop duplicates in Node.js, and post each new sale to a Slack channel.
-
How to register a WebhookRegister a webhook in the developer portal or through the API, choose events that are actually sent, run a test send, and change, pause or delete a webhook.
-
Webhook is not being deliveredWhen a webhook does not arrive: narrow it down with a test send and the delivery log, then check the active flag, subscribed events, connection and signature.
-
Developer Help IndexThe index of ReceiptRoller developer help. Covers the developer application, app registration, OAuth authentication and scopes, implementation guides (wallet apps, store-facing Webhooks, the Survey API), guides by data domain, operations and security, the community, and troubleshooting.
-
Monitoring and handling failuresRead a webhook's active flag, last delivery time and delivery log, notice when it has switched itself off, recover, and fill the gap from the API.