Defining Stable Ordering Rules for Cursor Pagination
When an API returns a large collection in pages, a client uses a cursor to ask for records after or before a…
A webhook lets a service send an HTTP request to a URL when an event occurs. The receiving service returns an HTTP response, and the sender uses that response to decide whether the delivery succeeded. The API contract should state how long the sender waits, which responses trigger another attempt, and what happens when it stops trying.
These rules vary by provider. A 2xx response often acknowledges delivery, but the receiver may still need to process the event. A timeout does not prove the receiver did no work. It might have processed the event before its response was lost. A useful contract therefore states what acknowledgment means, how the sender classifies failures, and how the receiver can recover an event that was not delivered.
HTTP Semantics, RFC 9110 defines general HTTP behavior, including the Retry-After response header. That header carries either a date or a delay in seconds. The RFC does not set webhook response deadlines, prescribe retry schedules, or say when a sender must disable a subscription.
The Standard Webhooks specification offers webhook-specific recommendations, including treating 2xx responses as successful, using exponential backoff with jitter for retries, and considering Retry-After. These recommendations are useful starting points, but they are not an IETF standard and do not describe every provider’s behavior.
The contract must therefore state the provider’s policy: accepted response codes, deadlines, which outcomes trigger retries, attempt limits, and what the sender does when attempts end.
A response deadline is the maximum time the sender waits for a response before marking an attempt as timed out. The contract should state when the clock starts, whether connection setup has a separate limit, and what happens when the deadline expires.
The contract also needs to define what a successful response acknowledges. The receiver might acknowledge that it received the event, stored it durably for later processing, or completed the relevant business operation. These promises mean different things. If the receiver returns success after placing an event in a queue, the contract should say that acknowledgment means durable acceptance for asynchronous processing, rather than completed processing.
Provider deadlines illustrate why a contract should give its own exact values. GitHub recommends a 2xx response within 10 seconds. Shopify documents a one-second connection timeout and a five-second overall timeout. The Standard Webhooks specification recommends a request timeout between 15 and 30 seconds. These are provider-specific examples and a community specification recommendation, not interchangeable defaults.
For asynchronous processing, the receiver should save the event at the level promised by the contract before acknowledging it. A receiver that acknowledges before durable storage risks losing an event if it fails before processing begins. The sender’s deadline should also say whether a timeout counts as a failed attempt and whether it triggers a retry.
A failed delivery does not guarantee another attempt. The sender can record an attempt as failed and stop there. The contract should map each relevant failure to an explicit action rather than relying on broad labels such as “retry on errors.”
| Outcome | What the contract should specify |
|---|---|
| Connection failure or timeout | Whether the sender retries, how the attempt counts, and when the next attempt occurs. |
2xx response |
Which codes count as success and what the receiver has accepted by returning one. |
3xx response |
Whether the sender follows redirects or treats the response as failure. Standard Webhooks recommends treating redirects as failures. |
4xx response |
Which codes are permanent and which, if any, are retryable. Do not assume every sender treats this class the same way. |
429 Too Many Requests |
Whether the sender retries and whether it follows a Retry-After value. |
5xx response |
Which codes trigger another attempt and how the sender schedules it. |
410 Gone |
Whether the sender disables the endpoint or subscription, if it supports that behavior. |
For example, Amazon SNS retries HTTP/S responses in the 5xx class and 429, while treating other response codes as permanent failures under the documented policy. GitHub, by contrast, does not automatically redeliver failed deliveries; a consumer must request redelivery manually or through an API. That difference shows why an API contract must state its own mapping.
The retry rule should state the maximum number of attempts and whether that count includes the initial delivery. It should also give the maximum time the sender will keep trying, the time between attempts, and whether the sender adds random timing variation, called jitter, to reduce simultaneous requests. If the sender honors Retry-After, the contract should state which responses qualify and what happens when the requested delay extends beyond the retry window.
Retries also mean that receivers must account for duplicate events. A sender might retry after the receiver has processed an event but before the sender receives its acknowledgment. Shopify warns that duplicate webhook deliveries can occur and recommends idempotent handling or deduplication. Its documentation distinguishes an event identifier from a delivery identifier, which helps consumers decide whether two requests represent the same underlying event or a repeated delivery. The contract should define the scope and stability of its identifiers, especially across retries and manual redelivery.
When a sender stops trying to deliver one event, that event has reached a terminal state: it will receive no further automatic attempts. That state does not necessarily mean the sender has disabled the endpoint or removed the subscription. Contracts should describe those outcomes separately.
After the sender reaches the retry limit, it might discard the event, keep it for recovery, place it in a dead-letter queue, or make it available for manual redelivery. It should also state whether the subscription remains active and how a consumer can resume delivery. A dead-letter queue is a separate holding area for messages that the sender could not deliver under its normal policy.
Provider behavior varies. Amazon SNS can discard an exhausted message unless a dead-letter queue is configured, without changing the subscription’s status. Shopify documents automatic removal of some subscriptions after repeated failures. GitHub supports manual or scripted redelivery of failed deliveries rather than automatic retries. The contract must say whether reaching the retry limit affects only the event or also the subscription, and whether recovery preserves the original event identifier.
The contract should also tell consumers where they can inspect delivery history. Useful details include attempt number, response code, response time, timestamp, and the reason the sender classified an attempt as failed. Those details help a consumer distinguish an endpoint outage from a permanent response error and determine whether manual recovery is possible.
Document each webhook event family using the same core decisions, even if the contract presents them in prose, a table, or an API-description format. At minimum, state the accepted response codes and their meaning; the deadline and its starting point; the retryable and terminal outcomes; the retry count and window; and the event’s terminal state after retries end.
For example, contract wording can follow this structure, with each bracket replaced by the provider’s actual behavior:
The sender considers a delivery acknowledged when the receiver returns [accepted status codes] within [deadline], measured from [defined starting point]. Acknowledgment means [receipt, durable acceptance, or completed processing].
The sender retries [listed failures] for up to [attempt limit and time window], using [retry schedule]. The attempt count [includes or excludes] the initial delivery. The sender [does or does not] honor
Retry-Afterfor [specified responses].After the retry limit, the sender [discards, retains, or makes the event available for redelivery]. Exhaustion [does or does not] disable the subscription. The sender identifies the event with [event identifier] and each delivery with [delivery identifier].
This wording makes delivery behavior testable and understandable across frameworks. The values must match the sender’s implementation; the template does not establish a universal webhook policy.
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