Guides a webhook
Send HTML form submissions to a webhook
A webhook turns a form into an event your own code can act on: add a lead to your CRM, open a ticket, kick off a workflow. sendm8 POSTs each submission to your URL as JSON, signs it so you can check it’s genuine, and retries if your server is down.
Set it up
- Sign in to sendm8 with GitHub or Google and create a form. You get an endpoint like
https://sendm8.com/f/k3x9q2m7ab. (Email-in-the-URL endpoints deliver by email only; channels like this one live on dashboard forms.) - Open the form’s Settings → Channels and add a webhook with your URL. It must be
httpsand reachable from the internet: private IP ranges and bare IP addresses are refused. - Copy the signing secret it shows you, and store it with your app (for example as
SENDM8_WEBHOOK_SECRET). - Click Send test to send a sample request.
The form
<form action="https://sendm8.com/f/k3x9q2m7ab" method="POST">
<label>Name <input name="name" required></label>
<label>Email <input name="email" type="email" required></label>
<label>Message <textarea name="message" required></textarea></label>
<input type="text" name="_gotcha" style="display:none" tabindex="-1" autocomplete="off">
<button>Send</button>
</form>What your endpoint receives
A POST with a JSON body. Your field names are kept as they are in data:
{
"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" }
}Check the signature
Anyone can post to a public URL, so verify each request. The X-Sendm8-Signature header holds a Unix timestamp t and a signature v1: an HMAC-SHA256, in hex, of the timestamp, a full stop and the raw body, keyed with your signing secret.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifySendm8(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.trim().split("=")));
const t = Number(parts.t);
if (!t || !parts.v1) return false;
// Refuse old deliveries, so a captured request can't be replayed
if (Math.abs(Date.now() / 1000 - t) > 300) 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);
}Check the signature against the raw body, before any JSON parsing changes it. The webhook docs have a complete handler.
Retries and duplicates
Answer with any 2xx within 5 seconds, and do slow work after responding. On an error or timeout, sendm8 tries again after 1m, 5m, 30m, 2h, 12h. Each retry carries a fresh timestamp and signature, so use the submission id to skip anything you’ve already handled.
No-code tools
Services such as Zapier, Make and n8n give you a “catch hook” URL. Paste it in as the webhook and map the data fields to the next step. Most of those tools can’t check the signature, so treat their URL like a password.
Spam is filtered before delivery, so your endpoint only sees submissions that passed the honeypot, rate limits, spam scoring and AI check. The same submission can go to email and chat channels too: a form can have up to 10 channels.
Questions
- Is the webhook payload signed?
- Yes. Each request has an X-Sendm8-Signature header with a timestamp and an HMAC-SHA256 of the timestamp and raw body, keyed with your signing secret.
- What happens if my server is down?
- sendm8 retries after 1m, 5m, 30m, 2h, 12h. Every attempt is shown on the submission in your dashboard, and the submission itself is stored either way.
- Can I use a webhook with Zapier, Make or n8n?
- Yes. Paste the catch-hook URL those tools give you as the webhook URL, then map the data fields in your workflow.