Test your Sume webhook receiver locally: openssl and curl

Sign a fake job.completed body with openssl, post it with curl and confirm your receiver answers 200 for valid, and 401 for stale and forged Sume deliveries.

4 min readSume
All posts

You can test a webhook receiver without spending a generation. A Sume delivery is an HTTP POST with two headers and a JSON body, and the signature is plain HMAC-SHA256, so openssl and curl are enough to fake a delivery on your laptop. That lets you prove three things before the first real job: a valid delivery returns 2xx, a stale one is rejected and a forged one is rejected.

The scheme, from the webhook guide, is HMAC-SHA256(secret, "<timestamp>.<raw_body>"), hex encoded, sent as x-sume-webhook-signature: sume-v1=<hex> with the Unix timestamp in x-sume-webhook-timestamp.

The script

Set SUME_WEBHOOK_SECRET to the same value your receiver reads and pass your receiver URL as the first argument. The body mirrors the documented job.completed payload: event, request_id, job_id, status and payload.artifacts[] with id, url, type and content_type. The script prints one status code per case.

set -euo pipefail
: "${SUME_WEBHOOK_SECRET:?set SUME_WEBHOOK_SECRET}"
URL="${1:-http://localhost:3000/sume/webhook}"

BODY='{"event":"job.completed","request_id":"job_test","job_id":"job_test","status":"OK","payload":{"artifacts":[{"id":"artf_test","url":"https://example.com/out.mp4","type":"video","content_type":"video/mp4"}]}}'
sign() {
  printf '%s.%s' "$1" "$BODY" | openssl dgst -sha256 -hmac "$SUME_WEBHOOK_SECRET" -hex | sed 's/^.* //'
}
send() {
  curl -sS -o /dev/null -w '%{http_code}\n' -X POST "$URL" \
    -H 'content-type: application/json' \
    -H "x-sume-webhook-timestamp: $1" \
    -H "x-sume-webhook-signature: sume-v1=$2" \
    --data-binary "$BODY"
}

TS=$(date +%s); OLD=$((TS - 400))
echo "valid:   $(send "$TS" "$(sign "$TS")")"
echo "stale:   $(send "$OLD" "$(sign "$OLD")")"
echo "forged:  $(send "$TS" "$(printf '0%.0s' {1..64})")"

What you should see

Expected receiver answers for each local test case (read 2026-10-03)
CaseWhat changesExpected status
validFresh timestamp, correct signature200
staleTimestamp 400 seconds old, correctly signed401 with a 300 second window
forgedFixed all-zero signature401

The stale case is signed correctly over its own old timestamp. That isolates the replay window: a receiver that checks only the HMAC returns 200 here, and a receiver that enforces 300 seconds returns 401.

Test a secret rotation too

During a signing-secret rotation Sume sends one entry per live secret in a single header, comma-separated, as sume-v1=<new>,sume-v1=<previous>. Change the send function to pass sume-v1=$2,sume-v1=$3 with one wrong and one right signature. A receiver that accepts when any entry matches returns 200, and one that reads only the first entry returns 401 whenever the wrong one comes first. Run the case before the day you rotate, not during it.

Pitfalls

  • Use --data-binary. Plain -d strips newlines and can change the bytes you signed.
  • Sign the exact string you send. If your receiver parses and re-serialises JSON before verifying, the valid case fails.
  • Depending on the OpenSSL version, openssl dgst -hex may print a prefix such as SHA2-256(stdin)= , so the sed strips everything up to the last space.
  • A real delivery also includes x-sume-webhook-secret-fingerprint, which you can ignore in a local test.

The header list and rotation rule are in the webhook guide. Once the receiver passes, keep status polling as a backup for deliveries you miss.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume