Glossary
Webhook signature
How an endpoint anyone on the internet can POST to tells real deliveries from forged ones.
By Roger Campos · Last updated: September 2026
TL;DR
A webhook signature is a hash-based message authentication code (HMAC) that the sender computes over a webhook's body with a secret shared with the receiver, and sends in a header. The receiver recomputes it; if the two match, the request came from the sender and was not altered on the way.
Free plan, no credit card. 1,000 credits a month.
How it works
How signing works
A webhook URL has to accept unauthenticated POSTs from the internet, so anyone who learns it can send it data. Signing fixes that. Sender and receiver share a secret. For each delivery the sender computes HMAC-SHA256(secret, message) and sends the hex digest in a header; the receiver computes the same thing and compares.
Two details make it safe. The timestamp is part of the signed message, so an old delivery replayed later fails a freshness check. The comparison is constant-time (hmac.compare_digest, crypto.timingSafeEqual), so the time it takes leaks nothing about the expected value. And it must run over the raw body: parse the JSON and re-serialise it, and key order or whitespace changes the digest.
URLpipe
URLpipe's scheme
Signing is enabled per project under Settings → Webhook Signing. Each signed delivery carries X-URLpipe-Timestamp (Unix seconds) and X-URLpipe-Signature, of the form v1=<hex>, computed over <timestamp>.<raw body>. During a secret rotation the header carries two comma-separated signatures for 24 hours, so accept the delivery if any one matches.
import hashlib, hmac, os, time
from flask import request, abort
SECRET = os.environ["URLPIPE_WEBHOOK_SECRET"].encode()
def verify():
body = request.get_data() # raw bytes, not request.json
ts = request.headers.get("X-URLpipe-Timestamp", "")
header = request.headers.get("X-URLpipe-Signature", "")
if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
abort(401)
expected = "v1=" + hmac.new(SECRET, ts.encode() + b"." + body, hashlib.sha256).hexdigest()
if not any(hmac.compare_digest(s.strip(), expected) for s in header.split(",")):
abort(401)Why not simply put a secret token in the webhook URL? Because URLs end up in logs, proxies and error trackers, a leaked one works forever, and it proves nothing about the body. A signature is computed per delivery over the exact bytes sent, so a captured request cannot be altered, and cannot be replayed once its timestamp is outside your tolerance.
A delivery your endpoint rejects is retried with backoff, and any delivery can be re-sent by hand from the dashboard. The JavaScript and PHP versions are in the async & webhooks docs.
Try it
Webhook signature (HMAC), on a page you choose
Related terms
FAQ
Frequently asked questions
Almost always because the digest was computed over a parsed and re-serialised body rather than the raw bytes, or with the wrong secret. Use the raw request body.
Five minutes is a common default: long enough for clock drift and slow networks, short enough to make replays useless.
No. HMAC proves who sent a message and that it wasn't changed; it doesn't hide the content. Use HTTPS for that.
See it on your own pages.
Free plan, no card. Confirm your email and your API key is live — you'll be making real requests in minutes.