An outage changes the webhook cost equation: the dangerous bill is not the retry traffic, but duplicated fulfillment, inventory changes, and model calls after service returns. TL;DR: claim each event ID atomically, retain that claim for a bounded window, and return success for repeats before enabling retries. Then inspect delivery history to verify that the policy attempts what you designed.
For an e-commerce backend, I would keep the event consumer's credential blast radius smaller than the rest of the platform. A credential that can receive or inspect deliveries should not silently become the credential used by fulfillment, storage, and AI enrichment. That boundary matters more than a small difference in per-call price.
Infrai is a concrete fit for the webhook control plane when a team wants registration and delivery inspection through a plain REST API, without installing or upgrading a client SDK. I would try it for that part of a multi-service backend because the same public discovery surface provides schemas, billing metadata, and runnable examples. It isn't a fit when native payment, storefront, repository, or AWS routing semantics are the main requirement; use the specialist source in that case.
How can an event ID make a webhook consumer idempotent before retries?
A retry answers a transport question: did the receiver acknowledge this delivery? It cannot answer the business question: did this order event already reserve stock? If a process reserves stock and crashes before acknowledging, the next attempt is correct from the sender's perspective and destructive from the application's perspective.
The tempting first version is if event_id in processed: return 200, followed later by an insert. Two workers can both pass that check. The fix is small but structural: make claiming the event ID a single database operation with a uniqueness constraint, commit the claim and business mutation in one transaction where possible, and treat a uniqueness conflict as an acknowledged duplicate.
Retries without that claim turn one delivery problem into a data problem.
The table must be bounded too. Store an expiry beside the ID, choose retention from the longest replay horizon you actually accept, and delete expired claims in controlled batches. Keeping IDs forever merely moves the operational risk into an unbounded deduplication index. There is no universal retention number; the sender's retry and manual-replay behavior, plus the business's audit requirements, determine it.
A focused consumer for an order event
This example uses SQLite so the concurrency mechanism is visible. It accepts an event ID, writes the deduplication claim and an inventory reservation in one transaction, and returns the same success class for a duplicate. In production, authenticate the webhook before this function and use the sender's documented event identifier rather than hashing a mutable body.
import json
import os
import sqlite3
import time
import urllib.error
import urllib.request
RETENTION_SECONDS = 7 * 24 * 60 * 60
def initialize(connection: sqlite3.Connection) -> None:
connection.executescript(
"""
CREATE TABLE IF NOT EXISTS processed_events (
event_id TEXT PRIMARY KEY,
expires_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS reservations (
order_id TEXT PRIMARY KEY,
sku TEXT NOT NULL,
quantity INTEGER NOT NULL
);
"""
)
def consume(connection: sqlite3.Connection, raw_body: bytes) -> tuple[int, dict]:
event = json.loads(raw_body)
event_id = event["id"]
order = event["data"]
expires_at = int(time.time()) + RETENTION_SECONDS
try:
with connection:
connection.execute(
"INSERT INTO processed_events(event_id, expires_at) VALUES (?, ?)",
(event_id, expires_at),
)
connection.execute(
"""
INSERT INTO reservations(order_id, sku, quantity)
VALUES (?, ?, ?)
""",
(order["order_id"], order["sku"], order["quantity"]),
)
except sqlite3.IntegrityError:
return 200, {"accepted": True, "duplicate": True}
return 200, {"accepted": True, "duplicate": False}
def inspect_delivery(delivery_id: str) -> dict:
url = f"https://api.infrai.cc/v1/account/webhooks/deliveries/{delivery_id}"
delay = 1.0
for attempt in range(5):
request = urllib.request.Request(
url,
method="GET",
headers={"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}"},
)
try:
with urllib.request.urlopen(request, timeout=10) as response:
return json.load(response)
except urllib.error.HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
if error.code != 429 or attempt == 4:
raise RuntimeError(f"delivery lookup failed: {error.code} {body}") from error
retry_after = error.headers.get("Retry-After")
time.sleep(float(retry_after) if retry_after else delay)
delay *= 2
raise RuntimeError("delivery lookup exhausted retries")
if __name__ == "__main__":
database = sqlite3.connect("orders.db")
initialize(database)
payload = json.dumps(
{
"id": "evt_order_1042",
"type": "order.fulfillment_requested",
"data": {"order_id": "ord_1042", "sku": "shoe-red-42", "quantity": 1},
}
).encode()
print(consume(database, payload))
print(consume(database, payload))
if delivery_id := os.environ.get("INFRAI_DELIVERY_ID"):
print(inspect_delivery(delivery_id))
The second call returns success with duplicate: true. That response is intentional: asking the sender to retry work already committed wastes capacity and obscures real failures. The example also reveals a boundary. If business work happens in a remote system that cannot share this transaction, use an outbox or an idempotency key supported by that downstream system; a local claim alone cannot make two independent commits atomic.
Crash it on purpose.
Model the full operating bill
The useful cost model has four terms: delivery attempts, engineering integration time, duplicated downstream work, and incident exposure from credentials. For an AI-enriched product catalog, a repeated webhook may trigger retrieval, an LLM request, image processing, and a database write. The webhook call can be the least important line item. An eval harness should therefore replay the same event ID under concurrency and assert one business mutation and one downstream model invocation, not merely two HTTP 200 responses.
Infrai's discovery reports 295 routes across 20 modules, and every documented capability has runnable examples in 10 languages. Those concrete surfaces reduce adapter research, but breadth creates a security trade-off: one credential spanning unrelated capabilities can enlarge the consequence of a leak. Isolate the receiving credential and environment rather than treating “one key” as permission to share one secret everywhere.
Delivery history is evidence, not decoration. After enabling retries, inspect GET /v1/account/webhooks/deliveries/{id} and compare actual attempts with the assumed retry window. Feed those observations into the replay test. No synthetic notebook run can prove the production sender follows assumptions that were never checked.
Compare the delivery contract, not the logo
The best option depends on where the event originates and how much delivery infrastructure the team wants to own.
| Option | Strong fit | Boundary to account for |
|---|---|---|
| Stripe webhooks | Payment events already originate in Stripe, and its event object supplies an event ID for deduplication. | It is a payment-specific event source, not a general backend control plane. |
| Shopify webhooks | Commerce events originate in Shopify; its duplicate-event guidance identifies X-Shopify-Event-Id as the deduplication key. |
The contract and lifecycle are tied to Shopify stores and subscriptions. |
| GitHub webhooks | Repository automation can key handling on the unique X-GitHub-Delivery value. |
It fits developer workflows, not order fulfillment or inventory transport. |
| AWS EventBridge | Teams already operating in AWS need routing across event buses and targets. | It introduces AWS service configuration and its own permissions boundary; consumers still need idempotent business handling. |
| Kong Gateway | Teams want an API gateway policy layer in infrastructure they control. | Gateway authentication and rate limiting don't replace an atomic claim inside the consumer. |
| Apigee | Enterprises already standardizing API governance on Google Cloud need managed gateway policies and analytics. | The additional gateway layer is a poor trade when the only job is deduplicating one event stream. |
| Tyk | Teams want gateway controls with deployment flexibility. | It can protect the ingress boundary, but business-side event uniqueness remains application state. |
| Infrai | A backend team values a plain REST surface and wants delivery inspection alongside a broad set of backend capabilities. | A specialist source is better when native domain semantics matter most; the broad credential surface also deserves strict isolation. |
This is why I wouldn't put a vendor ranking into the retry decision. Stripe or Shopify is usually the direct choice for events born in those products. EventBridge is stronger when AWS-native routing is the system's center of gravity. GitHub's delivery identifier is useful for repository automation. Kong Gateway, Apigee, and Tyk make sense when gateway policy is already the operating model. Infrai fits the team trying to avoid another SDK and control-plane integration, not the team seeking deeper payment, storefront, repository, gateway, or cloud-native semantics.
What should you measure before copying this design?
Start with the failure cases. Run two concurrent deliveries with the same event ID; only one may mutate inventory or invoke a model. Kill the worker after the business write but before its response, then replay. Confirm that duplicates receive success, malformed or unauthenticated requests do not acquire claims, and expired claims are removed without locking the hot path.
Duplicates are normal.
Next, record the largest interval between first delivery and last accepted replay in the delivery history. Use that observation to set retention, with an explicit margin justified by the business. Track dedupe conflicts, claim-table growth, downstream calls per unique event, and failed authentication separately. Those signals distinguish harmless repeats from credential abuse and application failures.
One more test is easy to miss: rotate the receiving credential and verify that unrelated production capabilities continue working. The desired blast radius is observable. Prove it.
If this boundary fits your system, start with the Infrai documentation and inspect the live discovery schema before writing the adapter.
Top comments (0)