A public webhook endpoint is an unauthenticated HTTP route that can trigger business logic: fulfil an order, grant an entitlement, update a customer record, open a ticket. Anyone who can find the URL can send it a request. The only thing separating a genuine event from a forged one is whether your receiver can prove who sent the request, whether it is fresh, and whether it has already been handled.
That is why webhook signature verification belongs on a CISO's review list, not only on an integration engineer's backlog. Most teams add a signature check because the vendor's quick-start says to. Far fewer can answer the harder questions. Which tenant does this event belong to, and how do we know? What happens when we acknowledge an event and then crash? Who owns the raw payloads and event identifiers we are now storing? Can we rotate the signing secret without dropping events?
This guide is written for CISOs, heads of security engineering and the VPs of Engineering who share the risk. It proposes a seven-gate control set (a design we are suggesting here, not an industry standard) that you can use to approve or block a webhook receiver before it handles consequential events. Every gate is a go/no-go check. There is no summed score, and passing the gates does not certify a system as production-ready; it only means these specific failure paths are closed.
- Who it is for: security and engineering leaders accountable for inbound integrations.
- What it covers: authenticity, freshness, tenant binding, verified acknowledgment, duplicate and conflict handling, terminal failure, key lifecycle and privacy ownership.
- What it does not cover: the vendor's own sending infrastructure, or the business logic of any specific event type.
Key Takeaways
- A signature proves origin and integrity, not authorization. It tells you the holder of a secret sent these exact bytes. It does not tell you the action is permitted for a given tenant.
- Verify the exact raw bytes. Parsing or re-serializing JSON before verification is a common cause of failed or disabled checks.
- Derive the tenant from the verified endpoint, never from an unverified payload field.
- Acknowledge only after durable recording. Return 2xx after the signature passes and the event, its payload hash and its work item are committed atomically.
- A replay window is not a deduplication horizon. A timestamp tolerance of minutes does not stop a legitimate retry or a late replay from repeating a consequential action.
- Unknown is not success and not failure. If a downstream call times out, reconcile authoritative state before retrying.
- Identifiers and raw bodies can be sensitive. Assign a named owner for retention, access and deletion.
- All seven gates are required. Treat any single failure as a no-go, not as a point deduction.
Why Inbound Webhooks Are an Authentication Problem Before They Are an Integration Problem
Key takeaway: A webhook is an inbound request from an unknown source. Signature verification answers one question (did the secret holder send these bytes?) and leaves several other questions open.
The Standard Webhooks specification puts it plainly: webhooks are HTTP requests from an unknown source, so verifying their authenticity is a requirement of any secure implementation. Stripe's webhook documentation makes the same point from the receiver's side: without verification, an attacker could send fake events to trigger actions such as fulfilling orders, granting account access or modifying records.
The risk is easy to underrate because the integration usually works in testing. The happy path never contains an attacker, a replayed request, a rotated secret or a tenant boundary. Security review tends to arrive late, after the endpoint is already wired to something valuable.
What an exposed webhook endpoint faces
| Threat | What it looks like | Potential impact | Primary control |
|---|---|---|---|
| Forged event | Attacker posts a plausible JSON body to the URL | Unauthorized fulfilment, entitlement or record change | Signature over the raw body, verified before any parsing |
| Replay | A captured valid request is re-sent later | Duplicate side effects | Signed timestamp with a non-zero tolerance, plus durable event-ID deduplication |
| Payload tampering | Body altered in transit or by an intermediary | Wrong amounts, wrong identifiers | Signature covers the exact bytes; reject any mismatch |
| Cross-tenant confusion | Payload claims a tenant the sender is not bound to | One customer's event mutates another's data | Tenant bound to the verified endpoint, not to payload fields |
| Secret leakage | Signing secret in logs, repos or shared across environments | Attacker can mint valid signatures | Per-endpoint secrets, rotation with overlap, no secrets in logs |
| Data exposure | Raw payloads and identifiers in observability tools | Privacy and breach exposure | Named data owner, retention limits, redaction |
What a valid signature does and does not prove
| A valid signature proves | A valid signature does not prove |
|---|---|
| The sender held the signing secret (symmetric) or private key (asymmetric) | That the sender is permitted to affect this tenant or this record |
| The signed bytes were not modified after signing | That the event is new, unique or in the right order |
| The signed timestamp, if the scheme signs one, was the sender's claim at signing time | That the payload is semantically valid or safe to apply |
| Nothing about your downstream systems | That your own receiver completed the work exactly once |
Scope matters here. HMAC-style webhook signatures authenticate an HTTP delivery from a specific producer to a specific endpoint. They are not an authorization system, not a substitute for tenant isolation, and not a guarantee about what the event means. Teams that treat "signature passed" as "action approved" are the ones who end up in incident reviews.
Common Failure Patterns: Why This Fails in the Real World
Key takeaway: Intelligent teams fail here because ownership is split between vendor libraries, platform engineering and security, and each assumes the others covered the gap.
Most organizations approach webhook security the same way: copy the vendor's verification snippet, test it once, and move on. That works until something changes. The scenarios below are illustrative composites of patterns commonly seen in integration reviews, not reports about specific clients or measured results.
Failure pattern 1: the body is parsed before it is verified
A team adds a global JSON middleware to its web framework. Verification then runs against a re-serialized body, and signatures start failing. Under delivery pressure someone adds a "skip verification" flag for staging. The flag is later copied into a production configuration template. Nobody decided to ship an unauthenticated endpoint; a convenience setting did it for them.
Why intelligent teams miss it: the error looks like a vendor bug. Stripe's troubleshooting guidance notes that frameworks which parse or mutate the body before your code sees it, by reordering keys, stripping whitespace or changing encoding, cause verification to fail. The Standard Webhooks specification calls the same thing a very common failure mode. The fix is architectural (capture raw bytes first), but the quick fix is to turn the check off.
Failure pattern 2: the tenant comes from the payload
A multi-tenant platform receives events from one shared vendor account. The receiver verifies a single shared secret, then reads account_id from the body to decide which tenant to update. A forged or misrouted event that still carries a valid signature, for example one generated by a compromised or over-privileged integration, now writes to whichever tenant the body names.
Why intelligent teams miss it: the signature check passed, so the data feels trusted. The control that was missing was binding the tenant to the endpoint or credential, then rejecting any payload that disagrees.
Failure pattern 3: verified, replayed, and applied twice
The receiver checks the timestamp tolerance and deduplicates event IDs in a short-lived cache. A vendor retry arrives after the cache entry has expired. Retries are generated with a fresh signature and timestamp (Stripe documents this behavior), so the request is perfectly valid and the cache no longer remembers the event. A credit is issued twice.
Why intelligent teams miss it: the standard guidance mentions saving event IDs briefly, and the replay window and the deduplication horizon get conflated. They are different controls with different lifetimes. The related engineering detail is covered in our guide to idempotent webhooks and API retries.
Failure pattern 4: secrets that can never be rotated
One signing secret is shared across staging, production and a partner sandbox. Rotating it would break all three at once, so nobody does. When an engineer leaves, the secret is still the same one that sits in an old CI log.
Why intelligent teams miss it: rotation is an operational event with no natural owner. Without an overlap period, rotation looks like an outage.
Failure pattern 5: observability becomes the data leak
To debug failed deliveries, the platform team logs full headers and bodies, including signature headers, event IDs, customer identifiers and sometimes personal data, to a log tool with broad read access and long retention.
Why intelligent teams miss it: the logging decision is made for reliability, and the privacy review happens (if at all) at design time rather than when the log fields change. See our guidance on privacy governance for integration event logs.
The common thread
None of these is a cryptography failure. They are gaps between systems and owners: framework middleware versus verification, a vendor library versus your tenant model, a cache versus a database, a platform team versus a privacy owner. A control set has to name the gap and the owner, not just the algorithm.
A Proposed Seven-Gate Go/No-Go Control Set for Webhook Receivers
Key takeaway: Use seven all-required gates. If any one fails, the receiver is a no-go for consequential events. Do not average the gates into a score.
The following control set is a proposed design offered for your review process. It is not a published standard, not a certification, and not a method we claim any particular client already runs. We built it by combining documented vendor guidance with the failure patterns above, so you can turn them into evidence requests.
Why checklist gates instead of a score
A weighted score invites trade-offs: strong cryptography can "offset" missing tenant binding in the arithmetic, even though an attacker does not care about your average. Mandatory gates reflect how these failures actually work. Each one closes a different path, and none substitutes for another.
| Gate | Required control | Evidence to request | No-go if |
|---|---|---|---|
| G1. Authentic origin | Signature computed over the exact raw bytes, compared in constant time, using a secret specific to this endpoint | Code path showing raw-body capture before parsing; negative test with a modified body | Verification can be disabled by configuration in production, or runs on re-serialized JSON |
| G2. Freshness | Signed timestamp checked against a non-zero tolerance; clock synchronization monitored | Configuration value, NTP policy, test with stale and future timestamps | Tolerance of zero (check disabled), or the timestamp is not covered by the signature |
| G3. Tenant binding | Tenant resolved from the verified endpoint or credential; any tenant claim in the body must match | Mapping table, test sending tenant A's valid event body with tenant B's claim | Tenant is chosen from an unverified payload field |
| G4. Verified acknowledgment | 2xx returned only after verification passes and the event record and work item are committed atomically | Transaction boundaries; crash test between receipt and processing | Acknowledgment can occur before durable recording, or recording and scheduling can diverge |
| G5. Duplicates and conflicts | Durable tenant-scoped uniqueness on source and event ID, bound to a payload hash; same ID with different payload is rejected and alerted | Unique constraint, conflict handling test, retention horizon longer than the vendor's retry horizon | Deduplication is cache-only, global across tenants, or silently accepts a changed payload |
| G6. Terminal failure and uncertain input | Defined handling for unverifiable, malformed, unknown-type and unknown-outcome events, with reconciliation before retry | Response policy table, quarantine process, unknown-outcome runbook | Unverified or unknown input is queued for blind retry, or a timeout is treated as success or failure |
| G7. Key lifecycle and privacy ownership | Secret rotation with overlap, no secrets or signatures in logs, named owner for payload and identifier retention | Rotation runbook and last test, log field inventory, named owner and retention schedule | No rotation path, shared secret across environments or tenants, or no accountable data owner |
Reading the gates correctly
- G1 and G2 come from the signature scheme itself. The Standard Webhooks design signs the message ID, timestamp and body together, so tampering with any of the three invalidates the signature. Stripe's signed payload also includes a timestamp. Some other providers sign only the body, in which case replay defense must come entirely from G5. Check each vendor's documentation rather than assuming.
- G3 is yours to build. No vendor can know your tenant model.
- G4 to G6 are the difference between "secure" and "safe to operate". They decide what happens when things go wrong between your edge and your database.
- G7 keeps the controls from decaying over time.
Practical Implications by Role: Who Owns Which Gate
Key takeaway: Every gate needs a named owner and an evidence artifact. "The integration team" is not an owner.
Gates fail most often at handoffs. The ownership table below is a starting point to adapt, not a prescribed org chart.
| Role | Primary gates | What this role should ask for |
|---|---|---|
| CISO or head of security engineering | G1, G2, G7 | Proof that verification cannot be disabled in production; secret inventory; last rotation drill result |
| VP Engineering or platform lead | G3, G4, G5, G6 | Atomicity evidence; crash and duplicate tests; reconciliation runbook for unknown outcomes |
| Privacy officer or data owner | G7 | Inventory of stored payload fields and identifiers; retention and deletion path; access list |
| Product owner of the affected workflow | G5, G6 | Definition of which later events are legitimately new (for example a second refund on the same order) versus duplicates |
| Procurement and vendor management | Upstream of all gates | Vendor's documented signature scheme, retry policy and secret rotation support |
When you consume vendor webhooks
Ask each vendor for primary documentation, not marketing claims: what is signed, which header carries the signature, whether the timestamp is covered, how retries are signed, how long retries continue, and whether the signing secret supports overlapping rotation. Stripe, for instance, documents rolling an endpoint secret with the old one remaining active for up to 24 hours, during which it generates one signature per active secret. That is a documented vendor capability for that product; it says nothing about other vendors.
Separate three kinds of claims in your vendor review: what the vendor states in documentation, what the product provides as a compliance-support feature, and what your own legal or regulatory obligations require. A vendor's security attestation does not discharge your duties as the party storing the payloads. Our guide to validating a vendor's security certifications explains how to check scope and evidence, and the companion piece on vendor webhook and API delivery guarantees covers the sending side.
When you produce webhooks for your customers
The same gates apply in mirror image. Per-customer signing secrets, signed message IDs and timestamps, a documented rotation overlap, and a published retry policy are what your customers' security teams will ask for. The Standard Webhooks specification recommends unique signing keys per endpoint and warns that reusing keys across customers can create security issues.
An Illustrative Reference Design: From Verified Request to Safe Mutation
Key takeaway: Make the inbox row the work item. One atomic insert gives you deduplication, durable recording and scheduling together, so you never acknowledge an event you cannot recover.
The code below is illustrative pseudocode in Python and SQL, not an official API contract for any vendor or library. It follows the Standard Webhooks signing scheme (message ID, timestamp and body concatenated with full stops, HMAC-SHA256, versioned signatures) only to make the gates concrete. Prefer the vendor's official verification library in production, and confirm secret decoding and header formats against the vendor's current documentation.
Step 1: a tenant-scoped inbox that doubles as the work queue
CREATE TABLE webhook_inbox ( tenant_id TEXT NOT NULL, source TEXT NOT NULL, event_id TEXT NOT NULL, payload_hash TEXT NOT NULL, body_ref TEXT NOT NULL, -- pointer to an encrypted body store with a retention policy status TEXT NOT NULL, -- received | claimed | applied | unknown | quarantined | failed_terminal claimed_by TEXT, lease_expires TIMESTAMPTZ, received_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (tenant_id, source, event_id) );The primary key is the durable, tenant-scoped operation identity. The payload hash binds that identity to the approved bytes, so the same event ID arriving with different content is detectable.
Step 2: verify, then record atomically, then acknowledge
def receive(request, endpoint_token): endpoint = registry.find(endpoint_token) # opaque token -> tenant, source, active secrets if endpoint is None: return respond(404) raw = request.raw_body # bytes exactly as received, before any parsing msg_id = request.headers.get('webhook-id') ts = request.headers.get('webhook-timestamp') sigs = request.headers.get('webhook-signature') if not (msg_id and ts and sigs) or not ts.isdigit(): return reject(endpoint, 'malformed', 400) if abs(now_epoch() - int(ts)) > endpoint.tolerance_seconds: # never zero return reject(endpoint, 'stale_or_future', 400) signed = msg_id.encode() + b'.' + ts.encode() + b'.' + raw verified = False for secret in endpoint.active_secrets: # current secret plus overlap secret during rotation expected = base64.b64encode(hmac.new(secret, signed, hashlib.sha256).digest()).decode() for part in sigs.split(' '): version, _, value = part.partition(',') if version == 'v1' and hmac.compare_digest(value, expected): verified = True if not verified: return reject(endpoint, 'bad_signature', 401) digest = hashlib.sha256(raw).hexdigest() body_ref = vault.put_encrypted(endpoint.tenant_id, raw) try: with db.transaction(): inserted = db.insert_inbox_if_absent( # INSERT ... ON CONFLICT DO NOTHING endpoint.tenant_id, endpoint.source, msg_id, digest, body_ref) if not inserted: prior = db.get_hash(endpoint.tenant_id, endpoint.source, msg_id) vault.delete(body_ref) if prior != digest: quarantine_conflict(endpoint, msg_id) return respond(409) # same ID, different bytes: never accept silently return respond(200) # identical duplicate, already durably recorded except DatabaseUnavailable: vault.delete(body_ref) return respond(503) # not recorded, so do not acknowledge return respond(200) # acknowledged only after the commitNotice what is absent: the tenant never comes from the body, nothing is parsed before the signature is checked, and nothing is acknowledged before the commit. Sweep for orphaned stored bodies as part of normal housekeeping, since the body store write sits outside the database transaction.
Step 3: claim work atomically and mutate conditionally
-- Claim one event; concurrent workers cannot receive the same row UPDATE webhook_inbox SET status = 'claimed', claimed_by = :worker, lease_expires = now() + interval '2 minutes' WHERE (tenant_id, source, event_id) = ( SELECT tenant_id, source, event_id FROM webhook_inbox WHERE status = 'received' ORDER BY received_at FOR UPDATE SKIP LOCKED LIMIT 1) RETURNING ;def process_one(worker_id): row = db.claim_next(worker_id) if row is None: return event = vault.get(row.body_ref) try: # Target-side conditional mutation, for example: # UPDATE orders SET state = 'paid' WHERE id = :id AND state = 'pending' result = target.apply_conditionally(row.tenant_id, event) except TimeoutError: db.mark(row, 'unknown') # completion could not be confirmed reconcile.enqueue(row) # read authoritative state before any retry return except PermanentRejection as err: db.mark(row, 'failed_terminal', reason=err.code) return db.mark(row, 'applied')Two details deserve emphasis. First, a read-then-write sequence in application code is not compare-and-swap. The condition must be enforced by the target system itself (a conditional update, a version check, or a documented idempotency key on the target API). If the target cannot do that, document the serialization or conflict limitation and treat the workflow as higher risk. Second, an expired lease on a claimed row means the outcome is unknown. Move it to reconciliation, not back to the front of the queue.
The response policy, in one table
| Condition | Response | What you record | Sender may retry? |
|---|---|---|---|
| Unknown endpoint token | 404 | Metric only | Vendor-specific; do not rely on it |
| Malformed headers or stale timestamp | 400 | Rejection counter with reason, no body | Often yes; fresh retries get fresh timestamps |
| Bad signature | 401 | Rejection counter, endpoint ID, no body or secrets | Treat as terminal on your side; alert on spikes |
| Verified, identical duplicate | 200 | None beyond the existing record | No |
| Verified, same ID but different payload hash | 409 | Conflict incident for security review | Investigate before accepting any retry |
| Verified, recorded, type unknown or schema invalid | 200 | Status quarantined for human triage | No; avoids retry storms |
| Verified but cannot persist | 503 | Alert | Yes, by design |
Be careful with the "sender may retry" column. Vendors differ in how they treat non-2xx responses, so terminal on your side does not mean terminal on theirs. Stripe, for example, retries events when an endpoint previously returned a non-2xx status. Plan for repeated rejected deliveries and make sure they cannot exhaust your resources.
For unknown-outcome handling at scale, see our framework for reconciling unknown-outcome operations, and for events that cannot be applied, our guide to dead letter queues, triage and replay.
Distinguishing a legitimate later operation from a duplicate
Do not collapse everything that looks similar into one key. A second refund on the same order is a new operation with a new event ID. A vendor retry of the first refund carries the same event ID and the same payload hash. Define this distinction with the workflow's product owner, because the receiver cannot infer business intent from the bytes.
Risks, Constraints and Trade-Offs Across Verification Approaches
Key takeaway: Each mechanism protects a specific layer. Combine them for defense in depth, and do not let one stand in for another.
Technical standards have a scope. A signature scheme, a transport-layer mechanism and a network allowlist each answer different questions. The table below summarizes what each can and cannot do.
| Mechanism | Good for | Not sufficient for | Trade-off |
|---|---|---|---|
| Symmetric HMAC signature (for example HMAC-SHA256, see RFC 2104) | Origin and integrity of a webhook body between two parties who share a secret | Non-repudiation; protecting a secret that either side leaks | Simple and fast, but both sides must protect the key. Standard Webhooks advises asymmetric signatures if you do not control the security of both producer and consumer |
| Asymmetric signature (for example Ed25519 as defined in Standard Webhooks) | Consumers verify with a public key; only the producer holds the private key | Freshness and deduplication | More CPU and key-distribution work; consumers need a trust list of keys, and must not read the public key from the request itself |
| HTTP Message Signatures (RFC 9421) | A general IETF standard for signing selected HTTP message components | Anything unless your producer actually implements it | Vendor adoption varies; do not assume your webhook provider uses it |
| Mutual TLS | Authenticating the transport peer | Message-level integrity once TLS is terminated at a gateway or proxy, or proving the business event is authentic | Certificate lifecycle overhead; many senders do not support client certificates |
| IP allowlisting | Reducing noise and attack surface | Authenticity on its own, since addresses change and shared infrastructure exists | Brittle if the vendor's ranges change; Stripe recommends it in addition to signature verification, not instead |
Trade-offs to decide explicitly
- Timestamp tolerance versus clock skew. A tight window limits replay; a loose one tolerates drift. Stripe's libraries default to five minutes and warn that a tolerance of zero disables the recency check entirely. Pick a value, monitor clock synchronization, and record the decision.
- Acknowledge fast versus acknowledge safely. Persisting before responding adds latency. That latency is usually small compared with the cost of a lost event, but measure it against the vendor's timeout.
- Keep raw payloads versus minimize. Raw bodies make reconciliation and forensics easier, and they also expand your privacy footprint. Store encrypted, reference by pointer, and set retention to the shortest period that still covers the vendor's retry and your reconciliation horizon.
- Rotation overlap versus exposure. An overlap period prevents dropped events, and it also keeps the old secret valid longer. After suspected compromise, shorten or skip the overlap and accept the reconciliation work.
- Strict conflict rejection versus operational noise. Rejecting a same-ID, different-payload event is correct, and it will occasionally surface a vendor bug. That is a feature, as long as someone owns the alert.
Identifiers are data too
Event IDs, account IDs, customer references, signature headers and endpoint tokens can each identify a person, a tenant or a credential. Treat them as data you are responsible for. Do not put signing secrets or full signature headers in logs. Keep opaque endpoint tokens out of shared dashboards. Apply the same retention and access discipline to the inbox table that you apply to the systems it feeds. The OWASP API Security project is a useful baseline for the wider API risk categories this sits within.
A Lower-Risk Rollout Path, Plus What Stays Constant as Standards Evolve
Key takeaway: Retrofit gates in order of blast radius: stop forgery first, stop duplicates second, then make recovery and privacy routine. Exit each phase on evidence, not on elapsed time.
You rarely get to rebuild every receiver at once. A phased path keeps risk falling at each step without promising dates that depend on your estate.
| Phase | Activities | Exit evidence |
|---|---|---|
| 1. Inventory | List every inbound webhook endpoint, its vendor, what it can change, its secret location and its owner | Complete register; endpoints ranked by what they can mutate |
| 2. Close forgery | Enforce G1 and G2 on every endpoint; remove or lock any verification bypass flags; separate secrets per endpoint and environment | Negative tests (bad signature, modified body, stale timestamp) pass in CI |
| 3. Bind and record | Implement G3, G4 and G5 on endpoints that mutate consequential records first | Crash test between receipt and processing loses no events; conflict test rejects changed payloads |
| 4. Handle uncertainty | Implement G6: quarantine paths, unknown-outcome reconciliation, alerting on rejection spikes | Drill demonstrating that an induced timeout is reconciled, not blindly retried |
| 5. Operate and prove | Implement G7: rotation drill, log field inventory, named data owner, retention schedule, periodic independent test | Rotation completed without event loss; log review completed; penetration test scope includes webhook endpoints |
Independent testing is worth scheduling early. Webhook receivers are easy to omit from a penetration test scope because they look like plumbing. If you want an outside view, CIS offers application penetration testing, and the broader design work sits within API security and threat protection and enterprise integration and APIs. For teams embedding these checks in pipelines, DevSecOps and secure engineering is the relevant discipline.
Standards update and the evergreen principle
As of late 2026, the landscape is mixed. Vendor-specific schemes such as Stripe's timestamped HMAC header sit alongside community specifications such as Standard Webhooks and the general IETF HTTP Message Signatures standard. None of these is universal, and GitHub and other platforms document their own HMAC header conventions (see GitHub's guide to validating webhook deliveries). Expect more convergence, but do not plan around it.
The principle outlasts any specific scheme: authenticate the exact bytes, bind the result to a tenant you determined yourself, commit before you acknowledge, deduplicate durably against a payload hash, treat unknown as unknown, and give every stored identifier an owner. If a future vendor adopts a different signature format, only G1 changes. The other six gates still apply.
Do you know which of your webhook endpoints can still be forged?
Most estates have at least one receiver whose verification was disabled "temporarily." Find it before someone else does.
Ask CIS engineers to review your inbound webhook endpoints against all seven gates.
Request Free ConsultationConclusion: Five Actions Before You Approve the Next Webhook Receiver
Webhook signature verification is necessary, and it is the smallest part of the job. The decision in front of a security leader is whether a receiver is safe to attach to consequential actions. These steps turn that decision into evidence.
- Build the register. List every inbound endpoint, what it can change, who owns it and where its secret lives. Rank by blast radius.
- Run the three negative tests on every endpoint. Modified body, stale timestamp, and a valid body presented under another tenant's identity. Any pass on a forged case is a no-go.
- Demand a crash test and a conflict test for consequential workflows. Prove no event is lost after acknowledgment and that the same event ID with a different payload is rejected.
- Write down the unknown-outcome rule. A timeout means completion is unconfirmed. Require reconciliation of authoritative state before any retry of a consequential mutation.
- Name the data owner and rotate a secret. Assign retention and access responsibility for payloads and identifiers, then complete one rotation drill without event loss.
Passing these checks does not certify an integration as production-ready. It confirms that the specific failure paths in this guide are closed, and it gives you a record to show auditors and your board.
About the authors and review
Cyber Infrastructure (CIS) is a software development and IT services company founded in 2003, with more than 1,000 in-house professionals serving clients in the USA, EMEA and Australia. CIS lists CMMI Level 5 appraisal, ISO 27001 and ISO 9001 certification, and SOC 2-aligned practices among its process maturity credentials; as with any vendor, confirm the scope and validity of current certificates directly when evaluating a partner. This article was reviewed by the CIS Expert Team.
Frequently Asked Questions
Is webhook signature verification enough to secure an endpoint?
No. Signature verification confirms that the secret holder sent the exact bytes you received. You still need freshness checks, tenant binding derived from the verified endpoint, durable deduplication bound to a payload hash, safe handling of unknown outcomes, secret rotation and privacy ownership of stored data.
Should we use HMAC or asymmetric signatures for webhooks?
It depends on who controls both ends. HMAC is simple and fast, but both parties must protect a shared secret. The Standard Webhooks specification advises asymmetric signatures when you do not control the security of both producer and consumer. In practice, you often must use whatever scheme the vendor supports, so verify it in their primary documentation.
How long should the timestamp tolerance be?
- Choose a small, non-zero window that tolerates normal clock drift and network delay.
- Stripe's libraries default to five minutes, and Stripe warns that a tolerance of zero disables the recency check.
- Keep servers synchronized with NTP and monitor drift.
- Remember that the timestamp tolerance limits replay of a single request. It does not replace durable event-ID deduplication, because retries are sent with new timestamps.
Do IP allowlists replace signature verification?
No. Allowlists reduce exposure but cannot establish that a request is authentic or unmodified. Stripe's documentation recommends using both IP allowlisting and signature verification. Allowlists also need maintenance when vendor address ranges change.
What should the receiver return when a signature fails?
Return a client error such as 401 or 400, record only a rejection counter with a reason, and never store the body or log secrets. Treat the event as terminal on your side. Be aware that some senders retry non-2xx responses, so alert on spikes and make sure repeated rejected deliveries cannot exhaust resources.
How should we rotate webhook signing secrets without dropping events?
Support an overlap period in which both the old and new secrets are accepted. Stripe documents rolling a secret with the old one active for up to 24 hours, and the Standard Webhooks specification supports multiple signatures in one header to enable zero-downtime rotation. After suspected compromise, shorten the overlap and reconcile any gaps. Test rotation in a drill before you need it.
Are event IDs and webhook payloads sensitive data?
They can be. Event IDs, customer or account identifiers, endpoint tokens and payload fields may identify people, tenants or credentials. Assign a named owner, limit access, encrypt stored bodies, set retention to the shortest workable period and keep signing secrets and full signature headers out of logs. Your own legal or regulatory obligations determine specific retention and deletion requirements.
What should happen if a downstream call times out after a verified event?
Mark the event as unknown, meaning completion could not be confirmed. Do not treat it as success or as verified failure. Read the authoritative state of the target system first, then decide whether to apply, skip or escalate. Blind retries of consequential mutations can duplicate their effects.
An unverified webhook is an open door to your business logic.
CIS engineers design, test and operate secure integrations for mid-market and enterprise teams across the USA, EMEA and Australia, from receiver architecture to independent security testing.
Get a practical gap review of your inbound integrations, with named owners and evidence requests for every gate.
Request Free ConsultationCyber-security-services
This article is most relevant for technology and digital-transformation leaders who need to roll out a practical technology solution. Use the related CISIN path to compare delivery options, implementation fit, risk, and practical next steps.
Reviewed for technology and business decision makers
This guide is reviewed for clarity, technical and operational relevance, service alignment, and a useful next step.
Validate legal, security, data, budget, and operational requirements with the relevant stakeholders before rollout.

