Docs/Verifying

Failure reasons

Every reason verify() can return, in the order the checks run, and what usually causes each one.

A failed verification resolves to { ok: false, reason }. It never throws, so a bad request can't crash your handler. Checks run in this order and stop at the first failure:

headers→id→timestamp→body→signature→JSON

The body is only read once the headers pass, so a request without valid headers is rejected without buffering its body.

Reasons

ReasonWhenUsual cause
missing_headersNo webhook-id, webhook-timestamp or webhook-signature (or svix-*)Not a webhook, or a proxy stripped the headers
invalid_idThe id contains a dot, which the spec forbidsA malformed or forged request
invalid_timestampThe timestamp is not an integer of secondsA date string or a decimal instead of Unix seconds
timestamp_out_of_toleranceMore than toleranceSec (300 by default) from now, either wayA replayed request, a clock that's off, or milliseconds
unreadable_bodyThe Request body was already read, or the stream failedAnother middleware read it first. Pass req.clone()
invalid_secretThe secret could not be imported as an HMAC keyRare: createVerifier() already rejects bad secrets
invalid_signatureNone of the v1, signatures matchThe wrong secret, or a body that was parsed and re-serialized
invalid_jsonThe signature is valid but the body is not JSONThe sender signed something that isn't JSON

unreadable_body only comes from verify(). verifyPayload() gets the body from you.

Signatures

The webhook-signature header can hold several space-separated signatures, like v1,abc… v1,def…, which senders use while rotating keys. The verifier tries up to 8 v1 entries and accepts the request if any of them matches. Entries with another version prefix are skipped.

Tolerance

The timestamp check is what stops replays: a captured request can't be sent again once it's older than the tolerance. Widen it only if your clocks drift:

const verifier = createVerifier(process.env.WEBHOOK_SECRET!, { toleranceSec: 600 });

toleranceSec must be a finite number, 0 or more. Anything else throws when you create the verifier.

Responding

Answer failures with a 4xx. A sender like @typedhook/sdk treats any non-2xx as a failed attempt and retries it later, so fixing a wrong secret on your side lets the next attempt through. Return result.reason in the body when the sender is yours, and keep it out of public responses if you'd rather not tell a forger which check failed.

Edit on GitHubLast updated on October 14, 2026

On this page

For coding agents

Every page is available as Markdown. Point your agent at the full docs in one file.