Get an endpoint Sign in

Docs Webhooks

Bay 11For your own code

#Webhooks

Add a webhook in Form settings → Channels → Webhook and we’ll POST every submission to your URL as JSON. It needs to be https and publicly reachable: private IP ranges and bare IP addresses are blocked. You’ll get a signing secret when you add it.

The request

Headers
POST /hooks/sendm8 HTTP/1.1
Content-Type: application/json
User-Agent: sendm8-webhooks
X-Sendm8-Signature: t=1789400527,v1=5967b4fd69839aeb4fabdbcee4b69e8e2f3f690d2602b67034f1b30382e1b79e
Body
{
  "event": "submission.created",
  "id": "01K53N7XQ2R8M4DE7Q2K9F4X3M",
  "tracking": "SM8 7Q2K 9F4X 3M",
  "created_at": "2026-09-14T15:42:07Z",
  "form": { "id": "k3x9q2m7ab", "name": "Studio contact" },
  "data": {
    "name": "Alex Moreno",
    "email": "alex@moreno.dev",
    "message": "Hiya! Are you free for a quick project in October?"
  },
  "files": [],
  "spam": { "score": 0.04, "verdict": "ok" }
}

Checking the signature

Anyone can post to a public URL, so check that a request really came from us. X-Sendm8-Signature holds a Unix timestamp t and a signature v1. The signature is an HMAC-SHA256, in hex, of the timestamp, a full stop and the raw request body, using your signing secret.

verify.js
import { createHmac, timingSafeEqual } from "node:crypto";

const FIVE_MINUTES = 5 * 60;

// rawBody must be the exact bytes we sent, before any JSON parsing.
export function verifySendm8(rawBody, header, secret) {
  const parts = Object.fromEntries(
    header.split(",").map((pair) => pair.trim().split("=")),
  );
  const t = Number(parts.t);
  if (!t || !parts.v1) return false;

  // Ignore old deliveries, so a captured request can't be replayed later
  if (Math.abs(Date.now() / 1000 - t) > FIVE_MINUTES) return false;

  const expected = createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");

  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1);
  return a.length === b.length && timingSafeEqual(a, b);
}