Spurlock Studios
Contact
Share LinkedIn X
A blank 21+ token. Thesis: WEBHOOK SECURITY AUTOMATIONS SIGNATURES SECRETS.

An open webhook URL is not an integration. It is a public function that mutates your business systems if you let it.

Production automations verify who is calling, reject traffic that is too old or already applied, and assume someone will replay a captured POST. This is the security baseline Spurlock Studios applies to n8n webhooks after 500+ automations — before any CRM or payment node runs.

Parent: Production n8n handbook.

The short answer

  • Verify the vendor signature on the raw body — Stripe’s Stripe-Signature header and GitHub’s X-Hub-Signature-256 are HMAC checks, not “trust the JSON.”
  • Reject replays — Stripe signs a timestamp and defaults to a 5-minute tolerance. GitHub does not; store X-GitHub-Delivery and apply idempotency keys.
  • Allowlist webhook IPs — Stripe publishes a dedicated webhook list. GitHub publishes hooks ranges on GET /meta. Neither list is identity.
  • Do not confuse n8n Header auth with HMAC — a shared header secret is weaker than a body signature. Use both only when the vendor cannot sign.
  • Least privilege on the side-effect token — a valid Stripe event still should not let the workflow delete the workspace.

What threats should a production webhook actually cover?

You do not need nation-state paranoia. You need the five failures that show up in real n8n graphs.

ThreatWhat the attacker doesWhat breaks
Forged eventPOSTs JSON to your production URLFake CRM rows, fake “paid” emails, fake deploys
ReplayResends a captured, still-valid signed bodyDuplicate invoices, double fulfillment, double Slack pings
Secret leakCopies a URL, whsec_, or GitHub secret from git or a screenshotAnyone who holds the secret can mint valid signatures
Over-scoped tokenUses founder OAuth that can delete the workspaceOne bad event becomes a wipe
Confused deputyTrusts a field (email, price_id, repo) as identityClient-side form values become server truth

Unsigned traffic is the cheap version of the first row. Signed-but-replayed traffic is the expensive version of the second.

  • Forged POST without a signature is rejected
  • Bad signature is rejected
  • Old timestamp (Stripe) is rejected
  • Same event ID twice applies once
  • Side-effect credential cannot do more than the workflow needs

Skip any three of those and you are running an honor system.

How do Stripe and GitHub sign webhook deliveries?

Both vendors sign the bytes they sent, not the object your stack built after JSON.parse. That distinction is the whole job.

VendorHeaderSchemeReplay hint in the signature
StripeStripe-SignatureHMAC-SHA256 over timestamp.raw_body; live scheme is v1Timestamp t= is signed. Libraries default to 5 minutes. Tolerance 0 disables the recency check.
GitHubX-Hub-Signature-256HMAC-SHA256 hex digest, prefixed sha256=No timestamp. Replay defense is X-GitHub-Delivery plus your store.

Stripe’s header looks like t=1492774577,v1=…. The timestamp is part of the signed payload, so an attacker cannot slide t= forward without invalidating v1. Stripe documents this under preventing replay attacks: if the signature is valid but the timestamp is too old, reject it. Keep the n8n host on NTP. Stripe also warns that a tolerance of 0 turns the recency check off.

GitHub computes an HMAC hex digest of the raw body with your secret and sends it as X-Hub-Signature-256: sha256=…. Their docs are explicit: never compare with ==. Use a constant-time check such as Node’s crypto.timingSafeEqual. The older X-Hub-Signature header is HMAC-SHA1 and exists for legacy only — do not verify against it.

Stripe’s own signature troubleshooting names the usual break: the framework parsed JSON, re-ordered keys, or changed whitespace before verification. GitHub says the same if a proxy rewrites the body.

Provider-specific facts worth budgeting for:

  1. Stripe test vs live secrets differ even if the URL is the same. A Dashboard whsec_ will not verify events from stripe listen, and the reverse is also true.
  2. Stripe retries mint a new signature and timestamp for each delivery attempt. A 5-minute window still accepts a legitimate retry. It does not accept a three-day-old capture.
  3. GitHub redelivery keeps the same X-GitHub-Delivery. That is the idempotency key, not a new event.
  4. GitHub’s documented test vector — secret It's a Secret to Everybody, payload Hello, World! — must produce sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17. If your Code node cannot hit that, stop and fix encoding before you touch production.

