> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.fluveo.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.fluveo.com/_mcp/server.

# Verify a webhook signature

Every delivery is signed with HMAC-SHA256 and carries a `Stripe-Signature` header
(plus an equivalent `Fluveo-Signature`). Verify it against your endpoint's
`whsec_…` secret **before** parsing the body, and always verify the **raw** body —
before any JSON parsing re-serializes a byte. SDKs are not supported, so do the
check yourself; it is a few lines in any language. For the full contract, replay
rules, and retry behaviour see [Webhooks](/webhooks).

## Steps

1. **Read the raw body** as bytes, exactly as received.
2. **Parse the header.** `Stripe-Signature` looks like `t=1769349712,v1=5257a8…`.
   Split on `,`, then each part on the first `=`. Take the `t` value (a Unix
   timestamp) and every `v1` value. Ignore other schemes.
3. **Build the signed payload:** the `t` value, a `.`, then the raw body —
   `<t>.<raw body>`.
4. **Compute HMAC-SHA256** of the signed payload, keyed with the full endpoint
   secret string (including the `whsec_` prefix), and hex-encode it in lowercase.
5. **Compare in constant time** with each `v1` value. Accept if any one matches.
6. **Check the tolerance window.** Reject when `t` is more than 300 seconds away
   from your current time, so an old delivery cannot be replayed.

Reject with `400` on any failure, and only then parse the JSON.

Keep handlers **idempotent** (key on the event `id`); deliveries can repeat.