API reference
Webhook payloads
The JSON that the platform delivers to your endpoint for each event type, and the headers that come with it.
You create endpoints in the console (Webhooks). See Webhooks for signing, verification and retries. Every delivery is an HTTPS POST with these headers:
| Header | Value |
|---|---|
content-type |
application/json |
user-agent |
f5send-webhooks/1 |
f5send-signature |
t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<body>"> |
f5send-delivery-id |
Unique id of this delivery (stable across retries). |
f5send-event-type |
The type below. |
Respond with any 2xx within 5 seconds.
email.* events
type is one of email.sent, email.delivered, email.delivery_delayed, email.bounced, email.complained, email.rejected, email.opened, email.clicked, email.unsubscribed, email.failed.
{
"type": "email.bounced",
"created_at": "2026-08-17T14:03:13.902Z",
"data": {
"email_id": "cmf9x1v0000018lfr6zot86p",
"from": "Acme <hello@example.com>",
"to": ["jane@example.org"],
"subject": "Welcome to Acme",
"recipient": "jane@example.org",
"kind": "invite",
"tags": [{ "name": "week", "value": "2026-33" }],
"bounce_type": "hard",
"detail": "General"
}
}
| Field | Notes |
|---|---|
email_id |
The message id from POST /emails. |
from, to, subject |
As sent. to is the to list of the message, not cc/bcc. |
recipient |
The one address this event is about. |
kind |
The message kind, or null. |
tags |
The tags array you sent, or — for template sends — { "template": { "id", "slug", "version" }, "tags": [...] }. null if none. |
bounce_type |
email.bounced only: hard, soft or undetermined. |
detail |
Provider sub-type (General, MailboxFull, …), delay type for email.delivery_delayed, or the failure text for email.failed. |
url |
email.clicked only: the destination link. |
The payload omits fields with no value.
email.received
The platform sends this event when a reply arrives on reply.<domain> (see Inbound).
{
"type": "email.received",
"created_at": "2026-08-17T15:10:02.000Z",
"data": {
"inbound_id": "cmi2…",
"email_id": "cmf9x1v0000018lfr6zot86p",
"from": "jane@example.org",
"to": ["reply@reply.example.com"],
"subject": "Re: Welcome to Acme"
}
}
email_id is present only when the platform could thread the reply to a message you sent. To thread a reply, the platform needs its In-Reply-To or References header to name a message id under one of your domains.
Verify a webhook
The HMAC covers the exact body bytes. A parsed body that you serialise again has different bytes, so verification fails.
Use verifyWebhookSignature from sdk/f5send.ts. Pass the raw body. Do not parse JSON first. The default tolerance is 300 seconds.
Next.js App Router
import { verifyWebhookSignature } from "./f5send";
export async function POST(request: Request) {
const rawBody = await request.text();
const result = verifyWebhookSignature({
rawBody,
headers: request.headers,
secret: process.env.F5SEND_WEBHOOK_SECRET!,
});
if (!result.ok) return new Response("unauthorized", { status: 401 });
return new Response("ok");
}
Hono
import { Hono } from "hono";
import { verifyWebhookSignature } from "./f5send";
const app = new Hono();
app.post("/webhooks/f5send", async (c) => {
const rawBody = await c.req.raw.text();
const result = verifyWebhookSignature({
rawBody,
headers: c.req.raw.headers,
secret: process.env.F5SEND_WEBHOOK_SECRET!,
});
if (!result.ok) return c.text("unauthorized", 401);
return c.text("ok");
});
Express
import express from "express";
import { verifyWebhookSignature } from "./f5send";
const app = express();
app.post("/webhooks/f5send", express.raw({ type: "*/*" }), (req, res) => {
const result = verifyWebhookSignature({
rawBody: req.body as Uint8Array,
headers: req.headers,
secret: process.env.F5SEND_WEBHOOK_SECRET!,
});
if (!result.ok) return res.status(401).send("unauthorized");
res.send("ok");
});
request.text(), c.req.raw.text(), and express.raw({ type: "*/*" }) keep the original bytes.