How do you verify signatures in n8n without breaking the raw body?

n8n’s Webhook node can require Basic, Header, or JWT auth. That is useful. It is not Stripe or GitHub HMAC.

Header auth checks that a caller sent a static name/value pair. HMAC checks that the body matches a signature the vendor computed with a shared secret. A stolen URL plus a guessed header is still a forged event if you never check the body.

Pattern that survives production:

  1. Webhook node: HTTP Method POST, enable Raw Body, set Respond to Immediately (or a Respond to Webhook node that returns 2xx fast).
  2. Optional: IP(s) Allowlist for the vendor webhook ranges.
  3. Code node: read the raw bytes, compute the expected signature, compare constant-time, then reject.
  4. Only after a pass: parse JSON, validate schema, apply idempotency, then side effects.

Stripe wants a 2xx before heavy work — their webhook handler guide says return success prior to logic that can time out. GitHub is sharper: respond within 10 seconds or they tear down the connection and mark the delivery failed.

Pseudo-check for a GitHub-style header. Adapt the signed string for Stripe (${t}.${raw}):

const crypto = require("crypto");
const secret = $env.WEBHOOK_SECRET;
const header = $headers["x-hub-signature-256"];
const raw = $json.rawBody; // only if Raw Body is on
if (!header || !raw) {
  throw new Error("Missing signature or raw body");
}
const expected = "sha256=" + crypto.createHmac("sha256", secret).update(raw).digest("hex");
const a = Buffer.from(header);
const b = Buffer.from(expected);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
  throw new Error("Invalid webhook signature");
}
return [{ json: JSON.parse(raw) }];

A !== on hex strings is how toy demos get written. GitHub’s docs call that out as a timing-attack footgun. Match their bar.

n8n settingWhat it actually doesWhat it does not do
Authentication → Header authRejects callers missing a static headerDoes not prove the body is untouched
Authentication → JWTChecks a bearer token you issuedDoes not verify Stripe or GitHub HMAC
IP(s) Allowlist403s callers outside the listDoes not survive NAT, shared egress, or a stale list
Raw BodyKeeps the bytes the vendor signedDoes nothing unless the next node uses them
Only Run IfExpression gate after IP + authFail-open if the expression errors — n8n logs a warning and lets the request through
Respond → ImmediatelyReturns Workflow got started fastDoes not verify signatures by itself

If the vendor offers signing, use it. A shared query-param secret is weaker and still better than nothing — rotate it, never log it, never paste it into a ticket.

How do you stop replay attacks after the signature already passed?

A replay is a valid payload, resent later. The signature still matches. That is the point.

Stripe’s mitigation is the signed timestamp. Official libraries default to 5 minutes between t= and now. Widen that only if your host clock is honest and you have a reason. Do not set tolerance to 0. Stripe says that disables the recency check entirely.

GitHub’s mitigation is not in the HMAC. Their best-practices page tells you to treat X-GitHub-Delivery as unique per event, and notes that a redelivery reuses the same delivery id. If you key only on “we have never seen this body,” a redelivery looks new. If you key on the delivery id, a redelivery is a no-op — which is what you want after you already applied the side effect.

LayerStripeGitHub
OriginStripe-Signature v1X-Hub-Signature-256
FreshnessSigned t=, default 5 minutesNone in the signature
Duplicate applyEvent id (evt_…) in your storeX-GitHub-Delivery in your store
Legitimate retryNew signature + new timestamp; same event idSame delivery id on redelivery
Business keydata.object.id + event typeDelivery id, then repo + action if you must

Combine all three:

  1. Signature valid.
  2. Timestamp inside policy (Stripe) or event created not older than your SLA (everyone else).
  3. Idempotency key written before the CRM or payment node.

Stripe live-mode delivery retries for up to three days with exponential backoff. Sandbox retries three times over a few hours. Dashboard resend works for 15 days; the CLI resend window is 30 days. Those numbers are from Stripe’s webhook docs. Your idempotency store has to outlive the retry window or a late retry becomes a second apply.

  • Timestamp / recency check on
  • Event or delivery id stored before side effects
  • Store TTL ≥ vendor retry window
  • Redelivery of the same id is a 200 with no second write
  • Clock synced (NTP) on the n8n host

Signatures prove origin. They do not, by themselves, stop replays inside the validity window.

When do IP allowlists help, and when do they lie?

Allowlists cut anonymous internet noise. They do not replace HMAC.

Stripe tells you to use both IP allowlisting and signature verification. The trap is allowlisting the wrong list. Stripe’s IP page separates api.stripe.com addresses from webhook notification addresses. Webhooks do not come from the API list. Subscribe to Stripe’s API announce list if you pin IPs — they give seven days’ notice before changes.

GitHub tells you the same split. Allow GitHub’s IP addresses, but pull current ranges from GET /meta and use the hooks element, not every GitHub CIDR. They change. About GitHub’s IP addresses is the overview; delivering webhooks to private systems is the reverse-proxy version: HTTPS POST only, from hooks ranges, then still verify the signature on the inner app.

n8n’s Webhook node has IP(s) Allowlist. A caller outside the list gets 403. Blank means everyone. That is the right place for Stripe’s webhook IPs or GitHub’s hooks ranges on self-hosted n8n sitting behind your proxy.

AllowlistUse it forDo not use it for
Stripe webhook IPsInbound POSTs to the payment workflowCalls out to api.stripe.com
GitHub hooks rangesInbound POSTs to the deploy / issue workflowapi.github.com client traffic
Your office / VPN egressPrivate enterprise callbacksSaaS vendors that will not publish stable egress
n8n Header auth + allowlistUnsigned legacy vendors you are retiringStripe or GitHub (they already sign)

Allowlists lie when:

  1. The vendor shares egress with other customers and you treat the IP as a person.
  2. You freeze a copied list and miss a rotation.
  3. A proxy or Cloudflare hop makes n8n see the proxy IP, not Stripe.
  4. You skip HMAC because “the firewall is enough.”

On n8n Cloud you often cannot put a vendor allowlist in front of the public webhook the way you can on a box you own. You still verify signatures. The automation lane is where we decide hosting vs application controls; this post is the application control.

If a vendor truly cannot sign, put a reverse proxy with IP allowlist, a shared header secret, or mTLS in front. Track those endpoints as exceptions with an expiry to renegotiate. Do not let “the old CRM cannot HMAC” become the default for Stripe.

Is a hidden webhook URL enough protection?

No. URLs leak.

They leak in browser history, Slack huddle screenshots, support tickets, HAR files, n8n execution previews, and the one time someone pasted a production path into a public Notion page. GitHub’s own warning is blunt: do not put API keys or secrets in the payload URL. Use a webhook secret instead.

Obscurity is a bonus layer. It is never the only layer.

LayerStopsFails when
Unguessable pathCasual scannersThe URL is copied once
TLSPassive tap on the wireYou accept HTTP in “just staging”
Header / Basic authCallers who only have the URLThe static secret is in the same ticket as the URL
HMAC on raw bodyForged or mutated JSONYou verified a re-serialized object
Replay window + idempotencyCaptured valid POSTsYou stored the key after the write
Least-privilege tokenBlast radius after a valid eventFounder OAuth is the production principal

Treat production webhook URLs as secrets anyway. Separate test and production paths. Disable unused test webhooks. Do not paste full URLs into public tickets.

n8n gives you a random path by default for a reason. Do not “clean it up” to /webhook/stripe on a host that is already in Shodan.

How should n8n store secrets and rotate them?

Store signing secrets and API tokens in n8n credentials or environment variables — not in Code node string literals, not in workflow sticky notes, not in the Webhook path.

Stripe generates a unique secret per endpoint. Test and live secrets differ. If you roll a Stripe secret, Workbench lets you expire the old one immediately or delay expiration for up to 24 hours. During that window Stripe signs with multiple secrets. Accept secret_current and secret_previous; reject only when neither matches. That procedure is in Stripe’s webhook docs under rolling endpoint signing secrets.

GitHub does not give you that dual-sign window in the product UI. If you rotate the GitHub secret, old in-flight deliveries fail verification. Keep the previous secret in the Code node for a short, named window if you must, then delete it.

RuleWhy
One secret per environmentA leaked staging whsec_ must not verify live events
Rotate on a calendar and on staff changes90 days is a common default; incident is immediate
Restrict workflow exportJSON exports are how secrets leave the building in comments
Redact error notificationsSlack “workflow failed” must not echo the header or the secret
Named rotation owner“We should rotate” without a name is how secrets turn five

