Skip to main content
Every delivery carries a Routeur-Signature header: an HMAC-SHA256 of the delivery’s timestamp and raw body, keyed with your subscription’s signing secret. Recompute it over the exact bytes you received, compare in constant time, and reject timestamps far from your clock. The functions below were checked against a signature produced by Routeur’s own signing code.

Headers

The header matches ^t=[0-9]+,v1=[0-9a-f]{64}$. There is one signature per delivery; there is no list of alternatives and no other version than v1.

The algorithm

  1. Take the raw request body as bytes, before any JSON parsing or re-encoding.
  2. Build the signing string: the decimal t, a single ., then the body bytes.
  3. Compute HMAC-SHA256 with the key set to your secret as ASCII, the whole string including the whsec_ prefix.
  4. Hex-encode the digest in lowercase and compare it with v1 using a constant-time comparison.
  5. Reject the delivery when |now − t| is larger than your tolerance. Five minutes is a sensible default.
Slack subscriptions are signed the same way over the Slack body.

Worked example

With this secret and timestamp:
and this body as one line with no trailing newline (1339 bytes):
the header is:
Save the body to body.json and reproduce the digest from a shell:
Use the same inputs to test your own implementation before pointing a subscription at it.

Code

Each function takes the raw body, the header value, the secret and a tolerance, and returns whether the delivery is authentic and recent. The now argument exists so you can test against the worked example with a fixed clock; leave it out in production.

In a handler

Read the body before anything parses it, verify, acknowledge, then do the work.
Frameworks that parse JSON before your handler runs change the bytes, and re-serializing the parsed object does not reliably give them back: the delivered body’s key order and whitespace are not guaranteed to match any serializer. Always verify the bytes as received.

Timestamps and replay

  • t is set when each attempt is signed, not when the event was created. A retry can arrive up to about an hour after created_at with a fresh t, so compare t with your clock, never with created_at.
  • A captured delivery can be replayed for as long as your tolerance allows. To close that window, record every Routeur-Delivery id you accept and ignore repeats; the id is also what makes retries idempotent.
  • Keep your endpoint’s clock synchronized. If you must accept a wider skew, widen the tolerance rather than dropping the check.