Skip to content

Latest commit

 

History

History
133 lines (102 loc) · 5.74 KB

File metadata and controls

133 lines (102 loc) · 5.74 KB

Developer API — same shape as api.mailkite.dev

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 (buildMessagedeliverOutbound → 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.

Authentication

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.

Send

POST /v1/send

// request
{
  "from": "hello@myapp.ai",          // required — a domain this server hosts
  "to": "ada@example.com",           // required (string or array)
  "subject": "Your invoice",         // required
  "html": "<p>Thanks!</p>",          // html and/or text required
  "text": "Thanks!",
  "cc": ["billing@myapp.ai"],        // optional
  "bcc": ["archive@myapp.ai"],       // optional — envelope-only, never in headers
  "replyTo": "support@myapp.ai",     // optional
  "inReplyTo": "<msg@host>",         // optional — threads
  "headers": { "X-Tag": "receipts" } // optional — extra raw MIME headers
}
// 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.

Hosted-only fields

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.

Receive

GET /api/messages — account-wide list, newest first

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
}

GET /api/messages/:id — one message with bodies

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.

GET /v1/me — account probe

{ "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.

Reading a mailbox with an app password

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.

POST /v1/send/batch

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.

What is deliberately not copied

  • 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.

Compatibility scope and follow-up

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.