If a secret may have leaked, rotate first, investigate second.

GitHub’s validation docs also say: never hardcode the token, never push it to a repository. That includes the n8n workflow JSON you commit “for backup.”

What does least privilege look like on the side-effect credentials?

Webhook auth answers “did Stripe or GitHub send this?” It does not answer “should this workflow be allowed to do that?”

Each connected app should use a principal that can only do what the graph needs.

Workflow needBad scopeBetter scope
Create CRM contactsFull adminContacts write + limited read
Send newsletter draftsFull account ownerDraft create only
Read one spreadsheetEdit all DriveSingle file access
Slack notifyWorkspace adminBot to one channel
Mark a GitHub issueOrg ownerContents or issues on one repo
Record a Stripe payment in the ERPStripe secret key with deletesRestricted key: read Events, write the one object you own

Personal founder OAuth for company production systems is a recurring audit finding. Use a service account with a named human owner.

GitHub’s best practices start with a smaller idea that maps here: subscribe to the minimum number of events. A workflow that listens to * will one day receive an action you never designed for. Check X-GitHub-Event and the payload action before you write.

  • Production credential is not a founder’s personal login
  • Scope list written next to the workflow, not “full access so it works”
  • Event allowlist (Stripe types / GitHub events) is explicit
  • Irreversible classes (refunds, deletes, deploys to prod) need a human gate

A valid signature on an event you did not subscribe to is still a no-op.

Why does app-level authorization still matter after a valid Stripe signature?

Even with a valid Stripe signature, your code should not trust a price_id that originated in a client-side form. Webhooks authenticate the provider. Your workflow still enforces business rules.

Classic failure: Checkout fires checkout.session.completed. The workflow reads metadata.sku from the session and fulfills whatever string the browser stuffed in. The signature is valid. The sku is not a price you sell.

Trust thisDo not trust this
Event id + type after HMAC passPrice, SKU, or role fields the browser set
Stripe object retrieved by id with your secret keyClient-supplied amounts
GitHub X-GitHub-Delivery + event + actionA ref you did not allowlist for deploy
Your idempotency store“The payload says this is the first time”

Procedure after auth:

  1. Verify signature + recency.
  2. Confirm event type is on your allowlist.
  3. Load the canonical object from the vendor API when money or access changes.
  4. Compare the result to your catalog, not to the webhook body alone.
  5. Then write.

Stripe’s quickstart frames the same idea as “secure your webhook” so a server acting like Stripe cannot inject events. The follow-on is yours: a server that is Stripe can still describe a purchase you do not offer if you skip the lookup.

What n8n Webhook node settings belong in production?

Settings that belong on every money or CRM webhook:

SettingProduction valueWhy
Webhook URLProduction URL, workflow publishedTest URLs die when you leave the editor
HTTP MethodPOST onlyGET webhooks become cache and crawler bait
AuthenticationHeader/JWT if the vendor can send it; HMAC in Code regardlessDefense in depth, not a substitute
Raw BodyOnSignature bytes
IP(s) AllowlistVendor webhook IPs when you control the edge403 the rest
Ignore BotsOnLink-preview GETs should not start fulfillment
RespondImmediately, then processStripe and GitHub both punish slow 2xx
PathKeep the random tokenGuessable paths are not a control

n8n documents a 16MB webhook payload cap (N8N_PAYLOAD_SIZE_MAX on self-host). That is a DoS knob, not a security control. A 16MB JSON blob that fails HMAC should never reach the CRM node — reject at the Code node.

Test vs production is a security setting. n8n registers the test URL when you click Listen; it registers the production URL when you publish. Mixing them is how a staging secret verifies a live event, or how a leftover test webhook stays public after the project ships.

Cloud vs self-hosted does not change the application checks. Cloud: you still verify signatures and scope tokens; the vendor patches the platform. Self-hosted: you also patch n8n, lock the admin UI, restrict who can create public webhooks, and watch egress. Neither hosting choice is a signature.

Webhook security is wasted if anyone at the company can edit production workflows.

  • Limit who can publish production workflows
  • Separate editor access from credential access when the edition allows it
  • Review sudden changes to auth nodes
  • Disable unused public webhook triggers

Your threat model includes a stolen laptop session, not only anonymous internet POST traffic.

