Webhooks
Mailbeam can push events to your server via HTTP webhooks. Use webhooks to receive real-time notifications when:
- A batch verification job completes
- A previously valid email starts hard-bouncing
- An MX record becomes unreachable
Setting up a webhook
- Go to Settings → Webhooks in your dashboard
- Click Add endpoint
- Enter your HTTPS URL and select the events to receive
- Save — Mailbeam will send a test event immediately
Webhook endpoints must be publicly reachable HTTPS URLs. Local development? Use ngrok or Cloudflare Tunnel.
Supported events
| Event | When it fires |
|---|---|
batch.completed | A batch job finishes processing |
email.bounced | A previously valid email starts hard-bouncing |
email.mx_degraded | MX records for a domain become unreachable |
quota.threshold | You reach 80% or 100% of your monthly quota |
Payload structure
All webhook payloads follow the same envelope:
{
"id": "evt_01hx9abc123",
"type": "batch.completed",
"created_at": "2025-01-15T14:32:00Z",
"data": {
"job_id": "job_01hx9xyz456",
"total": 5000,
"valid": 4213,
"invalid": 787,
"download_url": "https://api.mailbeam.dev/v1/jobs/job_01hx9xyz456/results"
}
}HMAC signature verification
Every webhook request includes an X-Mailbeam-Signature header. Always verify this signature before processing the payload.
The signature is an HMAC-SHA256 digest of the raw request body, keyed with your webhook secret.
import crypto from "crypto";
export function verifyWebhookSignature(
body: string | Buffer,
signature: string,
secret: string
): boolean {
const expected = crypto
.createHmac("sha256", secret)
.update(body)
.digest("hex");
// Use timingSafeEqual to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(`sha256=${expected}`, "utf8"),
Buffer.from(signature, "utf8")
);
}
// In your Express handler:
app.post("/webhooks/mailbeam", express.raw({ type: "application/json" }), (req, res) => {
const sig = req.headers["x-mailbeam-signature"] as string;
if (!verifyWebhookSignature(req.body, sig, process.env.MAILBEAM_WEBHOOK_SECRET!)) {
return res.status(400).send("Invalid signature");
}
const event = JSON.parse(req.body.toString());
// Handle event...
res.json({ received: true });
});Use
express.raw()— notexpress.json(). You need the raw body bytes for signature verification. Parsing JSON first will cause signature mismatches.
Retry policy
If your endpoint returns a non-2xx status or times out (30 second timeout), Mailbeam retries the webhook:
| Attempt | Delay |
|---|---|
| 1st retry | 5 minutes |
| 2nd retry | 30 minutes |
| 3rd retry | 2 hours |
| 4th retry | 8 hours |
| 5th retry | 24 hours |
After 5 failed attempts, the webhook is disabled and you receive an email notification.
Responding to webhooks
Return a 2xx status as quickly as possible. Offload heavy processing to a queue:
app.post("/webhooks/mailbeam", async (req, res) => {
// Verify signature first
if (!verifyWebhookSignature(req.body, req.headers["x-mailbeam-signature"], secret)) {
return res.status(400).send("Invalid signature");
}
// Acknowledge immediately
res.json({ received: true });
// Process asynchronously
await queue.add("process-mailbeam-event", JSON.parse(req.body.toString()));
});