Verifying signatures
Every delivery is signed with your webhook secret. Verify it before you trust anything in the body, and reject what does not match.
The header
X-Doorloom-Signature: t=1759312504,v1=5f2a9c8d1e…
| Field | Type | Description |
|---|---|---|
t
|
integer | Unix timestamp, in seconds, of when the delivery was signed. |
v1
|
string | Lowercase hex HMAC-SHA256. There may be more than one — see secret rotation below. |
How to verify
- Parse
tand everyv1out of the header. - Reject if
tis more than 300 seconds from your clock. - Compute
HMAC-SHA256(secret, "{t}.{raw_body}"), hex-encoded. - Accept if it matches any
v1, compared in constant time.
Hash the raw request bytes
request.body; in Express you need express.raw() or verify.Reference implementations
These are the implementations Doorloom tests against. Copying one is safer than writing your own.
const crypto = require('crypto');
function verify(secret, rawBody, header) {
const parts = header.split(',').map((p) => p.trim());
const t = Number((parts.find((p) => p.startsWith('t=')) || '').slice(2));
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex');
return parts
.filter((p) => p.startsWith('v1='))
.map((p) => p.slice(3))
.some((sig) =>
sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))
);
}
import hashlib
import hmac
import time
def verify(secret: str, raw_body: bytes, header: str) -> bool:
timestamp = None
signatures = []
for part in header.split(","):
key, _, value = part.strip().partition("=")
if key == "t":
timestamp = int(value)
elif key == "v1":
signatures.append(value.lower())
if not timestamp or abs(time.time() - timestamp) > 300:
return False
signed = f"{timestamp}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, sig) for sig in signatures)
Why more than one v1
When your webhook secret is rotated, the previous secret keeps verifying for 24 hours. Through that window Doorloom signs each delivery with both secrets and sends both results:
X-Doorloom-Signature: t=1759312504,v1=5f2a9c8d1e…,v1=b71e4a0c92…
This is what makes a rotation zero-downtime: whichever secret your running build holds, one of
the values matches. An implementation that reads only the first v1= works perfectly
until the day of a rotation and then rejects half its traffic — which is why both examples
above loop.
What to return on a bad signature
Return 401. That is a non-retryable 4xx, so Doorloom dead-letters the delivery and
alerts staff rather than hammering you — which is the outcome you want, because a signature
failure means something is genuinely wrong with the secret rather than with the moment.
Do not skip the timestamp check
±300 seconds, and
keep your server clock synchronised.