Skip to main content

Confirm

Are you sure?

Glossary

Webhook signature

How an endpoint anyone on the internet can POST to tells real deliveries from forged ones.

By · 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.

Verify a delivery (Flask)
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.

FAQ

Frequently asked questions

Why does my webhook signature not match?
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.
What timestamp tolerance should I use?
Five minutes is a common default: long enough for clock drift and slow networks, short enough to make replays useless.
Is HMAC the same as encryption?
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.