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
POST /hooks/sendm8 HTTP/1.1
Content-Type: application/json
User-Agent: sendm8-webhooks
X-Sendm8-Signature: t=1789400527,v1=5967b4fd69839aeb4fabdbcee4b69e8e2f3f690d2602b67034f1b30382e1b79e{
"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);
}worker.js
// Example: a fetch-style handler (Cloudflare Workers with nodejs_compat, Bun, Deno)
export default {
async fetch(request, env) {
const raw = await request.text();
const header = request.headers.get("X-Sendm8-Signature") ?? "";
if (!verifySendm8(raw, header, env.SENDM8_WEBHOOK_SECRET)) {
return new Response("Bad signature", { status: 401 });
}
const submission = JSON.parse(raw);
// ...do something with submission.data
return new Response(null, { status: 204 });
},
};