Webhooks

Signature header format: t=timestamp,v1=hex where hex is HMAC-SHA256 of `${timestamp}.${rawBody}`. Reject if |now − t| > 300s (configurable).

verify (node)
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(header, rawBody, secret, now = Math.floor(Date.now() / 1000)) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=").map((s) => s.trim())));
  const timestamp = Number(parts.t);
  if (!timestamp || Math.abs(now - timestamp) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
  const left = Buffer.from(expected);
  const right = Buffer.from(parts.v1 ?? "");
  return left.length === right.length && timingSafeEqual(left, right);
}

Delivery is at-least-once. Dedupe on the event id in the payload. Retry offsets: 0, 1m, 5m, 15m, 1h, 6h, 24h.

  • message.created
  • message.queued
  • message.submitted
  • message.delivered
  • message.failed
  • message.expired
  • message.rejected
  • campaign.started
  • campaign.completed
  • campaign.failed
  • wallet.low_balance
  • sender_id.approved
  • sender_id.rejected