Verifying Webhooks
Every webhook we send carries a signature so your endpoint can confirm the request genuinely came from SendItWhenever and wasn't tampered with. Always verify it before acting on a delivery.
The signature header
Each delivery includes an X-SendIt-Signature header:
X-SendIt-Signature: t=1750000000,v1=4f3a...e9
t— a Unix timestamp (seconds) of when the request was signed.v1— an HMAC-SHA256, hex-encoded.
The signature is computed over the string "{t}.{rawBody}" — the timestamp, a literal dot, and the raw request body bytes — keyed with your signing secret. Because t is part of the signed material, the signature also protects against replay.
Verifying with the SDK
The simplest path is verifySignature(). Give it the request headers and the raw body (not a parsed object — the bytes must match exactly):
import express from "express";
import { SendIt } from "@sendithq/sdk";
const sendit = new SendIt("sw_live_xxx", {
signingSecret: process.env.SENDIT_SIGNING_SECRET,
});
const app = express();
app.post("/hook", express.raw({ type: "*/*" }), (req, res) => {
const ok = sendit.verifySignature({
headers: req.headers,
rawBody: req.body, // a Buffer — the raw bytes
});
if (!ok) return res.status(401).end();
// Trusted from here. Deduplicate, then handle.
res.sendStatus(200);
});
You can also pass the secret per-call instead of in the constructor:
sendit.verifySignature({ headers, rawBody }, { secret: process.env.SENDIT_SIGNING_SECRET });
verifySignature does a constant-time comparison and rejects timestamps outside the allowed window (default 5 minutes) to block replays.
Python
The sendithq package exposes the same check as verify_signature(body, headers, ...). Pass the raw body bytes — for example, Flask's request.get_data():
from flask import request
from sendithq import SendIt
sendit = SendIt("sw_live_xxx", signing_secret=os.environ["SENDIT_SIGNING_SECRET"])
@app.post("/hook")
def hook():
ok = sendit.verify_signature(request.get_data(), request.headers)
if not ok:
return ("", 401)
# Trusted from here. Deduplicate, then handle.
return ("", 200)
Pass secrets=[current, next] to verify against both keys during a rotation:
pair = sendit.signing_secrets.get()
ok = sendit.verify_signature(
body, headers, secrets=[pair.current.secret, pair.next.secret]
)
Where the signing secret comes from
Your signing secret is separate from your API key. Fetch it from the dashboard, or via the SDK:
const { current, next } = await sendit.signingSecrets.get();
We keep two secrets active at once — current and next — so you can rotate without downtime.
Rotating the secret (zero downtime)
If a secret may have leaked, rotate it. Because two secrets are valid during a rotation, in-flight deliveries keep verifying.
const { current, next } = await sendit.signingSecrets.rotate();
The recommended flow:
- Deploy your endpoint so it accepts either the current or the next secret.
- Call
rotate()—nextis promoted tocurrent, and a newnextis generated. - Once you're confident nothing still signs with the old secret, drop it from your verifier.
Already-scheduled deliveries keep their secret. A schedule is signed with the secret that was current when it fires, but rotation does not re-sign work already queued for the far future. If you retire a secret, a long-dated schedule signed under it can fail verification. For schedules days or weeks out, keep the previous secret in your verifier until they've fired, or re-create them after rotating.
Verifying without the SDK
The check is plain HMAC-SHA256, so any language works:
- Read
tandv1fromX-SendIt-Signature. - Reject if
now - texceeds your tolerance (e.g. 300 seconds). - Compute
HMAC_SHA256(secret, "{t}.{rawBody}")and hex-encode it. - Compare to
v1with a constant-time equality check.
Use the exact raw body bytes — re-serializing parsed JSON will change the bytes and break the signature.