Get your API key
Receiving email

Inbound webhooks

When mail arrives at any address on a verified domain, MailKite parses it and POSTs a JSON event to your webhook. No IMAP, no polling, no MIME wrangling — just the whole message, decoded.

The email.received event

This is exactly what hits your endpoint the moment an email arrives:

POST /hooks/mailkite
{
  "id": "msg_2Hk9…",
  "type": "email.received",
  "from": { "address": "ada@example.com" },
  "to": [{ "address": "support@myapp.ai" }],
  "subject": "Re: invoice #1042",
  "text": "Looks good — approved!",
  "html": "<p>Looks good — approved!</p>",
  "threadId": "<a1b2c3@mail.example.com>",
  "auth": { "spf": "pass", "dkim": "pass", "dmarc": "pass", "spam": "ham" },
  "attachments": [
    {
      "id": "msg_2Hk9…:0",
      "filename": "po.pdf",
      "contentType": "application/pdf",
      "size": 18213,
      "url": "https://api.mailkite.dev/att/2Hk9…/0?exp=…&sig=…"
    }
  ]
}

Fields

FieldTypeNotes
idstringStable message id. Use it to make processing idempotent.
typestringAlways email.received for inbound.
fromobject{ address } of the sender.
toarrayRecipient address(es): [{ address }].
subjectstring · nullDecoded subject line.
textstring · nullPlain-text body, already decoded.
htmlstring · nullHTML body, already decoded.
threadIdstring · nullIn-Reply-To or Message-ID — group a conversation.
authobjectEdge verdicts: spf, dkim, dmarc, spam (each may be null if not scored).
attachmentsarraySee below — each has a short-lived signed URL.

Handling the event

Respond with any 2xx status to acknowledge. Keep the handler fast — do heavy work asynchronously so you don't hold the connection open.

webhook handler
// Express
import express from "express";
const app = express();
app.use(express.json({ limit: "5mb" }));

app.post("/hooks/mailkite", (req, res) => {
  const event = req.body;
  if (event.type === "email.received") {
    console.log("from", event.from.address, "·", event.subject);
    // ...create a ticket, reply, store it, hand to an agent.
  }
  res.sendStatus(200); // ack fast; do heavy work out of band
});

app.listen(3000);
Verify the x-mailkite-signature header before trusting an event — see Verifying signatures. Your webhook URL is public, so signature checking is what proves an event really came from MailKite.

Attachments

Attachments aren't inlined. Each entry carries a url: a signed, time-limited GET link you can fetch with no credentials. Links stay valid for 7 days, after which the stored object is deleted.

attachment
{
  "id": "msg_2Hk9…:0",
  "filename": "po.pdf",
  "contentType": "application/pdf",
  "size": 18213,
  "url": "https://api.mailkite.dev/att/2Hk9…/0?exp=…&sig=…"
}

The link is bound to that exact object and self-expiring, so a leaked URL can't be repurposed or extended. Download what you need to keep within the retention window.

Routing: choose which address goes where

By default a catch-all sends every address on a domain to the domain's webhook. To direct specific addresses to specific endpoints, create routes:

create a route
await mk.createRoute({
  match: "support@myapp.ai",
  action: "webhook",
  destination: "https://myapp.ai/hooks/support",
});

A route has three parts:

FieldValuesMeaning
matchsupport@domain or *@domainWhich recipient(s) this rule applies to.
actionwebhook · forward · store · dropWhat to do with a match. Defaults to webhook.
destinationURL or addressRequired for webhook (a URL) and forward (an address).

Whatever the action, every inbound message is also stored so you can list, inspect, and replay it later — see Messages.

Test events & retries

Send a representative email.received event to your endpoint at any time — it's signed exactly like a live delivery:

send a test event
await mk.testWebhook("dom_…");

Each delivery attempt is recorded with its HTTP status. If your endpoint was down or returned a non-2xx, re-deliver the stored message — the exact same payload — to the same destination:

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

Next: verify webhook signatures so you only act on real events.