Email webhooks send delivery, bounce, complaint and engagement events to your application as they happen, so you can suppress bad addresses, update records and alert your team without polling an API. This guide shows how to build a secure receiving endpoint, verify every signature, handle retries and duplicates, and test the flow.
Check the exact names in your dashboard. Header names, event names and payload fields in this guide are illustrative. Use the names shown in your MigoSMTP webhook settings and event reference, and keep this guide for the pattern.
How webhooks work
When an event occurs, the platform sends an HTTPS POST request with a JSON body to the URL you registered. Your endpoint checks the request, stores the event and replies. If the reply is not a success, the platform tries again later.
- The event happens. A message is delivered, bounces, is reported as spam or is clicked.
- The platform sends a POST request. The body is JSON and the request carries a signature header.
- Your endpoint verifies the signature. Invalid requests are rejected with a 4xx status.
- Your endpoint stores the event and answers 200. It does this before any slow work.
- A worker processes the event. It suppresses an address, updates a record or raises an alert.
Common event types
| Event | Meaning | What your system usually does |
|---|---|---|
| delivered | The receiving server accepted the message | Mark the message as delivered |
| bounced | The message was rejected permanently or temporarily | Suppress hard bounces and review the reason |
| deferred | The receiver asked the sender to retry later | Watch for repeated deferrals at one provider |
| complained | The recipient reported the message as spam | Stop mailing that address at once |
| unsubscribed | The recipient opted out | Update consent and preferences |
| opened or clicked | Engagement was recorded | Update engagement scores, with the limits of open tracking in mind |
The exact list for your account is in the event reference. See the bounce code guide for what the reasons in bounce events mean.
An example payload
A bounce event could look like this. Field names are illustrative.
{
"id": "evt_8f2c1a",
"type": "bounced",
"created_at": "2026-05-12T09:41:07Z",
"data": {
"message_id": "msg_41b7",
"recipient": "user@example.com",
"bounce_type": "hard",
"status": "5.1.1",
"reason": "mailbox does not exist"
}
}
Verify the signature
The signature is an HMAC-SHA256 of the raw request body, calculated with the secret you saved when you registered the endpoint. Compute the same value, then compare the two with a constant-time function so timing differences leak nothing.
PHP example
<?php
$secret = getenv('WEBHOOK_SECRET');
$raw = file_get_contents('php://input');
$given = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $raw, $secret);
if (!hash_equals($expected, $given)) {
http_response_code(401);
exit;
}
$event = json_decode($raw, true);
// store $event, then answer quickly
http_response_code(200);
Node.js example
import crypto from 'node:crypto';
import express from 'express';
const app = express();
app.post('/webhooks/mail', express.raw({ type: 'application/json' }), (req, res) => {
const expected = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
const given = req.get('x-signature') || '';
const ok = given.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
if (!ok) return res.sendStatus(401);
const event = JSON.parse(req.body.toString());
// enqueue event, then answer
res.sendStatus(200);
});
Three common mistakes. Parsing and re-encoding the JSON before hashing, comparing signatures with a plain equals check, and logging the secret. Hash the raw bytes, use hash_equals or timingSafeEqual and keep the secret in an environment variable.
Handle retries and duplicates
Delivery is at least once, not exactly once. The same event can arrive twice and events can arrive out of order. Design for both.
| Problem | Why it happens | How to handle it |
|---|---|---|
| Duplicate event | A retry after a timeout or a lost response | Store the event id with a unique constraint and ignore repeats |
| Out-of-order events | Parallel delivery and retries | Compare timestamps or states before overwriting a record |
| Slow endpoint | Work done inside the request | Store, answer 200 and process in a queue |
| Long outage | Endpoint unreachable beyond the retry window | Backfill from the events API or logs after recovery |
| Burst of events | A large campaign finishing | Scale workers and keep the endpoint stateless |
CREATE TABLE webhook_events (
event_id VARCHAR(64) PRIMARY KEY,
type VARCHAR(40) NOT NULL,
payload JSON NOT NULL,
received_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
processed TINYINT(1) DEFAULT 0
);
Security checklist
- HTTPS only: the endpoint must use a valid certificate.
- Verify every request: reject anything without a valid signature before reading the body.
- Check the timestamp: if one is signed, refuse requests older than a few minutes.
- Limit exposure: use a long random path, restrict methods to POST and apply rate limits.
- Keep secrets out of code: store them in environment variables or a secret manager and rotate them on a schedule or after any suspected leak.
- Log safely: record event ids and outcomes, not secrets or full personal data.
Test before you rely on it
| Test | Expected result |
|---|---|
| Send a valid test event | Your endpoint answers 200 and stores one row |
| Send the same event again | Your endpoint answers 200 and stores no second row |
| Change one character in the body | Your endpoint answers 401 |
| Stop the worker and send events | Events are stored and processed after the worker starts |
| Return a 500 on purpose | The platform retries and the event arrives again |
Need help with your endpoint? Send us your endpoint URL pattern and a sample failed delivery and we will help you find the cause.
Further reading and sources
Frequently asked questions
What is an email webhook?
An email webhook is an HTTP request that the email platform sends to a URL you control when something happens to a message, for example when it is delivered, bounces, is reported as spam or is clicked. It lets your application react straight away instead of asking the API again and again.
Why should I verify the webhook signature?
Your endpoint is a public URL, so anyone who finds it can post data to it. A signature proves the request came from the platform and that the body was not changed on the way. Without verification an attacker could mark real addresses as bounced or trigger actions in your system.
How does HMAC signature verification work?
The sender and you share a secret. The sender computes an HMAC of the request body with that secret and sends the result in a header. You compute the same value from the exact bytes you received and compare. If the values match, the request is genuine.
Why must I use the raw request body?
The signature is calculated over the exact bytes that were sent. If your framework parses the JSON and you serialise it again, spacing and key order can change and the signature will no longer match. Read the raw body first, verify it and only then parse it.
What should my endpoint return?
Return a 2xx status code as soon as you have stored the event. Return a 4xx code for requests you reject, such as an invalid signature. A 5xx code, a timeout or no response is treated as a failure and the event is normally retried.
What happens when my endpoint is down?
Most platforms retry failed deliveries on a schedule with growing delays and give up after a set number of attempts. Check the retry policy and the event retention period in the webhook documentation, and use the events API or logs to backfill anything you missed during a long outage.
Can I receive the same event twice?
Yes. Retries, network problems and timeouts can lead to duplicates, and ordering is not guaranteed. Treat every handler as idempotent: store the event id, skip ids you have seen and make your actions safe to repeat.
Why should I process events in a queue?
Webhook senders expect a quick answer. If you do slow work such as database updates, external calls or sending messages inside the request, you risk timeouts and retries. Store the event, answer with 200 and let a worker do the rest.
How do I protect against replay attacks?
If the signature includes a timestamp, reject requests that are older than a few minutes and keep a record of event ids. A captured request then cannot be replayed later. Check the webhook documentation for how the timestamp is delivered and signed.
How can I test a webhook on my laptop?
Use a tunnelling tool to expose your local server on a temporary HTTPS URL, register that URL for a test webhook and send a test event. Remove the tunnel and the test registration when you finish, and never reuse a production secret for testing.