Base URL
https://app.dmu.email/api/v1
Authentication
Bearer dmk_… API key
Rate limit
120 requests a minute per key

Verify a signature

Every request is signed with the webhook’s signing secret. Check the signature before you trust the body.

The signature

The signing secret (whsec_…) is shown once, when the webhook is made or its secret replaced. DMU-Signature is t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256, keyed with the whole secret, over <t>.<the raw request body>.

Checking it

Check it on the raw bytes, before parsing the JSON, and refuse a t more than five minutes from your clock. A captured request can’t then be replayed later, and a replay within the five minutes carries an event id you have already handled.

JavaScript

import { createHmac, timingSafeEqual } from 'node:crypto'

// `body` is the raw request body as a string: express.raw(), or
// `await request.text()` in a fetch handler. Not a re-serialised object.
export function verify(header, body, secret, toleranceSeconds = 300) {
  const parts = (header ?? '').split(',').map((p) => p.trim())
  const t = Number(parts.find((p) => p.startsWith('t='))?.slice(2))
  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false
  const expected = createHmac('sha256', secret).update(`${t}.${body}`).digest()
  return parts.filter((p) => p.startsWith('v1=')).some((p) => {
    const given = Buffer.from(p.slice(3), 'hex')
    return given.length === expected.length && timingSafeEqual(given, expected)
  })
}

// if (!verify(req.headers['dmu-signature'], rawBody, process.env.DMU_WEBHOOK_SECRET)) return res.status(400).end()

Replacing the secret

After the secret is replaced, the old one keeps signing beside the new one for 24 hours: the header then has two v1= values, and either may match. Switch your receiver over within the day.