Webhooks and events
Delivery outcomes, pushed to your own app
Mail.Land posts a signed event to your endpoint when a message is delivered, deferred, bounced or ends in failure, when a recipient complains and when inbound mail arrives, with a log of every attempt.
import crypto from "node:crypto";
// header: X-MLND-Signature: t=1708171200,v1=<hex>
export function verify(rawBody, header, secret) {
if (typeof header !== "string" || header === "") return false;
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (!parts.t || !parts.v1 || age > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Events you can subscribe to today
Eight event types are live. Subscribe to all of them or only the ones you need.
| Sent when | |
|---|---|
| message.delivered | The recipient's mail server accepted the message |
| message.deferred | Delivery was delayed for now; Mail.Land keeps trying |
| message.bounced | The recipient's server refused the message, with the bounce type and its reply |
| message.failed | Mail.Land marked the message as failed for an internal or policy reason; this event does not suppress the recipient |
| message.complained | The recipient reported the message as spam through a feedback loop |
| message.outcome_unknown | Mail.Land could not yet tell whether the message reached its delivery queue; a final event follows if Mail.Land later learns the outcome |
| message.retried | You retried a bounced or failed message and it is queued again |
| inbound.received | An email arrived on one of your domains and matched a webhook route |
Other event types, such as message.sent, opens and clicks, are in the catalog for the future and do not fire yet. The catalog endpoint marks which ones are available now.
Signed, and sent only where it should go
Every request can be checked, and Mail.Land refuses destinations that could be abused.
HMAC-SHA256 with a timestamp
The X-MLND-Signature header holds a timestamp and a signature of that timestamp plus the raw body. Check it in constant time and refuse old timestamps to block replays.
HTTPS on port 443 only
Private and loopback addresses are refused. Redirects are followed for at most three hops, and every hop is checked again before anything is sent to it.
One secret per webhook
The signing secret is shown once when you create the webhook. If it leaks, regenerate it: the old one stops working at once.
When your endpoint is down
A failed delivery is retried on this schedule by default. A 2xx answer within 15 seconds counts as success.
| Wait before the attempt | Time since the first attempt | |
|---|---|---|
| Attempt 1 | None | 0 |
| Attempt 2 | Immediate | About 0 |
| Attempt 3 | 1 minute | About 1 minute |
| Attempt 4 | 5 minutes | About 6 minutes |
| Attempt 5 | 15 minutes | About 21 minutes |
| Attempt 6 | 1 hour | About 1 hour 21 minutes |
Timeouts, network errors and 408, 429, 500, 502, 503 and 504 answers are retried, and a Retry-After header sets the next wait. Other answers outside 2xx are final. You can retry any failed delivery from the log.
Built for events that arrive late or twice
Delivery is at least once and not always in order. Each event carries what you need to handle that.
Status versions
Every message event carries a status version that rises with each change. If you subscribe to every message event and apply an event only when its version is higher than the one you stored, your status stays right whatever the arrival order. With fewer events, check the current status through the API.
Stable event IDs
The event ID is the same on every retry of an event. Store the IDs you processed and skip repeats.
Versioned payloads
Each event type has a schema version. Mail.Land only adds fields and headers to an existing version, so a receiver that ignores unknown fields keeps working.
Control and visibility
Manage webhooks in the dashboard or through the API.
Filter by product and domain
Choose transactional, marketing or inbound events, specific event types, and all domains or only some.
Test before you rely on it
Send a test event with the real signing and transport. The answer also tells you whether real events can reach the endpoint with its current subscription.
Delivery log
See each attempt with its status, filter failed ones and retry a delivery by hand.
Automatic pause
After 10 failed attempts in a row by default the webhook is paused and you get a notification in the dashboard. Resume it when your endpoint is back.
Webhook questions
Is there an event when a message is accepted or sent?
Not yet. message.sent is in the catalog but does not fire today. The API response to your send request carries the message IDs, and the first webhook event you receive is the delivery outcome.
Can I receive open and click events?
Not by webhook today. Opens and clicks, where tracking is on, are in your logs and analytics; the open and click events are in the catalog for later.
What if a delivered message bounces later?
It can happen: a receiving server can refuse a message after accepting it. Mail.Land then sends a bounced event with a higher status version, so keep processing events after a delivered one.
What happens if my endpoint is down for a day?
Each event is tried up to six times over about 80 minutes by default, and the webhook is paused after 10 failed attempts in a row by default. While it is paused, new events are not queued for it. After you resume it, retry the failed deliveries from the log, check the status of messages through the API and replay the inbound messages you missed.
How do I verify that a request came from Mail.Land?
Compute HMAC-SHA256 over the timestamp, a period and the raw body with your webhook secret, compare it with the v1 value in constant time, and refuse timestamps older than 5 minutes. The docs have a worked example with exact values.
Can one endpoint receive events for only some of my domains?
Yes. Set the webhook's scope to selected domains and list them; it then receives events for those domains only.
Connect your app to every delivery outcome
Create a free account, add an endpoint and send it a test event.