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.
Messages → Compose
From hello@myapp.ai
To ada@example.com
Subject Your invoice #1042
Body Thanks! Receipt attached.
Click [ Send ]Email ada@example.com from hello@myapp.ai
with subject "Your invoice #1042" and body "Thanks! Receipt attached."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>",
});import os
from mailkite import MailKite
mk = MailKite(os.environ["MAILKITE_API_KEY"])
res = mk.send({
"from": "hello@myapp.ai",
"to": "ada@example.com",
"subject": "Your invoice #1042",
"html": "<p>Thanks! Receipt attached.</p>",
})<?php
$mk = new \MailKite\Client(getenv('MAILKITE_API_KEY'));
$res = $mk->send([
'from' => 'hello@myapp.ai',
'to' => 'ada@example.com',
'subject' => 'Your invoice #1042',
'html' => '<p>Thanks! Receipt attached.</p>',
]);MailKite mk = new MailKite(System.getenv("MAILKITE_API_KEY"));
Object res = mk.send(Map.of(
"from", "hello@myapp.ai",
"to", "ada@example.com",
"subject", "Your invoice #1042",
"html", "<p>Thanks! Receipt attached.</p>"
));mk := mailkite.New(os.Getenv("MAILKITE_API_KEY"))
res, err := mk.Send(mailkite.Message{
From: "hello@myapp.ai",
To: "ada@example.com",
Subject: "Your invoice #1042",
HTML: "<p>Thanks! Receipt attached.</p>",
})require "mailkite"
mk = Mailkite::Client.new(ENV["MAILKITE_API_KEY"])
res = mk.send(
"from" => "hello@myapp.ai",
"to" => "ada@example.com",
"subject" => "Your invoice #1042",
"html" => "<p>Thanks! Receipt attached.</p>"
)curl https://api.mailkite.dev/v1/send \
-H "Authorization: Bearer $MAILKITE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "hello@myapp.ai",
"to": "ada@example.com",
"subject": "Your invoice #1042",
"html": "<p>Thanks! Receipt attached.</p>"
}' ← 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
| Field | Type | Notes |
|---|---|---|
from | string | Required. An address on a verified domain. |
to | string · array | Required. One address or a list. |
subject | string | Subject line — required unless a template supplies it. |
html | string | HTML body. Provide html, text, or both. |
text | string | Plain-text body. Recommended alongside html. |
templateId | string | Send a saved template — a user template (tpl_…) or base template (base_…); explicit subject/html/text override it. |
templateData | object | Values for the template's {{merge_tags}} (e.g. { "name": "Ann" }). HTML values are auto-escaped. |
cc / bcc | string · array | Optional additional recipients. |
replyTo | string | Optional Reply-To address. |
attachments | array | Each: { filename, url } (fetched by us) or { filename, content } (base64). |
inReplyTo | string | A threadId/Message-ID to reply in-thread. |
headers | object | Extra 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. |
metadata | object | Structured metadata kept server-side (scalars; ≤ 20 keys). Stored on the message and returned on reads, but never sent as a MIME header — so order ids and tenant keys stay off the wire. That is the difference from headers. |
sequence | string | Enroll this recipient in a sequence when the send succeeds — the name or id of an active sequence. Naming it is the consent; no trigger is involved, and an unnamed send enrols in nothing. The follow-ups inherit this message's sender, and steps can read it as {{trigger.subject}}. |
sequenceInput | object | Explicit input for that sequence, checked against its declared signature. Merged over what the message auto-injects (templateData + metadata), so an explicit value wins. |
scheduledAt | string · number | Send 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}). |
trackOpens | boolean | Open-tracking override for this send (HTML only). Omitted → the domain's default applies. |
trackClicks | boolean | Click-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.
Templates
A message body can come from three places, all interchangeable with plain
html/text. Only the first is part of the API —
the other two are conveniences the client libraries resolve before the
request goes out, so the wire always carries html/text.
A saved template, by id
Pass templateId — a template you saved (tpl_…) or one of
the base templates (base_…). It seeds
the subject, HTML and text; an explicit subject/html/text
on the request overrides it. {{merge_tags}} are substituted server-side
from templateData, with HTML values auto-escaped.
POST /v1/send {
"from": "hello@myapp.ai",
"to": "ada@example.com",
"templateId": "tpl_a878c1",
"templateData": { "name": "Ada", "invoice": "1042" }
}
A React component
The Node library takes a react element and renders it to HTML in your
process, exactly like a html string you built yourself. It needs the
optional peer dependency @react-email/render (or
@react-email/components) — install it to use this; the SDK itself stays
dependency-free.
send a React email import { MailKite } from "mailkite";
import { Welcome } from "./emails/welcome";
const mk = new MailKite(process.env.MAILKITE_API_KEY);
await mk.send({
from: "hello@myapp.ai",
to: "ada@example.com",
subject: "Welcome aboard",
react: <Welcome name="Ada" />,
});
The plaintext part is derived from the same component automatically, so a React email
still goes out as a proper multipart message — no second copy of the body to keep in
sync. Pass text yourself to override it.
createTemplate({ react: <Welcome /> }) stores the rendered result as a
MailKite template, turning a component into a tpl_… id the dashboard,
sequences, broadcasts and every other MailKite library
can send.
A local file
Keep templates next to your code: templateFile reads a local
.html, .htm or .txt file. A
welcome.txt sitting beside welcome.html becomes the
plaintext part. Available in Node, Python and PHP.
send a local template await mk.send({
from: "hello@myapp.ai",
to: "ada@example.com",
subject: "Welcome aboard",
templateFile: "./emails/welcome.html", // welcome.txt beside it → the text part
templateData: { name: "Ada" },
});
{{merge_tags}} left in a rendered component or a file are still filled
from templateData by the API, exactly as they are in a saved template —
so per-recipient values keep working however the body was authored. Because
react and templateFile produce the body locally, neither can
be combined with templateId.
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
Field Type Notes fromstring Required. An address on a verified domain, shared by every message. recipientsarray Required. 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 / textstring The base message. May contain {{merge_tags}}, filled per recipient. templateIdstring Seed the base message from a saved template (tpl_… or base_…). templateDataobject Shared default merge values; a recipient's own templateData wins key-by-key. headersobject Shared extra MIME headers; a recipient's own headers win key-by-key. replyTo / inReplyTostring Applied to every message. attachmentsarray Attached to every message. Same shape as a single send. scheduledAtstring · number Park the whole batch for later — one cancelable scheduled send per recipient. trackOpensboolean Open-tracking override for every message (HTML only). trackClicksboolean Click-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.