Receive webhooks
A webhook is a POST request MailPush sends to your server when the status of a recipient changes. You no longer poll GET /v1/emails/:id.
Prerequisites
Section titled “Prerequisites”- An endpoint reachable over
httpson the public internet. MailPush refuseshttpaddresses,localhostand private networks. - A verified domain and a live API key, to produce real events.
1. Write the handler
Section titled “1. Write the handler”The handler verifies the signature, acts on the event, and answers with a 2xx status. With the Node.js SDK:
import { MailPush } from "mailpush";
const mailpush = new MailPush(process.env.MAILPUSH_API_KEY);
export async function POST(request: Request): Promise<Response> { const event = await mailpush.webhooks.verify(request, process.env.MAILPUSH_WEBHOOK_SECRET);
if (event.type === "email.bounced") { await markAddressAsInvalid(event.data.recipient); } return new Response(null, { status: 204 });}verify throws when the signature is wrong, so an invalid request never reaches your code. Without the SDK, see Verify the signature below.
2. Register the endpoint
Section titled “2. Register the endpoint”In the dashboard, open Webhooks, enter the address of your handler and tick the events you want. MailPush resolves the address at once and refuses it if it is not public.
3. Store the signing secret
Section titled “3. Store the signing secret”The secret is shown after you add the endpoint and starts with whsec_. Put it in an environment variable. You can display it again from the endpoint’s page.
4. Send a test event
Section titled “4. Send a test event”On the endpoint’s page, click Envoyer un événement de test. Your handler receives a webhook.test event, signed like the others, and the delivery log shows your server’s answer.
Verify the signature
Section titled “Verify the signature”Each request carries three headers, in the open Standard Webhooks format, so its libraries work with MailPush too.
| Header | Content |
|---|---|
webhook-id |
The id of the message, msg_.... It stays the same on every retry. |
webhook-timestamp |
When the attempt was sent, in Unix seconds. |
webhook-signature |
v1, followed by the base64 HMAC-SHA256 of webhook-id.webhook-timestamp.body, keyed with your secret. |
Compute the signature over the raw body, before any JSON parsing. A parser that changes one space makes it wrong.
Refuse a request whose timestamp is more than 5 minutes away from now. That stops someone who captured a valid request from sending it again later.
Without a library, on Node.js, Bun or Deno:
import { createHmac, timingSafeEqual } from "node:crypto";
export function isFromMailPush(headers: Headers, body: string, secret: string): boolean { const id = headers.get("webhook-id") ?? ""; const timestamp = headers.get("webhook-timestamp") ?? ""; const age = Math.abs(Date.now() / 1000 - Number(timestamp)); if (!(age <= 300)) return false; const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64"); const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${body}`).digest(); return (headers.get("webhook-signature") ?? "").split(" ").some((signature) => { const [version, value] = signature.split(","); const received = Buffer.from(value ?? "", "base64"); return version === "v1" && received.length === expected.length && timingSafeEqual(received, expected); });}Answer the request
Section titled “Answer the request”Return a 2xx status within 10 seconds. If your processing takes longer, store the event, answer, and process it afterwards.
MailPush does not follow redirects. A 3xx answer counts as a failure, so register the final address.
Retries
Section titled “Retries”When your endpoint does not answer with 2xx, MailPush sends the message again after 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours. That is 8 attempts over 27 hours and 35 minutes, after which the message is marked as failed.
An endpoint that has not answered correctly for 3 days is disabled the next time a message runs out of attempts. Fix it, then enable it again from its page.
The page of each endpoint lists its last 50 messages with their attempts and your server’s answer.
Handle duplicates and order
Section titled “Handle duplicates and order”A retry can reach you after your server already processed the first attempt, for example when your answer was lost. Keep the webhook-id values you processed and ignore the ones you have seen.
Events can arrive out of order. An email.delivery_delayed sent before an email.delivered may reach you after it. Use the timestamp field of each event, not the order of arrival.
Next steps
Section titled “Next steps”- Webhook events: each event type and its fields.
- Node.js SDK: typed events and one-line verification.