Skip to main content

Confirm

Are you sure?

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

For the receiver itself, use the version for your stack: Python, Node.js, PHP, Ruby, Go, Java, and C#.

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.

Terminal
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.

send_signed_delivery.sh
#!/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.

check_signature.sh
#!/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?
The signature covers the exact bytes that were sent. Parsing the JSON and serializing it again changes key order and whitespace, and the HMAC with them. In cURL: --data-binary, which sends the file byte for byte.
Why can the signature header hold more than one value?
During a secret rotation every delivery is signed with the new and the old secret for 24 hours, so a receiver holding either one keeps verifying. Accept the delivery if any v1= value matches.
What should a receiver answer when the check fails?
A 401. The delivery is retried with backoff, up to six attempts, and the result stays available from GET /result/:token for 30 days either way.
Why a five-minute tolerance?
The timestamp is signed, so it cannot be changed; rejecting old ones stops a captured delivery being replayed later. Every retry is signed afresh, so genuine retries pass.

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.