Python · Code recipe
Verify a webhook signature in Python
Prove a delivery came from URLpipe before you act on it, in Python 3.9+ with requests. Every program on this page runs as it stands — each one was run against a stub of the API before it was published.
By Roger Campos · Last updated: September 2026
TL;DR
To verify a URLpipe webhook in Python, compute HMAC-SHA256 over the X-URLpipe-Timestamp value, a dot and the raw request body, keyed with your whsec_ secret. Prefix it with v1= and compare it with hmac.compare_digest against each comma-separated value of X-URLpipe-Signature; reject timestamps more than five minutes off.
Free plan, no credit card. 1,000 credits a month.
Your report_to URL accepts a POST from anyone who learns it. Turn on webhook signing for the project and every delivery carries X-URLpipe-Timestamp and X-URLpipe-Signature, so the receiver can prove the body came from URLpipe, unchanged, in the last five minutes.
The Python receiver below does the whole check: it reads the raw body (request.get_data() in Flask or request.body in Django), recomputes the HMAC, compares it in constant time with hmac.compare_digest, and rejects stale timestamps. To send it a signed test delivery, use the shell script on the cURL page.
Setup
Before you start
Nothing to install: the receiver below is standard library only. Turn signing on under Settings → Webhook Signing and copy the secret (it starts with whsec_).
export URLPIPE_WEBHOOK_SECRET="whsec_your_signing_secret"
Receiver
A receiver that verifies every delivery
verify() is the part to copy into your app. In Flask pass it request.get_data(), in Django request.body — the raw bytes, never request.json re-serialized. hmac.compare_digest takes the same time whether the first byte or the last one differs, which is what stops a timing attack.
import hashlib
import hmac
import json
import os
import time
from http.server import BaseHTTPRequestHandler, HTTPServer
SECRET = os.environ["URLPIPE_WEBHOOK_SECRET"].encode()
TOLERANCE = 5 * 60 # seconds
def verify(body: bytes, timestamp: str, signature_header: str) -> bool:
"""True when the delivery was signed with SECRET in the last five minutes."""
if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > TOLERANCE:
return False
signed = timestamp.encode() + b"." + body
expected = ("v1=" + hmac.new(SECRET, signed, hashlib.sha256).hexdigest()).encode()
# One signature normally, two during a secret rotation: accept any match.
return any(hmac.compare_digest(s.strip().encode(), expected) for s in signature_header.split(","))
class Webhook(BaseHTTPRequestHandler):
def do_POST(self):
if self.path != "/webhooks/urlpipe":
return self.reply(404)
# The raw bytes, exactly as sent.
body = self.rfile.read(int(self.headers.get("Content-Length", 0)))
timestamp = self.headers.get("X-URLpipe-Timestamp", "")
signature = self.headers.get("X-URLpipe-Signature", "")
if not verify(body, timestamp, signature):
return self.reply(401)
delivery = json.loads(body)
print(f"Verified delivery for {delivery['token']}", flush=True)
self.reply(200)
def reply(self, status):
self.send_response(status)
self.end_headers()
HTTPServer(("", int(os.environ.get("PORT", 8000))), Webhook).serve_forever()
Run it: python3 webhook.py
Details
What to know about signed deliveries
- Signing is off until you turn it on under Settings → Webhook Signing; the secret starts with
whsec_. Enabling it is safe at any time — the body does not change — so enable it first and deploy the check after. - Rotating the secret opens a 24-hour window in which every delivery carries two signatures, the new one first. That is why the header is a list and any match is accepted.
- Each attempt is signed with a fresh timestamp, so a retry passes the five-minute check like the first delivery did.
- A delivery your endpoint rejects is retried with backoff, up to six attempts over roughly twenty minutes, and can be resent by hand from the dashboard afterwards.
- Make the handler idempotent on
token: the same result can arrive more than once.
Other languages
Verify a webhook signature in another language
More Python: every Python recipe · how signing works, in the docs
FAQ
Frequently asked questions
Why verify against the raw body?
Why can the signature header hold more than one value?
What should a receiver answer when the check fails?
Why a five-minute tolerance?
Get a key and run it.
Free plan, no card. Paste your key into URLPIPE_API_KEY and every program on this page runs as it is.