Designing resends, ordering, and idempotency
For developers implementing the Webhook receiving side. It explains how to build a receiving side that behaves correctly in situations like "the same event arrived multiple times" or "an old event arrived after a new one".
Webhooks are a convenient mechanism, but the network is unstable and servers do go down. ReceiptRoller has a mechanism that automatically resends when delivery "didn't get through", but as a result, the same event may arrive two or more times and the order may be swapped. Design the receiving side on that premise.
Resend policy
If the receiving side does not return 2xx, ReceiptRoller attempts resends with exponential backoff.
| Attempt | Send timing |
|---|---|
| 1st | Immediate |
| 2nd | After 1 minute |
| 3rd | After 5 minutes |
| 4th | After 30 minutes |
| 5th | After 2 hours |
| 6th | After 6 hours |
| 7th | After 24 hours |
In total, it resends up to 7 times over about 32 hours. If it still does not succeed, it is recorded as a dead letter, which you can check in the developer portal's "Webhook delivery history".
Responses subject to resend
5xx(server error)429(rate limited)- Timeout (no response within 10 seconds)
- Connection error (DNS / TLS / TCP)
Responses not subject to resend
2xx: treated as success, not resent3xx: redirects are not followed automatically (treated as failure, but not resent)4xx(including 401/410): treated as a permanent rejection by the receiving side, not resent
5xx. Returning 4xx means no resend.
Order is not guaranteed
ReceiptRoller Webhooks do not guarantee delivery order. For example, situations like the following can occur.
receipt.refundedarrives beforereceipt.issuedproduct.updatedarrives beforeproduct.created- The first delivery is resent, so an old event arrives after a new one
For business logic that needs to handle ordering, look at occurred_at (the event occurrence time) in the payload and reorder on the receiving side, or design so that you re-fetch the latest state via the API. It is safer to treat a Webhook not as "a notification of the latest state" but as "a trigger that a change occurred".
Implementing idempotency
Ensuring that the same event arriving multiple times does not change the result is called idempotency. ReceiptRoller attaches a unique event_id to each event (and an X-RR-Event-Id header), so use it as the key to avoid duplicate processing.
Pattern 1: Record processed IDs
async function handleWebhook(event) {
// If already processed, ignore
const exists = await db.processedEvents.findOne({ event_id: event.event_id });
if (exists) return;
// Business processing
await processEvent(event);
// Record (ideally within the same transaction as the business processing)
await db.processedEvents.insert({
event_id: event.event_id,
received_at: new Date(),
});
}
Set a TTL on the processed-events table and auto-delete old records to prevent bloat (given the 32-hour resend window, retain for at least 7 days).
Pattern 2: Write with UPSERT
If the business processing "brings a record to its latest state", using UPSERT (INSERT or UPDATE) makes it naturally idempotent.
// Receive an inventory event and UPSERT keyed on the product ID
await db.inventory.upsert({
where: { product_id: event.data.product_id },
update: { quantity: event.data.quantity, updated_at: event.occurred_at },
create: { product_id: event.data.product_id, quantity: event.data.quantity },
});
With this pattern, you need to add an occurred_at comparison so that an old event arriving later does not overwrite the latest value.
Pattern 3: Determine by a natural key
If a business natural key (e.g. receipt_id) is available, using it for duplicate detection is also simple. Combining it with the event_id pattern makes it robust.
Handling out-of-order delivery
Typical measures for when multiple events for the same entity arrive out of order.
- Overwrite decision by occurred_at: do not apply an event older than the last-updated time you hold on the receiving side
- Version number: if the payload has a
version, adopt only the larger value - State-transition check: ignore transitions that are business-impossible, such as "refunded → issued"
- Re-fetch via API: on receiving a notification, re-fetch the latest state via the API (removes the need to think about order)
Handling dead letters
Events that fail all 7 resends are saved as "dead letters" and can be checked in the developer portal → the relevant endpoint → the "Delivery history" tab. Each record includes the following.
- Event ID, type, timestamp
- Each attempt's response code and body (first 1KB)
- A "Redeliver" button
After fixing the receiving side, you can manually resend with the "Redeliver" button. The window is 30 days after delivery.
Common anti-patterns
| A tempting implementation | The problem |
|---|---|
Because processing is heavy, wait until it completes before returning 2xx instead of enqueuing first |
The 10-second timeout causes a resend loop |
Return 200 when business processing fails |
It is not resent, so the event is lost |
| Increment/decrement stock without idempotency | Duplicate delivery throws off the stock count |
| Overwrite the latest flag without considering out-of-order delivery | An old event tramples the new state |
| Not monitoring dead letters | Days pass before you notice the failure |
The recommended receiving pattern
- Verify the signature and timestamp
- Idempotency check with
event_id(if already processed, immediately200) - Enqueue the payload into an internal queue (SQS / Cloud Tasks, etc.)
- Immediately return
200(the goal is within 1 second up to here) - Run the business processing in a queue worker (retries handled internally)
With this pattern, the receiving endpoint can always respond quickly, and business-processing failures do not lead to a resend loop.
Related guides
-
How to register a WebhookExplains how to register a Webhook endpoint in the ReceiptRoller developer portal, how to choose subscribed events, test delivery, using multiple endpoints, and how to delete or pause an endpoint.
-
Webhook is not being deliveredHow to isolate the cause when a Webhook does not reach your receiving endpoint. Walks through checking, in order, the endpoint settings, subscribed events, reachability, signature-verification failures, and firewalls.
-
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 failuresExplains how to read the ReceiptRoller Webhook delivery history, the metrics to monitor and how to design alerts, redelivering dead letters, and common failure patterns and recovery procedures.
-
SNS Webhook bypass (forwarding Webhooks from external SNS such as LINE)Explains the mechanism, setup, signature handling, and cautions of the "SNS Webhook bypass" feature, which forwards Webhooks ReceiptRoller receives from SNS platforms such as LINE to a developer app, with the store's consent.