RefreshDocsConsole →

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.