Webhook signature checker
Paste a delivery's secret, signature header and raw body and see whether the signature matches, how old it is, and what the signature should have been. Everything runs in this page; nothing you paste leaves your browser.
curl -X POST https://api.imageapis.com/v1/webhooks -H 'X-API-Key: YOUR_KEY' -H 'Content-Type: application/json' -d '{"url":"https://example.com/hooks/imageapis","events":["render.completed","video.completed"]}'What it checks
An ImageAPIs webhook delivery carries X-ImageAPIs-Signature: t=<unix seconds>,v1=<hex>. The v1 value is the HMAC-SHA256 of the timestamp, a dot and the raw request body, keyed with the webhook's whole secret, whsec_ included. This page computes that with your browser's own Web Crypto and compares:
- Match: the body is exactly what was signed, by someone who holds the secret.
- No match: the secret is a different webhook's, or the body changed on the way to your code. The usual cause is a framework that parsed the JSON and you stringified it again: spacing and key order change, and so does the signature. Verify the raw bytes before parsing.
- Age: deliveries are signed when they are sent. Reject ones more than a few minutes old in your server, so a captured request cannot be replayed later. A retry is signed again, so it is always fresh.
The example filled in above is a real signature for its body and secret, so the first check passes. Change one character of the body to see it fail.
The same check in your server
Node:
import crypto from "node:crypto";
function verify(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(String(header).split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const given = String(parts.v1 || "");
return given.length === expected.length && crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}
Python:
import hashlib, hmac, time
def verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in (header or "").split(",") if "=" in p)
t = int(parts.get("t", "0") or 0)
if not t or abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
Where the secret comes from
POST /v1/webhooks returns it when you subscribe a URL, and GET /v1/webhooks/{id} shows it again. Each webhook has its own. The webhooks guide covers the events, what each delivery carries, retries and test deliveries, and ImageAPIs on Zapier uses the same webhooks for its instant triggers.
The same scheme (a timestamp and an HMAC of timestamp.body) is what Stripe and several others use, so this checker works for any service that signs that way with SHA-256 and a hex digest.