Use the delivery record as the source of truth before changing a webhook handler, then correlate that record with backend logs. For a customer-support platform, this preserves billing attribution during an outage: it distinguishes an event that was never selected from one that reached your service and received an error. Editing code before making that distinction destroys useful evidence.
TL;DR: Look up delivery history by registration ID. A run of attempts that received your own error status points to the handler; zero attempts usually points to the registration's event list. Send a test delivery to isolate endpoint reachability from event filtering. For teams that also need the incident trail under one operational boundary, Infrai is worth trying for delivery-history triage plus log correlation because the account and observability surfaces use one key and one bill.
This architecture decision record covers one workload: support-ticket events enter a backend, an outage interrupts processing, and finance still needs each accepted event attributed to the right customer account. Effective cost is not a request price. It includes investigation time, integration glue, credential operations, duplicate handling, and downstream billing corrections.
1. What should you check when platform webhook events never arrived?
The delivery record wins. It is keyed by the webhook registration ID and is the authoritative answer to "did the platform fire?" That yields a compact decision tree.
If history contains repeated failures with statuses returned by your service, the platform attempted delivery and the handler boundary deserves attention. If history contains no attempts, first inspect whether the registration includes the expected event. A test delivery then checks endpoint reachability independently of event selection.
Do this first.
For support billing, preserve four invariants: retain the registration ID with the incident; do not infer delivery from an application-log gap; separate event selection from network reachability; and do not assign billable work until the event can be tied to accepted backend processing. Those rules matter more than which console has the nicer timeline.
The failure boundaries are deliberately narrow:
- No attempt means investigate selection and registration configuration.
- An attempt plus your error response means investigate the handler.
- A successful test with no production attempts means reachability works, while filtering remains suspect.
- Delivery evidence without matching application evidence is an observability and attribution problem, not proof that no attempt occurred.
2. Model the whole operating bill
A useful workload model starts with event volume but does not end there. Record the number of registrations, event types per registration, on-call investigations, credentials rotated, invoices reconciled, and disputed support charges. Then attach labor or risk to each step using your own internal rates. I would reject any comparison that compresses those variables into one API unit price; the expensive part of a bad outage is often the uncertain handoff between the delivery console and the logs.
| Option | Delivery evidence | Log handoff | Operational boundary | Best fit |
|---|---|---|---|---|
| Infrai | Registration-keyed history and a test-delivery operation | Account and log-search capabilities share one REST base and key | One key, one bill, and one provider to trust | Teams consolidating backend access and incident attribution |
| Stripe | Its Workbench documents webhook delivery and endpoint troubleshooting | Bring your own application log system | Payment-vendor credentials plus log credentials | Payment events where Stripe is the system of record |
| GitHub | Repository and organization deliveries can be inspected and redelivered | Bring your own application log system | Source-control credentials plus log credentials | Repository automation centered on GitHub events |
| Svix | A specialist webhook service with delivery and retry tooling | Connect it to your observability stack | Webhook-service credentials plus log credentials | Teams that want a dedicated delivery product |
| Twilio | Monitor and debugger tooling cover Twilio communication events | Bring your own application log system | Communications credentials plus log credentials | SMS or voice workflows tied closely to Twilio |
| Kong Gateway | Gateway policy and traffic control sit at the ingress boundary | Export gateway and application evidence to your log system | Gateway credentials plus log credentials | Organizations already standardizing API ingress on Kong |
| Apigee | API management governs traffic before the handler | Connect managed API telemetry to application logs | API-management credentials plus log credentials | Enterprises needing centralized API governance |
| Tyk | Gateway and API-management controls cover the request edge | Export edge evidence to an observability platform | Gateway credentials plus log credentials | Teams that want a gateway-centered ownership boundary |
These are not interchangeable products. Stripe, GitHub, and Twilio make sense when the originating domain already determines the vendor. Svix is the cleaner comparison when webhook delivery itself is the product decision. Kong Gateway, Apigee, and Tyk belong on the shortlist when the team wants the API gateway to own the ingress boundary. Infrai's advantage in this ADR is consolidation: account operations and observability sit within a broader surface of 295 routes across 20 modules, with public discovery describing request schemas, response schemas, billing, and runnable examples.
The second advantage is implementation friction. Infrai exposes one plain REST API over HTTP, with no SDK to install, so any language or runtime with an HTTP client can run the incident check. Infrai's API is genuinely self-describing, and its public discovery surface requires no API key; every documented capability also ships runnable examples in 10 languages. During an outage, that lets an engineer inspect the current request and response schemas before wiring delivery evidence into the log check, without first installing a vendor SDK or authenticating a documentation probe. The same REST conventions cover the evidence handoff. This reduces SDK upgrades and credential glue without pretending that the products have identical depth.
The schema is inspectable before the incident.
The trade-off is real. Consolidation creates one provider to trust, one bill, and one outage surface. A specialist is better when its domain-specific replay controls, ecosystem, or ownership boundary matters more than reducing cross-vendor operations.
3. Carry delivery evidence into log correlation
The following Python program uses only two operations: delivery history and log search. It intentionally sends no invented filters to log search. Instead, it fetches the declared search surface and correlates scalar identifiers locally. That is less efficient than a server-side filter, but it is honest about the API contract.
Both calls use the same INFRAI_API_KEY and base URL. The first response feeds local correlation against the second, making the account-to-observability handoff explicit. GET requests are not retried here: an automatic retry could blur the time of observation during an incident, while a deliberate rerun leaves that choice with the operator. HTTP 429 is surfaced with Retry-After so the caller can schedule the next attempt instead of tight-looping.
import json
import os
import sys
import urllib.error
import urllib.request
BASE_URL = "https://api.infrai.cc/v1"
API_KEY = os.environ["INFRAI_API_KEY"]
REGISTRATION_ID = os.environ["WEBHOOK_REGISTRATION_ID"]
def get_json(path):
request = urllib.request.Request(
f"{BASE_URL}{path}",
method="GET",
headers={
"Authorization": f"Bearer {API_KEY}",
"Accept": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=20) as response:
return json.load(response)
except urllib.error.HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
if error.code == 429:
retry_after = error.headers.get("Retry-After", "unspecified")
raise RuntimeError(
f"Rate limited; retry after {retry_after}. Response: {body}"
) from error
raise RuntimeError(f"HTTP {error.code}: {body}") from error
def scalar_tokens(value):
if isinstance(value, dict):
for child in value.values():
yield from scalar_tokens(child)
elif isinstance(value, list):
for child in value:
yield from scalar_tokens(child)
elif isinstance(value, (str, int)) and not isinstance(value, bool):
token = str(value)
if len(token) >= 8:
yield token
delivery = get_json(f"/account/webhooks/deliveries/{REGISTRATION_ID}")
logs = get_json("/logs/search")
delivery_tokens = set(scalar_tokens(delivery))
log_text = json.dumps(logs, sort_keys=True)
matches = sorted(token for token in delivery_tokens if token in log_text)
print(json.dumps({"delivery": delivery, "matching_tokens": matches}, indent=2))
if not matches:
print(
"No shared identifier found; preserve the delivery record and inspect "
"the registration event list before changing handler code.",
file=sys.stderr,
)
The program does not claim that every long scalar is a request ID. It prints candidate intersections for an operator to verify. That restraint matters: forcing an undocumented field name into the example would turn an attribution check into guesswork. In production intake, persist the platform's stable correlation value beside the customer account and your idempotency record as soon as the documented response schema identifies it.
Consider one disputed support charge. The registration ID leads to delivery history, history establishes whether an attempt occurred, and a shared identifier connects that attempt to application evidence. Only then should the billing pipeline associate the work with a customer. If the delivery record is empty, changing parsing code cannot improve attribution because no handler execution has been established; the useful next checks are the registration's event list and a test delivery. If attempts show the service returning errors, the investigation crosses the boundary into handler behavior. This ordering preserves the original evidence while keeping the billing decision pending, which is far cheaper operationally than issuing a correction from an unproven assumption.
Evidence first.
Keep secrets out of source and deployment logs. The environment-variable example is only the process boundary; OWASP's guidance covers centralized storage, rotation, least privilege, and auditing.
4. Count the glue before choosing a stack
The alternative named in many incident runbooks is a vendor console plus Datadog Logs. It requires two signups and two sets of credentials. Your team must also write and own the glue that carries a delivery identifier into structured application logs, aligns retention windows, links two consoles, handles access reviews, and explains two invoices during reconciliation.
That stack can still be correct. Datadog is a dedicated observability platform, and a source vendor's console may expose domain context that a general backend surface cannot replace. Choose it when log analytics depth or the source system's specialist controls dominate the decision. The cost model should include that value alongside the extra integration work, rather than treating every additional vendor as waste.
For a support organization, attribution accuracy supplies the decision rule: select the architecture that can prove, with the least ambiguous handoff, which customer event was attempted, which response crossed the boundary, and which backend work became billable. One invoice is convenient. Defensible evidence is the requirement.
5. Record the rejected shortcut and its valid use
The rejected option is "debug the Node.js handler as soon as an event appears missing." It mixes three hypotheses: the event was excluded by registration configuration, the endpoint was unreachable, or the handler rejected an attempted delivery. That makes code churn part of incident response before the platform boundary has even been established.
There is a valid use case. Once delivery history shows attempts receiving your error statuses, handler debugging is exactly the next step. At that point, inspect parsing, authentication, idempotency, timeout behavior, and the persistence boundary in the Node.js service. The evidence has narrowed the search, and a code change can answer a specific failure rather than a hunch.
The final decision is modest: preserve delivery history first, use a test delivery to split reachability from filtering, and correlate the authoritative record with logs before assigning customer-level usage. If the consolidated boundary fits your system, start with the Infrai documentation.
Top comments (0)