cURL · Code recipe
Verify a webhook signature in cURL
Prove a delivery came from URLpipe before you act on it, in any shell with curl with curl. 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
A URLpipe webhook signature is HMAC-SHA256 over the X-URLpipe-Timestamp value, a dot and the raw body, keyed with your whsec_ secret and sent as v1=<hex>. openssl dgst computes it in the shell — enough to send your receiver a signed test delivery or check one you captured. The production check belongs in application code, compared in constant time.
Free plan, no credit card. 1,000 credits a month.
Turn on webhook signing for a project and every delivery carries two headers: X-URLpipe-Timestamp, the Unix time it was signed, and X-URLpipe-Signature, one or more v1=<hex> values. The shell scripts below compute the same HMAC with openssl dgst, to test a receiver and debug a delivery.
Setup
Before you start
A shell is the wrong place to receive webhooks, and a string comparison in bash is not constant-time, so the production check belongs in your application — see the other languages below. What the shell is good for: sending your receiver a correctly signed test delivery, and checking a delivery you captured. Both need only curl and openssl.
openssl version
export URLPIPE_WEBHOOK_SECRET="whsec_your_signing_secret"
Test
Send your receiver a signed test delivery
Signs a sample payload exactly as URLpipe signs a real one — HMAC-SHA256 over the timestamp, a dot and the raw body — so you can test your endpoint before turning signing on. --data-binary sends the body byte for byte; -d would strip newlines.
#!/usr/bin/env bash
# Usage: ./send_signed_delivery.sh [endpoint]
set -euo pipefail
endpoint=${1:-http://localhost:8000/webhooks/urlpipe}
body='{"token":"test_token","operation":"markdown","labels":{},"success":true,"result":"# Example Domain","result_url":null,"error":null,"meta":{}}'
timestamp=$(date +%s)
signature=$(printf '%s.%s' "$timestamp" "$body" \
| openssl dgst -sha256 -hmac "$URLPIPE_WEBHOOK_SECRET" | sed 's/^.*= //')
curl -sS -o /dev/null -w 'Your endpoint answered %{http_code}\n' "$endpoint" \
-H "Content-Type: application/json" \
-H "X-URLpipe-Timestamp: $timestamp" \
-H "X-URLpipe-Signature: v1=$signature" \
--data-binary "$body"
Run it: bash send_signed_delivery.sh
Debug
Check a delivery you captured
Save the raw body of a delivery to a file (your receiver's logs, or a request inspector) and pass the two header values. The script recomputes the signature and says whether any value in the header matches and how old the timestamp is.
#!/usr/bin/env bash
# Usage: ./check_signature.sh TIMESTAMP 'SIGNATURE_HEADER' body.json
# For debugging only: bash compares strings in variable time.
set -euo pipefail
timestamp=$1
header=$2
body_file=$3
digest=$({ printf '%s.' "$timestamp"; cat "$body_file"; } \
| openssl dgst -sha256 -hmac "$URLPIPE_WEBHOOK_SECRET" | sed 's/^.*= //')
expected="v1=$digest"
age=$(( $(date +%s) - timestamp ))
IFS=',' read -ra signatures <<< "$header"
for signature in "${signatures[@]}"; do
if [ "${signature// /}" = "$expected" ]; then
echo "Valid signature, signed ${age#-} seconds ago"
if [ "${age#-}" -gt 300 ]; then
echo "Older than five minutes: a receiver with a 5-minute tolerance rejects it" >&2
exit 1
fi
exit 0
fi
done
echo "No signature matches: expected $expected" >&2
exit 1
Run it: bash check_signature.sh TIMESTAMP 'v1=…' body.json
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 cURL: every cURL 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.