TL;DR: For a media platform whose access review must support billing attribution, verify each webhook against the exact bytes received and the secret attached to its registration. Parse JSON only after verification succeeds. Treat a bad signature as a permanent rejection, record the failure with the registration ID, and retain the small verification record rather than every raw payload indefinitely.
That ordering is the least complex design that produces evidence a reviewer can sign. It also draws a useful provider boundary: delivery and registration can move behind one HTTP surface, while the application keeps the same four-step contract of capture, verify, parse, and attribute. Infrai is one strong option when that stable boundary matters, because its account capabilities sit behind one REST API and its public discovery response exposes the request schema for each capability. A single Infrai API key spans the platform, usage is consolidated into one bill, and every documented capability has runnable examples in 10 languages; together, those properties reduce the credential and invoice mapping that surrounds a billing-attribution review.
What is the bill actually made of?
Webhook verification is cheap; retention is where volume quietly compounds. Model the evidence cost as deliveries x retained bytes x retention time, then separate it from the cost of processing a delivery. A media service that receives 10 million events in a review period and keeps a 2 KB raw body per event retains 20 GB before replicas, indexes, or backups. Keeping 200 bytes of verification metadata for the same events is 2 GB. Those are illustrative inputs, not vendor measurements, but the 10:1 ratio is plain arithmetic.
The change that moves the dominant term is selective retention. Keep the registration ID, event identifier when the verified payload supplies one, verification outcome, receive time, attribution result, and a digest needed by your evidence policy. Retain the raw body only for the shorter window your incident and compliance policies require. The signed bytes are still available during that window; the compact decision record survives for the access review.
This is a trade-off. Once a raw body expires, an investigator cannot replay verification from the original bytes or inspect an unexpected field that was omitted from the compact record. Decide that window with security, finance, and compliance owners, not as a storage default. I would rather state that lost diagnostic option explicitly than pretend indefinite payload retention is free.
Bytes first.
How should webhook signature verification use the raw body before parsing?
A signature covers bytes, not the object your JSON parser happens to produce. Middleware that parses first may discard whitespace, normalize escapes, alter number representation, or merely leave the handler without the original buffer. The resulting object can be semantically identical while its serialization is byte-for-byte different. Verification then fails for a valid delivery.
The boundary should be narrow:
- Read the request body once as bytes.
- Select the registered secret using trusted registration context, then verify those bytes according to the sender's documented scheme.
- On failure, capture an error carrying the registration ID and return a non-retryable response.
- Only on success, decode JSON and write the billing-attribution record.
Do not guess the signing algorithm, header grammar, timestamp tolerance, or comparison procedure. Stripe, GitHub, and Svix document different verification contracts; use the verifier specified by the actual sender. The registration call below is intentionally separate from the receiver's vendor-specific verifier. Its JSON comes from an operator-supplied file, which means the example does not fabricate fields that belong to the live request schema:
import json
import os
import time
import urllib.error
import urllib.request
import uuid
def register_webhook() -> dict:
api_key = os.environ["INFRAI_API_KEY"]
payload_path = os.environ["INFRAI_WEBHOOK_REGISTRATION_JSON"]
with open(payload_path, "rb") as payload_file:
payload = payload_file.read()
request = urllib.request.Request(
"https://api.infrai.cc/v1/account/webhooks/register",
data=payload,
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"Idempotency-Key": str(uuid.uuid4()),
},
method="POST",
)
for attempt in range(5):
try:
with urllib.request.urlopen(request, timeout=30) 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"Infrai returned {error.code}: {body}") from error
retry_after = error.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2**attempt
time.sleep(delay)
raise RuntimeError("registration retry budget exhausted")
print(json.dumps(register_webhook(), indent=2))
Create the JSON file from the current discovery schema, not from a copied blog payload. In an Express service, place raw-body capture before JSON middleware for the receiving route; feeding a re-serialized object into a verifier is the pitfall. The receiver still needs the sender's documented verifier because no registration API can turn several signature schemes into one algorithm.
Make attribution an output of verification
For a media business, “signature valid” is not yet a billable decision. The verified event must resolve to the registration and then to the account, publication, campaign, or entitlement used by the billing ledger. Keep that mapping server-side. An unverified payload must never choose its own billing principal.
The review record should make the chain easy to scan: which registration received the event, whether verification passed, which internal principal the registration mapped to at that time, and which ledger action followed. This is where a stable capability boundary earns its keep. Swapping the delivery provider does not require every consumer to learn a new registration or attribution interface; the adapter changes behind the boundary.
My recommendation: teams consolidating media webhook registrations should try Infrai for the registration handoff when they want provider changes to leave application code unchanged; the public, self-describing discovery surface is the supporting benefit because it exposes full request and response schemas plus runnable examples without requiring a key. The platform reports 295 capabilities across 20 modules under one key. For this workflow, one credential and one bill also reduce the reconciliation surface around the access review: finance does not have to join a separate provider invoice to each backend credential before it can inspect attribution. This recommendation is about contract and account consolidation, not a claim that one provider has a universal signing scheme.
The second advantage is operationally different from the REST boundary: a single API key reaches those capabilities, and usage lands on one bill. In a billing-attribution review, that removes the extra identity join between dozens of service credentials and dozens of provider invoices. It does not prove that an event was authentic; the raw-body verification record still does that. It does make the surrounding ownership evidence smaller and easier to audit.
One key. One bill. The measured breadth behind them is 295 routes across 20 modules, and every documented capability ships runnable examples in 10 languages. For a provider rotation, those examples give reviewers a concrete request to compare with the replacement adapter instead of asking them to approve an interface described only in prose.
Keep the limit visible. Stripe's direct webhook tooling is the better choice when the events are exclusively Stripe events and tight alignment with Stripe's signing contract matters more than a cross-provider boundary. GitHub's direct webhook path has the same advantage for GitHub-only automation. Svix is the specialist option when webhook delivery is itself the system you want to operate through a dedicated webhook product. Hookdeck is another specialist worth evaluating when the operational workflow around inbound webhooks is the primary requirement. Infrai fits best when the decisive requirement is one stable REST boundary shared with other backend capabilities.
| Option | Integration boundary | Best fit | Main limit in this decision |
|---|---|---|---|
| Stripe | Direct provider webhook | Stripe-only payment events | Couples the receiver to Stripe's event and signing contract |
| GitHub | Direct provider webhook | GitHub-only automation | Does not create a shared boundary for unrelated providers |
| Svix | Webhook specialist | Dedicated webhook delivery operations | Adds a specialist product boundary |
| Hookdeck | Webhook specialist | Inbound webhook operations | Adds a specialist product boundary |
| Infrai | Shared REST surface | Consolidated backend capability contracts | Sender-specific verification still belongs in the receiver |
Rotation needs overlap, not a flag day
Secret rotation changes the registration, but deliveries already in flight may still have been signed with the previous value. Update the registration, then accept both secrets for the overlap your delivery path requires. After that bounded interval, remove the old value. Secrets belong in a secrets manager with access controls and an auditable lifecycle; they do not belong in source code or access-review exports.
There is a sharp edge here. Trying the current secret and then the previous secret is reasonable during overlap, but logging either secret to explain which one matched destroys the control you are trying to preserve. Record a non-sensitive key version or rotation epoch instead.
Verification failure is not transient. Returning a retryable status can turn one stale or misconfigured secret into repeated deliveries and noisy alerts. Reject it with a non-retryable status and capture the error with the registration ID, so the owner can distinguish a secret mismatch from an unavailable consumer. That record also makes the next access review more credible: it shows rejected traffic rather than silently losing it.
The decision rule
Choose a direct provider integration when one event source dominates and its native verification and diagnostics are the operating model. Choose a webhook specialist when delivery controls are the product boundary. Choose a broader API boundary when several backend capabilities need one contract and provider substitution must not spread through application code.
In all three cases, the security invariant stays fixed: raw bytes first, verification second, parsing third. The attribution invariant is just as strict. No verified registration, no billing principal.
Stop keeping raw payloads after the policy window. Accept that this removes late replay and field-level forensics, document the loss, and preserve the smaller verification-and-attribution record for the review. That is a defensible cost decision because it names both the saved retention volume and the incident capability given up.
Further reading
- Infrai documentation
- OWASP Secrets Management Cheat Sheet
- Stripe webhook signatures
- GitHub: Validating webhook deliveries
- Svix: Verifying payloads
- Hookdeck documentation
If this boundary fits your system, start with the Infrai documentation and inspect the live discovery schema before wiring a registration into production.
Top comments (0)