Dashboard
Webhooks
HTTPS endpoints that receive signed delivery events, with retries and replay.
Webhooks lists the endpoints of the organization (URL, subscribed events, enabled, created). Click one to open its delivery log.
Creating an endpoint
Add webhook asks for an https:// URL and the events to receive. Leave the selection empty to receive all events. On creation, the console shows the signing secret (32 random bytes, base64url) once. Store it next to the URL in your app. The platform keeps the secret encrypted and cannot show it again. To get a new secret, delete the endpoint and create it again.
You can disable an endpoint. A disabled endpoint receives no new deliveries, and the platform marks its pending retries as Exhausted. You can also delete an endpoint. The delivery log goes with it.
Every producer app gets one endpoint for email.bounced, email.complained and email.delivery_delayed. The app then hears about a problem before a customer does.
Events
email.sent, email.delivered, email.delivery_delayed, email.bounced, email.complained, email.rejected, email.opened, email.clicked, email.unsubscribed, email.failed, email.received. Webhook payloads documents the payloads and headers.
Signature
Every request has an f5send-signature header:
f5send-signature: t=1755439391,v1=5f1c9c…e2
t is the Unix timestamp (seconds) at signing. v1 is HMAC-SHA256(secret, "<t>.<raw body>") in hex. Verify with a constant-time compare. Reject timestamps that are more than 5 minutes off:
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyF5sendSignature(secret: string, header: string, rawBody: string): boolean {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2) as [string, string]));
const t = Number(parts.t);
if (!Number.isFinite(t) || !parts.v1) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - t) > 300) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(parts.v1, "hex");
return a.length === b.length && timingSafeEqual(a, b);
}
Read the raw body before your framework parses JSON. Use f5send-delivery-id to de-duplicate. A retried delivery has the same id.
Delivery and retries
- Each event produces one delivery per matching endpoint. The
POSTtimes out after 5 seconds. Any2xxcounts as success. - On failure, the platform retries the delivery after 1 min, 5 min, 30 min, 2 h, 12 h. After the fifth retry, it marks the delivery Exhausted.
The endpoint page shows every delivery. Each row shows the event, the attempt number and the status (Pending / Success / Failed / Exhausted). It also shows the response code, the time and the first 2 KB of the response body.
Replay
Replay on a delivery row sends the delivery again now. It works for Failed and Exhausted rows; you cannot replay a Success. The retry counter continues from where it was. Use Replay after you repair a broken endpoint, instead of waiting for the next scheduled retry.