Get your API key
Sending email

Send API

One endpoint, one key. The same address that receives can send — transactional mail, replies, and broadcasts go out over your own authenticated domain, DKIM-signed and aligned so they land in the inbox.

Send a message

POST /v1/send with an API key. The from address must be on a verified domain. Pick your language — the choice is remembered across the docs.

send a message
import { MailKite } from "mailkite";

const mk = new MailKite(process.env.MAILKITE_API_KEY);

const { id, status } = await mk.send({
from: "hello@myapp.ai",
to: "ada@example.com",
subject: "Your invoice #1042",
html: "<p>Thanks! Receipt attached.</p>",
});
Install Docs →
response
← 202 Accepted
{ "id": "msg_2Hk9…", "status": "queued" }

A 202 Accepted means the message was queued for delivery. Use the returned id to look it up later via the Messages API.

Request fields

FieldTypeNotes
fromstringRequired. An address on a verified domain.
tostring · arrayRequired. One address or a list.
subjectstringSubject line — required unless a template supplies it.
htmlstringHTML body. Provide html, text, or both.
textstringPlain-text body. Recommended alongside html.
templateIdstringSend a saved template — a user template (tpl_…) or base template (base_…); explicit subject/html/text override it.
templateDataobjectValues for the template's {{merge_tags}} (e.g. { "name": "Ann" }). HTML values are auto-escaped.
cc / bccstring · arrayOptional additional recipients.
replyTostringOptional Reply-To address.
attachmentsarrayEach: { filename, url } (fetched by us) or { filename, content } (base64).
inReplyTostringA threadId/Message-ID to reply in-thread.
headersobjectExtra raw MIME headers (string → string), applied after threading headers. For what the fields above can't express — List-Unsubscribe, a dedup key (X-Entity-Ref-ID), a tag (X-Tag). Carried on immediate and scheduled sends.
scheduledAtstring · numberSend later: ISO 8601, "in 2 hours", or a ms-epoch. A future time parks the message (response carries an ssnd_… id, status scheduled; cancel via DELETE /v1/scheduled/{id}).
trackOpensbooleanOpen-tracking override for this send (HTML only). Omitted → the domain's default applies.
trackClicksbooleanClick-tracking override (HTML only): links are rewritten to a signed redirect that records the click, then sends the reader on. Omitted → the domain's default applies.

Every snippet above is the official client library — same shape in Node, Python, PHP, Java, Go, and Ruby — or raw curl.

Batch send

POST /v1/send/batch sends one personalized message per recipient (up to 50) in a single call. Shared fields form the base message; each recipients[] entry gets its own message to exactly one address, with its templateData and headers merged over the shared ones (per-recipient wins). Recipients never see each other.

POST /v1/send/batch
{
"from": "billing@myapp.ai",
"subject": "Your {{plan}} invoice",
"html": "<p>Hi {{name}}, your {{plan}} invoice is ready.</p>",
"templateData": { "plan": "Pro" },
"recipients": [
{ "to": "Ada Lovelace <ada@example.com>", "templateData": { "name": "Ada" } },
{ "to": "grace@example.com", "templateData": { "name": "Grace", "plan": "Team" } }
]
}
response
← 200 OK
{
"results": [
{ "to": "Ada Lovelace <ada@example.com>", "id": "msg_2Hk9…", "status": "sent" },
{ "to": "grace@example.com", "id": "msg_8Rw2…", "status": "sent" }
],
"sent": 2, "scheduled": 0, "failed": 0
}

Every message passes the same gates as a single send and gets its own id, so a batch can partially succeed — check each result's status (sent · scheduled · failed). Results come back in request order. Pass scheduledAt to park the whole batch: each recipient gets its own cancelable ssnd_… scheduled send.

Batch request fields

FieldTypeNotes
fromstringRequired. An address on a verified domain, shared by every message.
recipientsarrayRequired. 1–50 entries, each { to, templateData?, headers? } — one message to that one address, its values merged over the shared ones (per-recipient wins).
subject / html / textstringThe base message. May contain {{merge_tags}}, filled per recipient.
templateIdstringSeed the base message from a saved template (tpl_… or base_…).
templateDataobjectShared default merge values; a recipient's own templateData wins key-by-key.
headersobjectShared extra MIME headers; a recipient's own headers win key-by-key.
replyTo / inReplyTostringApplied to every message.
attachmentsarrayAttached to every message. Same shape as a single send.
scheduledAtstring · numberPark the whole batch for later — one cancelable scheduled send per recipient.
trackOpensbooleanOpen-tracking override for every message (HTML only).
trackClicksbooleanClick-tracking override for every message (HTML only).

Replying in-thread

To reply to something you received, set inReplyTo to the inbound event's threadId. The reply threads correctly in the recipient's client:

reply.json
{
"from": "support@myapp.ai",
"to": "ada@example.com",
"subject": "Re: invoice #1042",
"text": "Approved — thanks!",
"inReplyTo": "<a1b2c3@mail.example.com>"
}

Attachments

Attach files two ways:

  • By URL{ filename, url }. We fetch it at send time. Best for files you already host.
  • Inline{ filename, content, contentType } with base64 content. Best for small, generated files.

Engagement events

Want the results pushed to you? Set a per-domain tracking webhook and MailKite POSTs signed email.sent, email.bounced, email.complained, email.opened and email.clicked events as they happen — opens and clicks flag proxy/scanner hits (machine: true) so your human counts stay honest, and bounces carry the DSN diagnostic your suppression logic needs. It's a separate URL from the inbound webhook, verified the same way.

Deliverability

Outbound is DKIM-signed and SPF/DMARC-aligned automatically on your verified domain — there's nothing to configure beyond publishing your DNS records. Always send a text part alongside html, keep your from consistent, and warm new domains gradually for the best inbox placement.

See every parameter and response in the API reference.