Skip to content

Verify a delivery came from us

A target can carry a signing secret. Every delivery to that target then arrives with three extra headers, and your receiver can use them to prove the body came from EchoRelay and reached you unaltered. A target without a signing secret is unchanged — no extra headers, nothing to do.

This is not the same thing as the target's auth. Auth proves us to your endpoint when it asks for a credential, and anyone who learns that credential can send the same header. A signature proves the body, to your code, on every delivery.

Turn it on

  1. Generate a secret — 32 random bytes, base64, with a whsec_ prefix:

    echo "whsec_$(openssl rand -base64 32)"
    
  2. Paste it into Signing secret on the target, under Endpoints → your endpoint → the target's request settings.

  3. Give the same value to whatever receives the delivery.

You hold this secret. We seal it the moment you save it and never show it back — a target that has one reads as *** over the API — so keep your own copy. You can replace it at any time; we cannot recover it for you.

Check the signature

Verify with a Standard Webhooks library rather than writing the check by hand. It is a cross-vendor specification, and its reference libraries cover C#, Elixir, Go, Java, JavaScript, PHP, Python, Ruby and Rust. A receiver built on one of those verifies us without a line of EchoRelay-specific code.

Hand the library three things: your secret, the raw body bytes exactly as they arrived, and these headers unchanged.

Header What it carries
webhook-id Names this delivery. The same on every retry of it.
webhook-timestamp Unix seconds, taken at the attempt that is reaching you now.
webhook-signature v1, then the signature itself.

Verify the bytes, then parse

Verify before you deserialize, and verify the body you actually received. Parsing JSON and re-encoding it changes bytes — key order, spacing, escapes — and the signature covers the bytes we sent, so a re-encoded body fails a check that should have passed.

Retries re-sign, under the same id

Each attempt is signed with its own timestamp, so a delivery that reaches you hours later — after retries, or after you replayed it from Held for retry — still verifies inside your library's replay window. The id does not change, which is what makes it safe to deduplicate on.

One incoming call can fan out to several targets, and two of those targets can be the same receiver holding two different bodies. Each delivery carries its own webhook-id, so deduplicating on that id drops real repeats and never drops a sibling.

If you relay a request that is already signed

A call you send us may already carry webhook-* headers of its own, from whoever signed it first. On a signed target we replace those headers rather than adding a second value, because a header with two values lets your receiver check the wrong one.

Rotate it, or stop signing

To rotate, paste a new value on the target and give it to your receiver. Keep the old one accepted there for a while: a delivery that we accepted before the change carries the secret it was accepted with, on every retry of it, so the two overlap until the older deliveries have drained.

To stop signing, set the target's signingSecret to null over the API or from an agent tool. The panel form sets and replaces the secret but does not clear it.

If you clear a signing secret and then roll that line back to a revision where the target was signing, we can restore the secret for a limited window after it was cleared. Past that window the rollback still happens, and the preview names every target that comes back unsigned before you confirm it.

Limits

  • A target has one signing secret at a time: whsec_ followed by exactly 32 random bytes in base64, as the command under Turn it on makes.
  • Queued, synchronous and streaming deliveries are all signed.
  • How old a timestamp your receiver accepts is set by your Standard Webhooks library, not by us.

When it fails

  • A secret in the wrong shape is refused when you save it.
  • If your receiver rejects a signature, we don't see it: to us the delivery succeeded or failed on the status your receiver returned, so log rejections on your side. The usual causes are the wrong secret, or a body that was parsed and serialised again before the check. Verify the raw bytes.

What it costs

Signing is free: a signed delivery is charged the same as an unsigned one.

Try it

Check one delivery by hand. Save its raw body as body.bin, then:

# not run headless: needs the signature and body from a delivery you received
WEBHOOK_ID='…'; WEBHOOK_TIMESTAMP='…'; WEBHOOK_SIGNATURE='v1,…'
WEBHOOK_SECRET='whsec_…'
KEY_HEX=$(printf '%s' "${WEBHOOK_SECRET#whsec_}" | openssl base64 -d -A | xxd -p -c 256)
EXPECTED=$(printf '%s.%s.' "$WEBHOOK_ID" "$WEBHOOK_TIMESTAMP" | cat - body.bin \
  | openssl dgst -sha256 -mac HMAC -macopt "hexkey:$KEY_HEX" -binary | openssl base64 -A)
case " $WEBHOOK_SIGNATURE " in *" v1,$EXPECTED "*) echo verified ;; *) echo rejected ;; esac

This is what a Standard Webhooks library does for you; use the library in your receiver.

FAQ

Do I have to sign anything to use EchoRelay?

No. Signing is per target and off until you set a secret. Targets without one behave exactly as they did before.

Can I use one secret for several targets?

You can paste the same value on more than one target, but a secret per target is the better habit: replacing one then affects one receiver, and a receiver only holds the key for its own deliveries.

Why is the timestamp not the time the request was accepted?

Because a signature minted when we accepted the request would already be outside your replay window by the time a retry arrives. Signing each attempt at the attempt is what lets a late delivery verify.

Will a signature ever appear on a target I did not configure?

No. The headers are added only when that target carries a signing secret.