What fails when you skip one of these layers?

The failure we see is not theoretical theater. It is a specific skip.

Unsigned production URL. Someone POSTs { "email": "victim@client.com", "status": "paid" }. The workflow creates the CRM row and fires the “welcome” sequence. Cost: cleanup, plus the conversation where you explain that the form was never the door.

Signature on parsed JSON. Stripe and GitHub start failing “randomly.” You disable verification to “unblock launches.” A week later the URL is in a ticket. You have no door.

HMAC on, no replay store. A captured invoice.paid from 90 seconds ago is still inside Stripe’s 5-minute window. It applies twice. Finance spends the afternoon reconciling. The idempotency post is the fix; this post is why the signature was not enough.

Allowlist only. You copied Stripe’s API IPs. Webhooks start 403ing because they come from the webhook list. Someone “fixes” it by blanking the allowlist and leaving HMAC for later.

Founder OAuth. A valid GitHub push event runs a workflow whose token can delete the org. The event was real. The blast radius was a choice.

SkipSymptomFirst fix
No HMACMystery CRM rowsVerify raw body, reject mismatch
HMAC on parsed JSONIntermittent 400s from the vendorEnable Raw Body
No timestamp / delivery idDuplicates that “shouldn’t be possible”Replay window + idempotency
Wrong IP listVendor deliveries 403Use webhook / hooks ranges
Secret in the pathLeak in one screenshotRotate, then move the secret
Admin-wide tokenOne event, outsized damageRe-issue a scoped principal

If you only have time for one improvement this week, turn on raw-body HMAC and a duplicate test in staging. That loop beats another connector.

How do you test a webhook before it sees live money?

Fifteen minutes in staging prevents a public incident.

Use the vendor’s tools, not a hand-built JSON file you signed with the wrong secret.

CheckExpectTool
POST with no signatureRejectcurl / n8n test URL
POST with a garbage headerRejectSame
POST with an old Stripe t=RejectMutate t= without a matching v1 — must fail
POST with GitHub test vectorPassSecret / payload pair from GitHub’s docs
Same valid event twiceOne business applyYour idempotency store
Stripe CLI vs Dashboard secretFail if crossedWebhook quickstart
Error pathNo secret in Slack / emailTrigger a deliberate fail

GitHub documents the exact test vector. If your implementation cannot produce sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17 for It's a Secret to Everybody + Hello, World!, you are not ready for a real repo.

Stripe’s CLI (stripe listen) is the right local loop. Do not verify those events with the Dashboard endpoint secret.

  • Unsigned reject
  • Bad signature reject
  • Stale timestamp reject (Stripe)
  • Duplicate delivery id is a no-op
  • Logs show execution id, event id, verify result — not the secret, not a full PII dump

If any item fails, keep the production trigger disabled.

What do you do when a signing secret leaks?

Speed beats a perfect timeline.

  1. Roll the secret at the vendor. Stripe: Workbench → endpoint → Roll secret. GitHub: edit the webhook secret.
  2. Update n8n credentials / env. For Stripe, dual-accept during the up-to-24-hour overlap if you delayed expiry.
  3. Invalidate the old secret as soon as both sides agree.
  4. Review execution history for volume spikes, unknown event types, or side effects you did not schedule.
  5. Quarantine suspicious CRM / finance rows.
  6. Write the postmortem even if nothing applied.
SymptomLikely causeMove
Valid signatures you did not expectSecret or URL leakRotate, then audit applies
Sudden verify failuresWrong env secret, or a roll you did not deployCheck test vs live vs CLI
403s from the vendorAllowlist driftRefresh Stripe webhook IPs or GitHub hooks
Duplicates after a leak responseRetries during the incidentIdempotency store, not a second rotate

If one n8n hosts multiple clients: separate credentials, separate webhook paths, never let client A payloads write with client B tokens. Shared automation infrastructure without that split is a mapping bug away from a breach.

What belongs on the production checklist?

Print this next to the workflow. If any line is unchecked, the trigger stays off.

  • HMAC verified on the raw body (Stripe v1 or GitHub SHA-256)
  • Constant-time compare
  • Timestamp / replay window enforced where the vendor provides one
  • Event or delivery id stored before side effects
  • Secrets in credential store, not source text
  • Test and live secrets are different objects
  • Least-privilege tokens on every side-effect node
  • Event-type allowlist
  • IP allowlist uses webhook / hooks ranges, and someone owns the refresh
  • Fast 2xx (Stripe: before heavy work; GitHub: under 10 seconds)
  • Error alerts without secret or full-PII leakage
  • Rotation owner named
  • Unused test webhooks disabled
  • Admin publish rights limited

Annual pass, or after any incident: rotate signing secrets, re-check scopes, remove unused public webhooks, re-run the staging checks, confirm redaction still holds.

Security is a calendar item, not a one-time setup screen.

Unsigned webhooks turn your CRM into a public write API. Treat them accordingly.

FAQ

How do I secure n8n webhooks?

Verify the provider HMAC on the raw body, keep secrets in credentials, separate test and live endpoints, allowlist the vendor’s webhook IPs when you control the edge, and use least-privilege tokens on side effects. Only then run business logic behind an idempotency key and a schema check. n8n Header auth is an extra gate, not a replacement for Stripe or GitHub signatures.

What is webhook signature verification?

It is a cryptographic check that the payload was sent by the vendor who holds the shared secret. Stripe puts an HMAC in Stripe-Signature; GitHub puts one in X-Hub-Signature-256. If the signature does not match the raw bytes, reject the request before any CRM or payment node runs.

Is a hidden URL enough protection?

No. URLs leak from tickets, screenshots, HAR files, and execution logs. Treat an unguessable path as a bonus layer. GitHub’s docs say not to put secrets in the payload URL — use a webhook secret and verify it.

Should webhooks be behind a VPN?

Sometimes, for private enterprise callbacks where both sides sit on the same network. Most SaaS vendors need a public HTTPS endpoint, so TLS, HMAC, replay controls, and least privilege do the work. IP allowlists help when the vendor publishes stable webhook egress, as Stripe and GitHub do.

What do I log?

Log execution id, event or delivery id, verification result, and the business identifiers you need to reconcile. Do not log signing secrets, OAuth tokens, or full payloads that are all PII. Auth failures can be counted without storing attacker bodies forever.

How does this relate to dead-letter queues?

Auth failures are noise and probes — count them, alert on spikes, and drop the body. Business-logic failures that happen after a valid signature belong in a parked-failure path with enough context to replay safely, minus secrets. The door and the recovery lane are different jobs.

CTA

If your webhook trusts anyone who can POST JSON, fix that before you add another integration.

Harden the door, then build the path. Read the Production n8n handbook, and use the automation lane or book an automation audit for a production security pass on the workflows that already move money.

FAQ

What questions does this article answer?

How do I secure n8n webhooks?
Verify the provider HMAC on the raw body, keep secrets in credentials, separate test and live endpoints, allowlist the vendor's webhook IPs when you control the edge, and use least-privilege tokens on side effects. Only then run business logic behind an idempotency key and a schema check. n8n Header auth is an extra gate, not a replacement for Stripe or GitHub signatures.
What is webhook signature verification?
It is a cryptographic check that the payload was sent by the vendor who holds the shared secret. Stripe puts an HMAC in `Stripe-Signature`; GitHub puts one in `X-Hub-Signature-256`. If the signature does not match the raw bytes, reject the request before any CRM or payment node runs.
Is a hidden URL enough protection?
No. URLs leak from tickets, screenshots, HAR files, and execution logs. Treat an unguessable path as a bonus layer. GitHub's docs say not to put secrets in the payload URL — use a webhook secret and verify it.
Should webhooks be behind a VPN?
Sometimes, for private enterprise callbacks where both sides sit on the same network. Most SaaS vendors need a public HTTPS endpoint, so TLS, HMAC, replay controls, and least privilege do the work. IP allowlists help when the vendor publishes stable webhook egress, as Stripe and GitHub do.
What do I log?
Log execution id, event or delivery id, verification result, and the business identifiers you need to reconcile. Do not log signing secrets, OAuth tokens, or full payloads that are all PII. Auth failures can be counted without storing attacker bodies forever.
How does this relate to dead-letter queues?
Auth failures are noise and probes — count them, alert on spikes, and drop the body. Business-logic failures that happen **after** a valid signature belong in a parked-failure path with enough context to replay safely, minus secrets. The door and the recovery lane are different jobs.
Sources

Last reviewed

More from this lane

Automation

All →
Book the audit