Webhook signature verifier

Check an HMAC SHA256 webhook signature: compute it over the exact signed payload with your secret, encode it like the sender and compare in constant time. Every step is shown.

  • Runs in your browser
  • Nothing uploaded
  • No sign up
preset

Signature valid

Signature valid. Accept the request and answer with a 2xx status.

› scheme

Placeholders: {id}, {timestamp}, {body}. The ${id} form works too.

pipeline

  1. 1

    signed payload · 101 bytes

    3f6c2b1e-8d4a-4f7b-9c2e-5a1d0b7e6f43.1790760000.{"type":"post.outcome","data":{"status":"published"}}
  2. 2

    key · 32 bytes from hex

    Hex pairs decoded to raw bytes.

  3. 3

    HMAC SHA-256 · raw digest

    9e45b3eb7ece95db8cf32c4320334dfb00bcf138f0ef9e52e2bd70edcaaab616

  4. 4

    encode · hex + "v1="

    v1=9e45b3eb7ece95db8cf32c4320334dfb00bcf138f0ef9e52e2bd70edcaaab616

    received

    v1=9e45b3eb7ece95db8cf32c4320334dfb00bcf138f0ef9e52e2bd70edcaaab616

  5. 5

    compare in constant time

    equal

    timestamp age 12s, window ±300s: fresh

Signature valid. Accept the request and answer with a 2xx status.

Computed with the Web Crypto API in this tab. Secrets and bodies are never sent anywhere. Use a test secret, not a live one.

Live. Everything runs on your device.Nothing is uploaded

How to verify a webhook signature

The console opens with a valid request. Change one character in the body and watch stage five fail.

  1. # 1 choose the scheme

    Pick the Kairo Post preset, a generic preset, or set the scheme yourself.

  2. # 2 paste the secret

    Enter a test secret and say whether it is text, hex or base64.

  3. # 3 paste the headers

    Paste the request headers as Name: value lines.

  4. # 4 paste the raw body

    Paste the body exactly as it arrived, before any JSON parsing.

  5. # 5 read the pipeline

    Follow the five stages; on a mismatch the tool names the likely cause.

Why webhook signatures need a constant time comparison

== stops at the first differing byte, so a guess that starts right takes slightly longer to reject. Measured often enough, that rebuilds a signature one character at a time.

Use this, not ==

  • Nodecrypto.timingSafeEqual
  • Pythonhmac.compare_digest
  • Gohmac.Equal

Node's version throws on different lengths, so check length first. Length is not secret.

# expected vs guess, one byte at a time

== stops at byte 8, answer arrives early

constant time reads all bytes, same delay every time

Webhook verification code for Node, Python and Go

Each example reads the raw body, rejects stale timestamps and compares in constant time before parsing anything.

Another sender? Change three things

  1. 1Signed stringoften just {body}
  2. 2Encodingdigest("base64") or b64encode
  3. 3Prefixsha256=, v1= or none
server.js · Express
import crypto from "node:crypto";
import express from "express";

const app = express();
// The endpoint secret: 64 hex characters, decoded to 32 raw bytes.
const SECRET = Buffer.from(process.env.KAIRO_WEBHOOK_SECRET, "hex");
const TOLERANCE_SECONDS = 300;

// express.raw keeps the exact bytes. Never verify a body that was parsed and serialised again.
app.post("/webhooks/kairo", express.raw({ type: "application/json" }), (req, res) => {
  const id = req.get("Kairo-Webhook-Id") ?? "";
  const timestamp = req.get("Kairo-Webhook-Timestamp") ?? "";
  const signature = req.get("Kairo-Webhook-Signature") ?? "";

  const now = Math.floor(Date.now() / 1000);
  if (!/^\d+$/.test(timestamp) || Math.abs(now - Number(timestamp)) > TOLERANCE_SECONDS) {
    return res.status(400).send("stale or missing timestamp");
  }

  const signed = Buffer.concat([Buffer.from(`${id}.${timestamp}.`), req.body]);
  const expected = "v1=" + crypto.createHmac("sha256", SECRET).update(signed).digest("hex");

  const a = Buffer.from(expected);
  const b = Buffer.from(signature);
  // timingSafeEqual throws on different lengths, so check the length first.
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.status(401).send("bad signature");
  }

  const event = JSON.parse(req.body.toString("utf8"));
  // Store id and skip any delivery you have already handled.
  res.sendStatus(204);
});

