Events
Webhooks
Get a request to your server when an email is delivered, bounces, is opened or is marked as spam.
NeedsAny accountTest keys produce simulated events, so you can build your handler before sending real email.
1. Add an endpoint
Under Webhooks in the dashboard, or:
const webhook = await pm.webhooks.create({
url: "https://acme.ng/webhooks/padimail",
event_types: ["email.delivered", "email.bounced", "email.complained"],
});
console.log(webhook.secret); // whsec_…: store it now, it's shown only here2. Handle the request
Each delivery is a POST with a JSON body and three headers: webhook-id, webhook-timestamp and webhook-signature. Check the signature before trusting it (see Verify signatures), then answer 2xx within 10 seconds. Do slow work after answering, for example in a queue.
import { verifyWebhook } from "@padimail/node";
export async function POST(req: Request) {
const event = await verifyWebhook({ payload: await req.text(), headers: req.headers, secret: process.env.PADIMAIL_WEBHOOK_SECRET! });
if (await alreadyHandled(req.headers.get("webhook-id")!)) return new Response(null, { status: 204 });
switch (event.type) {
case "email.bounced":
if (event.data.bounce.type === "hard") await markEmailInvalid(event.data.to);
break;
case "email.complained":
await unsubscribe(event.data.to);
break;
}
return new Response(null, { status: 204 });
}The body looks like this:
{
"type": "email.delivered",
"created_at": "2026-10-27T13:05:09Z",
"data": {
"email_id": "em_01JB…",
"recipient_id": "rcp_01JB…",
"to": "ada@example.com",
"from": "hello@acme.ng",
"subject": "Your receipt #1042",
"tags": [{ "name": "category", "value": "receipt" }],
"stream": "transactional",
"smtp_response": "250 2.0.0 OK"
}
}Every event type and its fields are in the event reference.
Delivery rules
- At least once. The same event can arrive twice. Skip any
webhook-idyou've already handled. - Any order.
email.openedcan arrive beforeemail.delivered. Usecreated_at, not arrival order. - Retries. If your endpoint doesn't answer
2xxwithin 10 seconds, we retry after 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours. An endpoint that keeps failing for 3 days is disabled; turn it back on from the dashboard once it works. - Replays. Every delivery is listed under the endpoint for 30 days, and you can send one again:
const { data } = await pm.webhooks.deliveries.list(webhook.id, { status: "failed" });
await pm.webhooks.deliveries.retry(webhook.id, data[0].id);Test it
await pm.webhooks.test(webhook.id, { event_type: "email.bounced" });curl -X POST https://api.padimail.app/v1/webhooks/wh_01JB.../test \
-H "Authorization: Bearer $PADIMAIL_API_KEY" -H "Content-Type: application/json" -d '{"event_type": "email.bounced"}'To test locally, expose your dev server with a tunnel such as cloudflared tunnel --url http://localhost:3000 and use that URL as the endpoint.
Was this page helpful?