Free & open · no paywall, ever

IntegrAuth Academy

Identity, security and practical AI — free, from zero. Byte-sized lessons, real diagrams, hands-on labs, zero fluff. Two programs, each with its own exam and certificate.

20 tracks 241 lessons 242 hands-on labs 230+ diagrams 100% free

English is hard? Your browser can translate this page into Tamil, Telugu, Kannada, Malayalam or Hindi. Tech words stay in English so you learn the real names.

How?
  • Chrome on phone: ⋮ menu → Translate
  • Chrome on computer: right-click → Translate to …
  • Edge: the translate icon in the address bar
Identity & API Security Track 6 · API Security
0/166 lessons read
API SECURITY · LESSON 6

Signed requests and webhooks

A payment provider calls Bot A's server: "Maya's ₹500 payment worked. Ship her order." Anyone on the internet can send that same message. Your goal: check that a message is real, unchanged and new, before you act on it.

Key points
  • HMAC uses a shared secret to make a tag. Changing one byte changes the tag.
  • Sign a canonical request: method, path, query, headers, time and body hash.
  • A valid signature does not prove the message is new. Check the time and the id.
  • Check webhooks over the raw bytes, and handle retries once.
  • Use a key pair when a third party must check who sent a message.
Think of it like…

a "payment received" SMS at a small shop. Anyone can type that message, so careful owners check the bank app. A webhook signature is that check, built into the message. Only the real bank can make the right seal. The owner also asks: is this message new, or an old screenshot shown again?

HMAC: a seal made from a shared secret

HMAC (hash-based message authentication code) takes a secret key and a message. It runs them through a hash function such as SHA-256 and returns a short tag. Anyone with the same secret can compute the tag again and compare. Without the secret, you cannot make a valid tag. Change one byte of the message, and the tag is completely different.

HMAC is symmetric: both sides hold the same secret. So it only proves "someone who knows the secret wrote this". The receiver could have written it too. When a third party, or many parties, must check who sent a message, use a key-pair signature. The private key signs. Anyone can check with the public key. With HMAC, even a secret for each customer cannot prove authorship to an auditor.

Sign the whole request: the canonical request

What you sign matters as much as how. Sign only the body, and an attacker can change the query string, such as amount=5 to amount=500. So sign a canonical request. This is a strictly formatted summary of the parts that matter. It holds the method, the path, the sorted query, chosen headers, the timestamp and a hash of the body. The server builds it again from what it really received. If any part changed on the way, the tags differ.

Canonical request — one part per linePOST/v1/refundscurrency=usd&order=8271content-type:application/jsonhost:api.example.comx-date:20260925T101500Zcontent-type;host;x-date9f2c41e0… (SHA-256 of body)methodpathquery, sortedheaders: lowercase, sortedthe time is signed toowhich headersString to signtimestamp + scope + hash of the requestHMAC-SHA256(secret, …)= the signatureAuthorization headerkey id · signed headers · signatureThe server rebuilds the canonical request from what it received,recomputes the HMAC, and compares the two in constant time.
Figure — Sign the request, not just the body. Method, path, query, headers, time and a body hash all go into one text. Change any of them, and the signature no longer matches.

Real is not the same as fresh

A valid signature proves a message is genuine. It does not prove the message is new. An attacker can capture a signed "refund ₹500" request and send it again. This is a replay attack. Two defenses work together:

  • A timestamp inside the signed content. Reject anything older than a few minutes (five is common). The attacker cannot refresh a signed timestamp. A window that is too tight rejects honest requests from drifting clocks.
  • A unique id or nonce (a number used once). The receiver remembers it for the length of the window and rejects repeats.

Also compare tags with a constant-time function, such as hmac.compare_digest in Python. A plain == stops at the first different character. How long it takes can leak how much of a guess was right.

Webhooks: checking what comes in

A webhook is an HTTP callback: when something happens, a provider sends a POST to a URL you registered. The URL is public, so the signature is its lock. The Standard Webhooks pattern uses three headers: a unique message id, a timestamp and the signature. The signed content is id.timestamp.body. Five habits:

  1. Verify over the raw body, before you parse. Re-written JSON can differ in spacing or key order, so a real message fails.
  2. Check the timestamp against your window.
  3. Make retries harmless (be idempotent). Providers retry when you are slow. A retry has a valid signature and a fresh time, so Bot A could ship twice. Record each id when work starts. If an id repeats, return 2xx and do nothing. This also protects retried API calls (idempotency keys).
  4. Rotate without an outage. The sender signs with both old and new secrets and sends both. Accept the message if any one matches.
  5. Answer fast. Send a 2xx quickly and do slow work from a background queue. Slow answers cause retries.
1 · Raw bytesread the body as-isdon't parse yet2 · Signature?HMAC of id.ts.bodyconstant-time compare3 · Fresh?timestamp withina few minutes4 · New id?message id notprocessed before5 · Parse + actrecord the id,answer 2xx fast✗ reject, log it✗ reject as a replayduplicate → 2xx,do nothing again
Figure — Check, then trust, then act. Nothing is parsed until the signature over the raw bytes is good and the message is fresh. A duplicate gets a 2xx and no second action.
Watch out

A signature proves who sent a message and that nobody changed it. It does not prove the sender may ask for this. Keep the secret out of logs. A secret in a debug line is a forgery kit.

Go deeper: Length extension and the canonical list

Use the real HMAC construction. Never invent sha256(secret + message). With hashes like SHA-256, that lets an attacker add data and compute a valid tag without the secret. This is a length-extension attack. HMAC's two nested hashing passes close the hole.

A canonical request holds: the HTTP method, the path, the query sorted by name, chosen headers (lowercase, trimmed, sorted, including host and timestamp), the list of signed headers, and a SHA-256 hash of the body. That text, plus the timestamp and a scope, goes through HMAC. The result travels in the Authorization header with a key id. Some schemes derive a separate signing key for each day and service, so a stolen key works only in a narrow scope.

Go deeper: Sending webhooks: the SSRF trap

When your platform sends webhooks, customers type in the URL. An attacker can type an address that points inward, such as the cloud metadata address 169.254.169.254 or localhost. Your server then becomes a messenger into your own network. This is server-side request forgery (SSRF). More ways APIs break covers it. Defenses:

  • Look up the host name and refuse private, loopback, link-local and metadata addresses when you connect.
  • Do not follow redirects blindly.
  • Send from an isolated worker with no route to internal systems.
  • Sign what you send, with a separate secret for each customer.
Go deeper: A standard way to sign HTTP messages
HMACKey-pair signature
KeysOne secret, held by both sidesPrivate key signs, public key checks
Can the receiver forge it?YesNo
Many checkersEach needs the secretPublish the public key, like a JWKS
SpeedVery fastSlower, needs key distribution

HTTP Message Signatures (RFC 9421, February 2024) sign HTTP messages with either kind of key. A Signature-Input header lists what is covered, such as @method, @path, chosen headers and a Content-Digest header for the body. It also holds created, expires, nonce and keyid. The Signature header carries the result. Algorithms include HMAC-SHA256, Ed25519, ECDSA P-256 and RSA-PSS. The standard does not force replay protection. The checker must look at created and nonce.

🧪 Interactive lab — enable JavaScript to play with this one.

🧪 Try it free

Run printf '%s' 'msg_1.1790000000.{"amount":500}' | openssl dgst -sha256 -hmac 'demo-secret'. Change one character and run it again. The tag changes completely. Paste the same secret and message into the lab above to get the same tag. For the full pattern, read the open Standard Webhooks specification.