Guides
Webhooks
YourMail can push real-time delivery events to your server as signed HTTP POST requests. Register a webhook endpoint in the dashboard and YourMail will notify you when an email is sent, delivered, bounced, or complained about.
Why use this
GET /v1/emails/:id works for occasional status checks, but webhooks let you react to delivery outcomes as they happen without keeping a poll loop running. This is how you keep your own database in sync with what YourMail knows.For example
When alice@example.com hard-bounces, YourMail fires email.bounced at your endpoint within seconds — so your app can immediately suppress her address and avoid burning your sender reputation.
Register an endpoint
Go to Webhooks in the dashboard, click Add endpoint, and paste your HTTPS URL. YourMail will show you a signing secret — copy it into your server as YOURMAIL_WEBHOOK_SECRET. You can register multiple endpoints and choose which event types each one receives.
Event types
Every event has a type field. The full taxonomy:
email.sent- YourMail accepted the email and handed it to AWS SES.
email.delivered- The receiving mail server confirmed delivery.
email.opened- The recipient opened the email (requires open tracking to be enabled). May fire more than once.
email.clicked- The recipient clicked a tracked link in the email (requires click tracking to be enabled). May fire more than once.
email.bounced- A hard bounce was returned. The recipient address is automatically suppressed.
email.complained- The recipient filed a spam complaint. The address is automatically suppressed.
email.delivery_delayed- SES is retrying delivery due to a transient failure (soft bounce). A later delivered or bounced event will follow.
email.failed- The email could not be submitted to SES — e.g. the from address failed DKIM signing after dispatch.
Payload shape
Every event POST body is a JSON object. The data field carries the email metadata. For email.bounced and email.complained, data also includes the affected recipient and a reason string from the mail server.
// Every webhook POST body has this shape:
{
"id": "evt_01j9abc123def456", // unique event ID — dedupe on this
"type": "email.delivered", // event type
"created_at": "2026-06-30T12:00:00.000Z", // ISO 8601 timestamp
"data": {
"email_id": "jn7abc123def456", // the email this event belongs to
"from": "hello@mail.acme.com",
"to": ["alice@example.com"],
"subject": "Your receipt",
"tags": [{ "name": "category", "value": "receipt" }]
}
}// email.bounced — data carries the affected recipient and reason:
{
"id": "evt_01j9xyz789ghi012",
"type": "email.bounced",
"created_at": "2026-06-30T12:00:02.000Z",
"data": {
"email_id": "jn7abc123def456",
"from": "hello@mail.acme.com",
"to": ["alice@example.com"],
"subject": "Your receipt",
"tags": [],
"recipient": "alice@example.com", // the specific address that bounced/complained
"reason": "550 5.1.1 The email account does not exist."
}
}
// email.complained has the same shape — recipient is who filed the complaint.At-least-once · not ordered
Events are delivered at least once and are not guaranteed to arrive in order — your handler can receive email.delivered before email.sent. Dedupe on the event id; if you need sequencing, sort by created_at. Your endpoint must return a 2xx response within 10 seconds or YourMail will retry.
Signature verification
Every request includes a yourmail-signature header in the form t=<unix>,v1=<hmac-hex>. The HMAC is SHA-256 over "<timestamp>.<rawBody>" using your webhook secret as the key. Always verify this before processing an event — it proves the request came from YourMail, not a third party.
The easiest path is the SDK helper:
import { verifyWebhookSignature } from "yourmail";
// In your handler — pass the RAW request body, not a parsed object.
const ok = await verifyWebhookSignature({
secret: process.env.YOURMAIL_WEBHOOK_SECRET!,
header: req.headers["yourmail-signature"],
payload: rawBody,
});
if (!ok) return res.status(400).end();import { createHmac, timingSafeEqual } from "node:crypto";
function verifyYourMailWebhook(
secret: string,
header: string, // the YourMail-Signature header value
rawBody: string, // the un-parsed request body string
): boolean {
// Header format: t=<unix_timestamp>,v1=<hmac_hex>
const parts = Object.fromEntries(
header.split(",").map((p) => p.split("=") as [string, string]),
);
const timestamp = parts["t"];
const signature = parts["v1"];
if (!timestamp || !signature) return false;
const expected = createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
// Use a constant-time comparison to prevent timing attacks.
return expected.length === signature.length &&
timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}Important — pass the raw request body (a string or Buffer), not a parsed JSON.parse result. Body parsers in Express or Next.js may reformat whitespace, which invalidates the signature.
Full handler example
A minimal Next.js App Router route that verifies the signature and branches on event type:
// app/api/webhooks/yourmail/route.ts (Next.js App Router)
import { verifyWebhookSignature } from "yourmail";
import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest) {
const rawBody = await req.text();
const header = req.headers.get("yourmail-signature") ?? "";
const ok = await verifyWebhookSignature({
secret: process.env.YOURMAIL_WEBHOOK_SECRET!,
header,
payload: rawBody,
});
if (!ok) {
return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
}
const event = JSON.parse(rawBody);
switch (event.type) {
case "email.delivered":
// mark order as sent in your DB
break;
case "email.bounced":
// suppress or notify the recipient
break;
case "email.complained":
// unsubscribe the recipient
break;
}
return NextResponse.json({ received: true });
}