app.listen(3000);

Common webhook signature failures

Nearly every failed check is one of these. The console tests several for you on a mismatch.

Common webhook signature failures, symptoms and fixes
failuresymptomfix
Parsed body, not rawAlways mismatchesVerify raw bytes before any JSON middleware.
Wrong output encodingTwice as long, or has + and /SHA256 hex is 64 characters; base64 is 44.
Secret decoded wronglyMismatch with the right secretDecode a hex secret to bytes first.
Missing or extra prefixEverything after the prefix matchesAdd or strip v1= or sha256= before comparing.
Trailing newlineOnly pasted or logged bodies failTest with the body from the request, not a log.
Clock skewValid, rejected as staleUse network time; allow a window both ways.
Header renamed by a proxySignature header is emptyLog the header names your app receives.
Wrong character encodingFails only with accents or emojiVerify bytes; decode UTF-8 afterwards.

Webhook replay protection with timestamps

A valid signature proves who sent a request, not that it is new. A captured request would verify again.

  • Sign the timestamp

    Then changing it breaks the signature, and keeping it ages the request out. Check both directions.

  • Store delivery ids

    Keep every accepted id for at least the window and skip repeats. Retries become harmless too.

Window ±300 seconds around now

now12s old400s old500s ahead
−600s+600s

How Kairo Post signs its webhooks

Kairo Post can send signed events, such as a finished publish or an approval request, to an HTTPS endpoint you control. This is what our delivery code does.

Algorithm
HMAC SHA256
Key
32 random bytes, shown once as 64 hex characters
Signed payload
{id}.{timestamp}.{body}
Signature header
Kairo-Webhook-Signature: v1=<64 hex>
Delivery id
Kairo-Webhook-Id: a UUID
Timestamp
Kairo-Webhook-Timestamp: Unix seconds
Key id
Kairo-Webhook-Key-Id: which secret signed it
Other headers
Content-Type: application/json, User-Agent: KairoPost-Webhooks/1.0

Delivery rules

  • Public HTTPS on port 443

    No credentials or query string in the URL.

  • Bodies up to 16 KB

    Our reference verifier allows 300 seconds of clock difference.

  • Any 2xx is delivered

    Redirects are never followed; 10 second timeout.

  • New endpoints start switched off

    Install the secret first. Rotating switches it off and cancels queued deliveries.

Retries on 408, 425, 429, 5xx · up to 8 attempts

  1. 30s
  2. 2m
  3. 10m
  4. 1h
  5. 6h
  6. 12h
  7. 24h

A Retry-After header can lengthen a wait, up to 24 hours.

$ man webhook signatures

How do I verify an HMAC SHA256 webhook signature?
Build the signed string the sender describes, compute HMAC SHA256 over it with the shared secret, encode it the same way (hex or base64), and compare with the signature header in constant time.
Is it safe to paste my webhook secret here?
The tool runs in your browser with the Web Crypto API and sends nothing anywhere. Even so, use a test secret, and rotate any live secret you have pasted into a web page.
Why does my signature not match?
Usually a parsed and reserialised body instead of raw bytes, hex versus base64, a hex secret read as text, a missing prefix such as sha256=, or a trailing newline.
What is a constant time comparison?
One that takes the same time wherever the strings differ. A normal equality check stops at the first difference, which can leak how much of a guessed signature was right.
Why do webhooks include a timestamp?
So receivers can reject old requests. When the timestamp is part of the signed payload, a captured request cannot be replayed later without failing the age check.
How does Kairo Post sign webhooks?
HMAC SHA256 over id.timestamp.body with the endpoint secret decoded from hex, sent as v1= plus the hex digest in Kairo-Webhook-Signature, beside Kairo-Webhook-Id and Kairo-Webhook-Timestamp.
Which algorithms does this tool support?
HMAC with SHA-256, SHA-384, SHA-512 and SHA-1, the hashes Web Crypto offers. SHA-1 is only there for old integrations. There is no sign up and no cost.

Building on X as well? Estimate API usage with the X API cost calculator.

Post at the right moment.
Starting today.

Connect an account, describe your voice and let Kairo Post do the legwork.