Webhooks
Verifying Signatures
Every webhook delivery includes a Trumpet-Signature header so you can verify the request genuinely came from trumpet and was not tampered with. Always verify before acting on a payload.
The signature header format
The Trumpet-Signature header has this form:
Trumpet-Signature: t=1717160000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd| Field | Description |
|---|---|
t | Unix timestamp (seconds) at which trumpet signed and sent the request. |
v1 | Hex-encoded HMAC-SHA256 signature. |
How the signature is computed
trumpet computes the signature as:
HMAC_SHA256( signing_secret, "{t}.{rawBody}" )- The signed string is the timestamp, a literal
.(full stop), then the exact raw request body bytes. - The key is your endpoint's signing secret including the
whsec_prefix — use the full secret string exactly as shown when you created the endpoint. - The result is hex-encoded and placed in
v1.
Verification steps
Read the raw body.
Capture the request body as raw bytes or a string before any JSON parsing. Parsing and re-serialising will change the bytes and break verification.
Do not parse before verifying
Parse the header
Split the Trumpet-Signature header on , and extract the t and v1 values.
Recompute
Compute HMAC_SHA256(signing_secret, "{t}.{rawBody}") and hex-encode it. Use the full whsec_-prefixed secret as the key.
Compare in constant time
Compare your computed value to v1 using a constant-time comparison (e.g. crypto.timingSafeEqual in Node, hmac.compare_digest in Python), not ==. This prevents timing-based attacks.
Check the timestamp
Reject the request if t is more than 5 minutes in the past (or future) relative to your server clock. This defends against replay attacks where an attacker re-sends a previously captured valid request.
Code samples
Complete, runnable examples. Both use the raw body, sign "{t}.{rawBody}", use the full whsec_ secret, compare in constant time, and enforce the 5-minute window.
import crypto from 'node:crypto';
// Use express.raw({ type: 'application/json' }) (or equivalent) so that
// req.body contains the raw Buffer — do not let body-parsing middleware
// parse it first.
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const isValid = verifyTrumpetWebhook(
req.body,
req.headers['trumpet-signature'],
process.env.TRUMPET_WEBHOOK_SECRET,
);
if (!isValid) {
return res.status(400).send('Invalid signature');
}
const event = JSON.parse(req.body.toString());
// handle event...
res.status(200).send('OK');
});
function verifyTrumpetWebhook(rawBody, signatureHeader, signingSecret) {
const parts = Object.fromEntries(
signatureHeader.split(',').map((kv) => kv.split('=')),
);
const timestamp = Number(parts.t);
const provided = parts.v1;
// 5-minute replay window
if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > 300) {
return false;
}
const expected = crypto
.createHmac('sha256', signingSecret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(provided ?? '');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}import hmac, hashlib, time
from flask import Flask, request
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def webhook():
raw_body = request.get_data() # raw bytes — before any parsing
signature_header = request.headers.get('Trumpet-Signature', '')
signing_secret = os.environ['TRUMPET_WEBHOOK_SECRET']
if not verify_trumpet_webhook(raw_body, signature_header, signing_secret):
return 'Invalid signature', 400
event = request.get_json(force=True)
# handle event...
return 'OK', 200
def verify_trumpet_webhook(raw_body: bytes, signature_header: str, signing_secret: str) -> bool:
parts = dict(kv.split("=", 1) for kv in signature_header.split(","))
timestamp = parts.get("t")
provided = parts.get("v1", "")
if not timestamp or abs(time.time() - int(timestamp)) > 300:
return False
expected = hmac.new(
signing_secret.encode(),
f"{timestamp}.{raw_body.decode()}".encode(),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, provided)If verification fails
4xx and do not process the event. A failing signature means either the secret is wrong, the raw body was modified (often by body-parsing middleware), or the request did not originate from trumpet.Regenerating the secret
You can regenerate an endpoint's signing secret at any time from Settings → Webhooks → Edit. A new whsec_-prefixed secret is generated immediately.
Regeneration invalidates the previous secret immediately