All docs

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:

  1. Deploy your endpoint so it accepts either the current or the next secret.
  2. Call rotate()next is promoted to current, and a new next is generated.
  3. 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:

  1. Read t and v1 from X-SendIt-Signature.
  2. Reject if now - t exceeds your tolerance (e.g. 300 seconds).
  3. Compute HMAC_SHA256(secret, "{t}.{rawBody}") and hex-encode it.
  4. Compare to v1 with a constant-time equality check.

Use the exact raw body bytes — re-serializing parsed JSON will change the bytes and break the signature.