Get your API key
Receiving email

Webhook delivery & reliability

Every inbound email becomes a signed POST to your endpoint. If that delivery fails, MailKite retries it on a backoff schedule, stores the event so you can replay it later, and emails you when an endpoint goes unhealthy. The promise is simple: you never miss an email.

What counts as a successful delivery

A delivery attempt succeeds only when your endpoint returns a 2xx:

  • Success is HTTP 2xx only. Anything else is a failure and gets retried.
  • Redirects are not followed. A 3xx is treated as a failure — point the webhook at the final URL.
  • Each attempt has a 15-second timeout. A slower endpoint counts as a failure, so ack fast and do heavy work out of band.
  • The response body is ignored in the default mode — the status code is all that matters (the acknowledgement modes below change this).

Every attempt is signed exactly like a live delivery, so verify the signature before acting on it — see Verifying signatures.

Acknowledgement modes

By default any 2xx acknowledges a delivery. But a placeholder endpoint that returns 200 without doing anything looks identical to one that actually processed the message. Set an acknowledgement mode per webhook (the ackMode field, configured when you set up the webhook) to make MailKite require proof of handling:

ackModeWhat MailKite expects
lenient (default) Any 2xx. The response body is ignored.
ack Every delivery must return { "status": "ok" } (or an x-mailkite-ack: ok header). A 2xx with no ack is treated as failing — so a stub endpoint is detected and retried.
control The 2xx response body tells MailKite what to do with the stored message (see below).

Acknowledging a delivery (ack mode)

Return the SDK's replyOk() body — { "status": "ok" } — once you've handled the event. It needs no API key:

ack response body
{ "status": "ok" }
acknowledge a delivery
import express from "express";
import { MailKite } from "mailkite";

const SECRET = process.env.MAILKITE_WEBHOOK_SECRET;
const app = express();
app.use("/hooks/mailkite", express.raw({ type: "application/json" }));

app.post("/hooks/mailkite", (req, res) => {
  if (!MailKite.verifyWebhook(req.headers["x-mailkite-signature"], req.body, SECRET)) {
    return res.sendStatus(401);
  }
  const event = JSON.parse(req.body.toString("utf8"));
  // ...handle event.type === "email.received"
  // Confirm receipt: returns the JSON body {"status":"ok"}.
  res.type("application/json").send(MailKite.replyOk());
});
Install Docs →

Control mode: act on a message from your response

In control mode your 2xx body is an instruction. Return one of these and MailKite applies it to the stored message:

control responses
{ "status": "ok" }                                  // processed — keep the stored message
{ "status": "spam" }                                // flag the stored message as spam
{ "status": "drop" }                                // discard (delete) the stored message
{ "status": "ok", "actions": [{ "type": "block-sender" }] }  // block this sender for good
ResponseEffect
{ "status": "ok" }Processed normally — keep the stored message.
{ "status": "spam" }Mark the stored message as spam.
{ "status": "drop" }Discard (delete) the stored message.
{ "status": "ok", "actions": [{ "type": "block-sender" }] }Block this sender — future inbound from them is dropped before delivery.

The SDKs ship a helper per response so you don't hand-build the JSON: replyOk(), replySpam(), replyDrop(), and replyBlockSender() (each language uses its own naming — Go ReplySpam(), Python reply_spam(), and so on). Each returns the body string above.

control a message
import express from "express";
import { MailKite } from "mailkite";

const SECRET = process.env.MAILKITE_WEBHOOK_SECRET;
const app = express();
app.use("/hooks/mailkite", express.raw({ type: "application/json" }));

app.post("/hooks/mailkite", (req, res) => {
  if (!MailKite.verifyWebhook(req.headers["x-mailkite-signature"], req.body, SECRET)) {
    return res.sendStatus(401);
  }
  const event = JSON.parse(req.body.toString("utf8"));
  res.type("application/json");

  if (isObviousSpam(event)) {
    // Mark stored as spam — or block the sender for good:
    return res.send(MailKite.replyBlockSender()); // {"status":"ok","actions":[{"type":"block-sender"}]}
  }
  // ...process the message normally
  res.send(MailKite.replyOk());
});
Install Docs →

Automatic retries

When a delivery fails — a non-2xx, a redirect, a timeout, or a connection error — MailKite retries it automatically on an exponential backoff schedule. Seven retries spread over roughly 27.5 hours, and they stop early the moment an attempt succeeds:

RetryWait after the previous attempt
15 seconds
25 minutes
330 minutes
42 hours
55 hours
610 hours
710 hours

Every attempt is recorded with its HTTP status, so you can see the full delivery history in the dashboard.

Manual replay

Every inbound email is stored for your plan's retention window, so you can replay a delivery any time within it — the exact same signed payload, to the same destination. Replay one delivery:

retry a delivery
await mk.retryDelivery("dlv_…");
Install Docs →

Recovering from an outage? Re-arm all failed deliveries at once — for a single route or the whole account. This resets the backoff so every failed event is attempted again right away:

re-arm failed deliveries
# Re-arm every failed delivery for one route (resets the backoff)…
curl -X POST https://api.mailkite.dev/api/routes/rt_…/retry-failed \
  -H "authorization: Bearer <session-token>"

# …or for the whole account at once.
curl -X POST https://api.mailkite.dev/api/deliveries/retry-failed \
  -H "authorization: Bearer <session-token>"
Replay is bounded by retention: a message that has aged out of your plan's window is no longer stored, so there's nothing to replay. On zero-retention passthrough domains nothing is stored, so there's no replay at all — the live delivery is the only one.

Endpoint health & alerts

After sustained failures, MailKite flags the webhook unhealthy in the dashboard and emails the account owner — once per failing streak, so a flapping endpoint doesn't spam you.

Crucially, inbound mail is never silently dropped. A failed delivery doesn't lose the email: the event is stored and auto-retried on the schedule above, and you can replay it manually for the whole retention window. Even while your endpoint is down, the mail keeps arriving and waiting — so when you fix it, nothing was lost.

Next: verify webhook signatures so you only act on real events, or revisit inbound webhooks for the event shape.