Normalizing Cursor, Offset, and Link Header Pagination
When a service combines data from several APIs, each API may use a different method to return the next batch of records.…
When a provider sends a webhook, it sends an HTTP request to your endpoint to report an event, such as an order update. The provider expects an HTTP response confirming receipt. If your service handles the event but the response is lost or arrives too late, the provider may send the request again. Your endpoint then receives a duplicate delivery, another request that may represent the same underlying event.
Expect a provider to send the same delivery more than once, and use its documented identifiers to recognize repeats. Verify each delivery and save it in storage that survives a restart before acknowledging it. Then make sure a repeated request does not repeat the business action. This handles retries and later replays without treating every similar-looking request as the same event.
A provider judges whether delivery succeeded by what it sees, especially the HTTP response. The receiver may finish the work even when the provider does not see a successful response:
A missing acknowledgment does not tell the provider whether the receiver finished the work. The provider can send the request again after the receiver has completed the action.
Slow handlers make this situation more likely. Shopify’s HTTPS webhook documentation specifies a five-second overall timeout, while GitHub treats a response taking longer than ten seconds as a failure. Both providers recommend queueing longer-running work so the endpoint can respond promptly. Those limits and retry rules are provider-specific, so check the documentation for the webhook product you use. See Shopify’s delivery guidance and GitHub’s webhook best practices.
A provider can also retry after an error response or replay a failed delivery later. For example, PayPal documents up to 25 delivery attempts over three days for REST webhooks when it does not receive a successful response. GitHub does not automatically redeliver failed deliveries, but an operator can redeliver them manually or use a script to recover them. A receiver should not assume that every provider retries, or that retries stop when an automatic window ends.
Use a provider-defined identifier, and confirm what that identifier represents. A delivery ID identifies a particular delivery; an event ID may group multiple deliveries caused by the same underlying action. Those identifiers serve different purposes.
GitHub says X-GitHub-Delivery stays the same when an operator redelivers a webhook, making it useful for recognizing a replay of that delivery. Shopify distinguishes X-Shopify-Webhook-Id, which identifies an individual delivery, from X-Shopify-Event-Id, which can be shared by separate subscription deliveries for the same merchant action. Shopify’s identifier guidance makes the distinction explicit.
Choose the key according to what you want to suppress. If you want to ignore another attempt at the same delivery, use the provider’s delivery identifier. If you also need to correlate deliveries across subscriptions, retain the event identifier for that purpose. Deduplicating only by a shared event ID can suppress work intended for a separate subscription.
Matching payload bodies is not a dependable substitute for provider identifiers. Similar payloads help investigate repeated requests, but they do not establish that the requests represent the same delivery or should receive the same handling.
A webhook endpoint should verify the request, save the delivery so it can be handled later, and then acknowledge it. If the business work takes longer than the provider’s response window, the endpoint can save the verified delivery in storage or a queue that survives a restart, then return success before the work finishes. The receiver can recover a saved delivery after a process restart or failure.
The endpoint must save the delivery before returning success. Otherwise, a crash can make the provider believe the receiver accepted work that was then lost. If the endpoint waits for all business work to finish, a timeout can prompt the provider to resend a delivery the receiver already handled.
Save each delivery in a persistent record indexed by the provider’s delivery identifier. Add a rule that prevents two requests from claiming the same delivery at once. A separate “check, then insert” can fail when two copies arrive almost simultaneously: both requests can check before either one writes. A unique key enforced by the datastore, or another operation that checks and claims the delivery as one step, prevents this race.
Record whether the receiver has queued a delivery or finished its business work. If a worker fails after the receiver acknowledges the request, the receiver needs a way to find and retry that unfinished work. The right storage and transaction design depends on how the receiver is built.
If a verified request has a delivery identifier that the receiver already recorded as accepted, return success so the provider does not retry it again. Shopify recommends skipping a known duplicate and returning success. Do this only when the record shows that the receiver can recover the work. A record created before the receiver safely queues the delivery does not show that.
Check the provider’s signature separately from duplicate detection. A signature, checked with a shared secret, helps verify that the request came from the expected provider and was not altered. A delivery ID identifies a repeated request. These checks serve different purposes. Shopify documents HMAC signature verification for HTTPS webhooks, and GitHub recommends validating deliveries with a webhook secret.
Log the provider’s identifier, event type, receipt time, response status, and processing result. Include the attempt number or failure reason when the provider provides it. These details help you tell a retry from a separate delivery and see whether slow responses or processing failures are prompting repeats. GitHub documents delivery records and failure details in its failed-delivery guidance.
A duplicate does not always mean the provider retried automatically. An operator may have replayed a failed delivery, or separate subscriptions may have sent separate deliveries for one action. Providers also make different guarantees. Zendesk describes delivery as best effort, which means some circumstances can result in duplicate or missed events. Its webhook documentation describes its retry conditions.
Keep deduplication records for a period that matches the provider’s documented retry and replay behavior and your recovery practices. Automatic retry windows vary, and operators can redeliver events after those windows end. No single retention period works for every integration. If operators can replay older deliveries, make sure the receiver’s records and recovery procedures cover them.
Give Vroni a GitHub issue, bug report, spec, or rough idea. It reads the repo, plans the change, writes code, runs checks, and works toward a review-ready pull request.
Take a look at vroni.com