Skip to content
Webhooks

Verifying Signatures

Every outbound webhook from kirim.dev carries an HMAC signature in the X-Kirim-Signature header. You MUST verify it. Without verification, anyone who learns your endpoint URL can fire fake events at you.

X-Kirim-Signature: t=1716480000,v1=<hex>[,v1=<hex>...]
ComponentMeaning
t=…Unix epoch seconds (UTC) when kirim.dev signed the payload.
v1=…Hex-encoded HMAC-SHA256 of "{t}.{raw_body}" keyed by one of your subscription’s active signing secrets.

Multiple v1= segments are comma-separated and appear during secret rotation — kirim.dev signs each delivery with every currently active secret so you can verify against either while you roll your stored secret over.

The signing payload is "{t}.{raw_body}" — literally the unix timestamp, a single dot, then the raw bytes of the request body as kirim.dev sent them (no pretty-printing, no key reordering).

The official SDK ships a verifier that runs on Node, Bun, Deno, and any Web-Crypto-capable edge runtime.

import { verify } from '@kirimdev/sdk/webhooks'
app.post('/webhooks/kirim', async (req, res) => {
const rawBody = req.body.toString('utf8') // express.raw() — bytes preserved
const ok = await verify({
rawBody,
signatureHeader: req.headers['x-kirim-signature'],
secrets: [
process.env.KIRIM_SECRET_PRIMARY!,
process.env.KIRIM_SECRET_ROTATING ?? null,
].filter(Boolean) as string[],
toleranceSeconds: 300, // optional; defaults to 5 min
})
if (!ok) return res.status(401).send('invalid signature')
const event = JSON.parse(rawBody)
// ... process event
res.status(200).send('ok')
})

verify accepts an array of secrets so you don’t have to write rotation logic yourself — pass every active secret, it returns true if any of them produces a matching signature.

The recipes above reject signatures with a t= more than 5 minutes away from now. This guards against an attacker who captures a signed delivery (e.g. from a proxy log) and replays it weeks later — the timestamp won’t validate.

If your endpoint runs geographically far from kirim.dev’s region, increase toleranceSeconds to absorb clock drift, but keep it under 10 minutes.

Subscriptions support multiple active signing secrets simultaneously. Rotate at any time:

  1. Mint a new secret. kirim.dev returns the plaintext once — store it immediately.

    Terminal window
    curl -X POST \
    https://api.kirim.chat/v1/webhook_subscriptions/wbs_…/secrets \
    -H "Authorization: Bearer $KIRIM_KEY"
    const next = await kirim.webhookSubscriptions.addSecret('wbs_…')
    // next.secret is plaintext — persist it now.
  2. Deploy your verifier with both secrets in the secrets array. For the duration of the overlap, kirim.dev signs every delivery with both secrets (two v1= segments per header). Either one must verify.

    secrets: [OLD_SECRET, NEW_SECRET].filter(Boolean)
  3. Confirm in production that deliveries verify under the new secret — the dashboard’s Recent deliveries view shows which secret matched each request.

  4. Revoke the old secret.

    Terminal window
    curl -X DELETE \
    https://api.kirim.chat/v1/webhook_subscriptions/wbs_…/secrets/whs_OLD \
    -H "Authorization: Bearer $KIRIM_KEY"

    Future deliveries are signed only with the new secret. Remove the old one from your environment.