api-local exposes the same JSON send/receive surface MailKite Cloud does, so SDKs and
code written against api.mailkite.dev run unchanged against a self-hosted server — point
the client's baseUrl at this server instead of https://api.mailkite.dev.
The JSON surface is the compatibility layer. Underneath it is this server's own
pipeline (buildMessage → deliverOutbound → the SMARTHOST you configured), so "send"
means what it always meant here: local delivery plus whatever the smarthost reaches. This
document is the surface, not the transport.
Every endpoint here is user-trust: Authorization: Bearer <API key> with a key minted
for the account (mk_local_…, from POST /api/admin/keys or cli.mjs add-key). The key's
account must own the domain a send uses as its from.
Browser clients are supported with CORS. The default is Access-Control-Allow-Origin: *
because authentication is bearer-token-only; set CORS_ORIGIN to restrict it.
// 202 response
{ "id": "msg_2Hk9QpVn4tLd", "status": "sent" }The same gates as /api/relay and the console compose apply: the from domain must be
owned by the key's account (403 from_domain), recipients and the subject/body are
validated, and a send that would have nowhere to go (external recipients with no
SMARTHOST set) is refused before anything is stored (400 no_smarthost). A smarthost
failure returns 502 with the Sent copy already stored — the same trade the relay makes.
Fields that only exist on the managed product are refused with 400 unsupported, not
silently ignored, so a caller can't think they scheduled or attached something that never
happened: templateId, templateData, attachments, scheduledAt. Tracking flags
(trackOpens, trackClicks) and attribution are accepted but are no-ops — nothing is
tracked or attributed here.
Returns a bare JSON array (like the cloud), across both INBOX and Sent, newest first.
| Query | |
|---|---|
limit |
max rows, 1–200 (default 100) |
before |
a received_at epoch-ms cursor — pass the last row's received_at to page |
search |
case-insensitive substring over sender, recipient, and subject |
Each element carries the cloud's message shape (list rows, no bodies):
{
"id": "msg_2Hk9QpVn4tLd",
"direction": "inbound", // or "outbound"
"from": { "address": "ada@example.com" },
"to": [{ "address": "hello@myapp.ai" }],
"from_addr": "ada@example.com",
"to_addr": "hello@myapp.ai",
"subject": "Invoice question",
"received_at": 1769731200000, // epoch ms
"size_bytes": 4821
}Returns the cloud message-detail envelope. Its message member is the list shape plus
text_body, html_body, and headers_json (stored MIME headers as a JSON string).
deliveries, events, deliveryAttempts, opens, and attachments are empty arrays:
self-hosted has no cloud tracking records to report. 404 is returned for an unknown id.
{ "email": null, "verified": true, "plan": "self-hosted" }. Honest stubs: a self-hosted
server has no account email or plan, but the fields are present so SDK clients that ping
this on connect don't fail.
The mailbox REST routes (GET /api/mailbox/messages, …/raw, …/flags) are the same
shape on both sides already — see app-passwords.md and the "Mailbox API" section of
contract.md. That is how an agent reads its own inbox over plain HTTPS.
Accepts the cloud batch shape with a shared message body and up to 50 recipients entries.
Each entry is sent independently through the same single-message path, and the response
contains per-recipient results, sent, scheduled: 0, and failed counts.
- Templates, scheduling, attachments, tracking — hosted product features; refused
(
400 unsupported) rather than faked. - Delivery guarantees. The cloud's
status: "sent"means "on the warmed path"; here it means "stored and handed to your smarthost (or delivered locally)". Same shape, honest meaning.
This is the first plug-in-compatible slice for applications that use the MailKite library
to send and receive mail: point the library's baseUrl at the self-hosted server and use
an mk_local_… key. The core JSON contract, response envelopes, message IDs, batch send,
browser CORS, and raw mailbox responses are covered by the OSS and Node SDK regression
tests.
It is not yet a complete implementation of every MailKite Cloud API method. The remaining scope includes management APIs such as templates, domains, routes, delivery retry, attachments, tracking, realtime, and other hosted product features. Full API parity is a follow-up project; add each endpoint only with an explicit local behavior and tests rather than presenting a hosted-only no-op as compatible.