Skip to main content
MailLand

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.

Verify a signature
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);
}
HMAC-SHA256 over the timestamp, a period and the raw body. Reject anything older than 5 minutes.

Events you can subscribe to today

Eight event types are live. Subscribe to all of them or only the ones you need.

Events you can subscribe to today
Sent when
message.deliveredThe recipient's mail server accepted the message
message.deferredDelivery was delayed for now; Mail.Land keeps trying
message.bouncedThe recipient's server refused the message, with the bounce type and its reply
message.failedMail.Land marked the message as failed for an internal or policy reason; this event does not suppress the recipient
message.complainedThe recipient reported the message as spam through a feedback loop
message.outcome_unknownMail.Land could not yet tell whether the message reached its delivery queue; a final event follows if Mail.Land later learns the outcome
message.retriedYou retried a bounced or failed message and it is queued again
inbound.receivedAn 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.

When your endpoint is down
Wait before the attemptTime since the first attempt
Attempt 1None0
Attempt 2ImmediateAbout 0
Attempt 31 minuteAbout 1 minute
Attempt 45 minutesAbout 6 minutes
Attempt 515 minutesAbout 21 minutes
Attempt 61 hourAbout 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.