Sending
Send transactional email over a verified domain, and upload attachments to reference in sends. See Send API for a guided walk-through.
Auth: API key · Base URL:
https://api.mailkite.dev
Endpoints
Each row expands to its request and response schema. The address bar follows along, so any endpoint can be linked to directly.
POST /v1/send Send a message over a verified domain. Pass `templateId` (+ optional `templateData`) to send from a saved or base template.
Send a message over a verified domain. Pass `templateId` (+ optional `templateData`) to send from a saved or base template.
Request body
send-request.json| Field | Type | Notes |
|---|---|---|
| from req | string | An address on a verified domain. |
| to req | string · string[] | One recipient or a list. |
| subject | string | Required unless supplied by a template. |
| html | string | |
| text | string | |
| templateId | string | Send using a saved template — a user template (tpl_…) or a base template (base_…). Its subject/html/text seed the message; explicit subject/html/text here override them. |
| templateData | object | Values substituted into the template's {{merge_tags}} (e.g. {"name":"Ann"} fills {{name}}). HTML values are auto-escaped. |
| cc | string · string[] | |
| bcc | string · string[] | |
| replyTo | string | |
| inReplyTo | string | |
| headers | object | Extra raw MIME headers, applied after threading headers (caller wins). Use for what the structured fields can't express — e.g. `List-Unsubscribe`, a dedup/idempotency key (`X-Entity-Ref-ID`), or a tag header (`X-Tag`). Carried on both immediate and scheduled sends. |
| attachments | object[] | |
| attachments[].filename req | string | |
| attachments[].url | string | Fetch the file from this URL at send time and attach it. Any URL works; an uploadAttachment() URL is the secure, recommended choice. Prefer this over `content` for large files. |
| attachments[].content | string | Inline file bytes as base64. Simple for tiny files, but re-uploaded on every send — use `url` (see uploadAttachment) for anything large. |
| attachments[].contentType | string | MIME type; defaults to application/octet-stream. |
| scheduledAt | string · number | Send later: ISO 8601, simple relative natural language ("in 2 hours"), or a ms-epoch. A future time parks the message with the scheduler — the response carries an ssnd_… id and status "scheduled", cancelable via DELETE /v1/scheduled/{id}. Omitted or past sends now. |
| trackOpens | boolean | Open-tracking override for this send (HTML only). Omitted → the from-domain's default applies. |
| trackClicks | boolean | Click-tracking override for this send (HTML only): http(s) links are rewritten to a signed redirect that records the click, then 302s to the destination. Omitted → the from-domain's default applies. |
The smallest body that makes this call — every other field is optional.
{
"from": "hello@app.mailkite.dev",
"to": "ada@example.com",
"subject": "Hi",
"text": "It works."
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/send-request.json",
"title": "Send request body",
"type": "object",
"required": [
"from",
"to"
],
"additionalProperties": false,
"properties": {
"from": {
"type": "string",
"description": "An address on a verified domain."
},
"to": {
"description": "One recipient or a list.",
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
},
"minItems": 1
}
]
},
"subject": {
"type": "string",
"description": "Required unless supplied by a template."
},
"html": {
"type": "string"
},
"text": {
"type": "string"
},
"templateId": {
"type": "string",
"description": "Send using a saved template — a user template (tpl_…) or a base template (base_…). Its subject/html/text seed the message; explicit subject/html/text here override them."
},
"templateData": {
"type": "object",
"description": "Values substituted into the template's {{merge_tags}} (e.g. {\"name\":\"Ann\"} fills {{name}}). HTML values are auto-escaped.",
"additionalProperties": {
"type": [
"string",
"number",
"boolean",
"null"
]
}
},
"cc": {
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
},
"bcc": {
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
},
"replyTo": {
"type": "string"
},
"inReplyTo": {
"type": "string"
},
"headers": {
"type": "object",
"description": "Extra raw MIME headers, applied after threading headers (caller wins). Use for what the structured fields can't express — e.g. `List-Unsubscribe`, a dedup/idempotency key (`X-Entity-Ref-ID`), or a tag header (`X-Tag`). Carried on both immediate and scheduled sends.",
"additionalProperties": {
"type": "string"
}
},
"attachments": {
"type": "array",
"items": {
"type": "object",
"required": [
"filename"
],
"properties": {
"filename": {
"type": "string"
},
"url": {
"type": "string",
"description": "Fetch the file from this URL at send time and attach it. Any URL works; an uploadAttachment() URL is the secure, recommended choice. Prefer this over `content` for large files."
},
"content": {
"type": "string",
"description": "Inline file bytes as base64. Simple for tiny files, but re-uploaded on every send — use `url` (see uploadAttachment) for anything large."
},
"contentType": {
"type": "string",
"description": "MIME type; defaults to application/octet-stream."
}
}
}
},
"scheduledAt": {
"type": [
"string",
"number"
],
"description": "Send later: ISO 8601, simple relative natural language (\"in 2 hours\"), or a ms-epoch. A future time parks the message with the scheduler — the response carries an ssnd_… id and status \"scheduled\", cancelable via DELETE /v1/scheduled/{id}. Omitted or past sends now."
},
"trackOpens": {
"type": "boolean",
"description": "Open-tracking override for this send (HTML only). Omitted → the from-domain's default applies."
},
"trackClicks": {
"type": "boolean",
"description": "Click-tracking override for this send (HTML only): http(s) links are rewritten to a signed redirect that records the click, then 302s to the destination. Omitted → the from-domain's default applies."
}
}
} Response body
send-response.json| Field | Type | Notes |
|---|---|---|
| id req | string | |
| status req | string | |
| scheduledAt | number | Present on scheduled sends: when the message will fire, in ms epoch. |
An actual response, recorded from the conformance suite.
{
"id": "msg_minimal",
"status": "queued"
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/send-response.json",
"title": "Send response body",
"type": "object",
"required": [
"id",
"status"
],
"properties": {
"id": {
"type": "string"
},
"status": {
"type": "string"
},
"scheduledAt": {
"type": "number",
"description": "Present on scheduled sends: when the message will fire, in ms epoch."
}
}
} POST /v1/send/batch Send 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 per-recipient `templateData` and `headers` merged over the shared ones. Every message passes the same gates as send() and gets its own id; the response reports each recipient's outcome in order, so a batch can partially succeed. Pass `scheduledAt` to park the whole batch for later (one cancelable ssnd_… per recipient).
Send 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 per-recipient `templateData` and `headers` merged over the shared ones. Every message passes the same gates as send() and gets its own id; the response reports each recipient's outcome in order, so a batch can partially succeed. Pass `scheduledAt` to park the whole batch for later (one cancelable ssnd_… per recipient).
Request body
batch-send-request.jsonSend one personalized message per recipient in a single call. Shared fields (subject/html/text/templateId/templateData/headers/…) form the base message; each recipients[] entry produces an independent message to exactly one address, with its own templateData and headers merged over the shared ones (per-recipient wins). Each message passes the same gates as a single send() and gets its own id — recipients never see each other.
| Field | Type | Notes |
|---|---|---|
| from req | string | An address on a verified domain. Shared by every message in the batch. |
| recipients req | object[] | One entry per message. Order is preserved in the response's results[]. |
| recipients[].to req | string | Exactly one recipient address (bare or `Name <addr>`). This message is sent to this address only. |
| recipients[].templateData | object | Per-recipient merge values, merged over the shared templateData (this wins on conflicts). Fills {{merge_tags}} in the subject/html/text for this recipient's message. |
| recipients[].headers | object | Per-recipient extra raw MIME headers, merged over the shared headers (this wins on conflicts). |
| subject | string | Required unless supplied by a template. May contain {{merge_tags}}, filled per recipient. |
| html | string | May contain {{merge_tags}}, filled per recipient. |
| text | string | May contain {{merge_tags}}, filled per recipient. |
| templateId | string | Send using a saved template — a user template (tpl_…) or a base template (base_…). Its subject/html/text seed every message; explicit subject/html/text here override them. |
| templateData | object | Shared default merge values for every recipient; a recipient's own templateData overrides these key-by-key. HTML values are auto-escaped. |
| headers | object | Shared extra raw MIME headers for every message, applied after threading headers (caller wins); a recipient's own headers override these key-by-key. |
| replyTo | string | |
| inReplyTo | string | Thread every message under this Message-ID. |
| attachments | object[] | Attached to every message in the batch. Same shape as send()'s attachments. |
| attachments[].filename req | string | |
| attachments[].url | string | Fetch the file from this URL at send time and attach it. Any URL works; an uploadAttachment() URL is the secure, recommended choice. Prefer this over `content` for large files. |
| attachments[].content | string | Inline file bytes as base64. Simple for tiny files, but re-uploaded on every send — use `url` (see uploadAttachment) for anything large. |
| attachments[].contentType | string | MIME type; defaults to application/octet-stream. |
| scheduledAt | string · number | Send later: ISO 8601, simple relative natural language ("in 2 hours"), or a ms-epoch. A future time parks every message with the scheduler (each gets its own ssnd_… id, individually cancelable); omitted or past sends now. |
| trackOpens | boolean | Open-tracking override for every message in the batch (HTML only). Omitted → the from-domain's default applies. |
| trackClicks | boolean | Click-tracking override for every message in the batch (HTML only): http(s) links are rewritten to a signed redirect that records the click, then 302s to the destination. Omitted → the from-domain's default applies. |
The smallest body that makes this call — every other field is optional.
{
"from": "hello@app.mailkite.dev",
"subject": "Your {{plan}} invoice",
"html": "<p>Hi {{name}}, your {{plan}} invoice is attached.</p>",
"templateData": {
"plan": "Pro"
},
"recipients": [
{
"to": "Ada Lovelace <ada@example.com>",
"templateData": {
"name": "Ada"
}
},
{
"to": "grace@example.com",
"templateData": {
"name": "Grace",
"plan": "Team"
},
"headers": {
"X-Entity-Ref-ID": "inv-1043"
}
}
]
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/batch-send-request.json",
"title": "Batch send request body",
"description": "Send one personalized message per recipient in a single call. Shared fields (subject/html/text/templateId/templateData/headers/…) form the base message; each recipients[] entry produces an independent message to exactly one address, with its own templateData and headers merged over the shared ones (per-recipient wins). Each message passes the same gates as a single send() and gets its own id — recipients never see each other.",
"type": "object",
"required": [
"from",
"recipients"
],
"additionalProperties": false,
"properties": {
"from": {
"type": "string",
"description": "An address on a verified domain. Shared by every message in the batch."
},
"recipients": {
"type": "array",
"description": "One entry per message. Order is preserved in the response's results[].",
"minItems": 1,
"maxItems": 50,
"items": {
"type": "object",
"required": [
"to"
],
"additionalProperties": false,
"properties": {
"to": {
"type": "string",
"description": "Exactly one recipient address (bare or `Name <addr>`). This message is sent to this address only.",
"examples": [
"Ada Lovelace <ada@example.com>"
]
},
"templateData": {
"type": "object",
"description": "Per-recipient merge values, merged over the shared templateData (this wins on conflicts). Fills {{merge_tags}} in the subject/html/text for this recipient's message.",
"additionalProperties": {
"type": [
"string",
"number",
"boolean",
"null"
]
}
},
"headers": {
"type": "object",
"description": "Per-recipient extra raw MIME headers, merged over the shared headers (this wins on conflicts).",
"additionalProperties": {
"type": "string"
}
}
}
}
},
"subject": {
"type": "string",
"description": "Required unless supplied by a template. May contain {{merge_tags}}, filled per recipient."
},
"html": {
"type": "string",
"description": "May contain {{merge_tags}}, filled per recipient."
},
"text": {
"type": "string",
"description": "May contain {{merge_tags}}, filled per recipient."
},
"templateId": {
"type": "string",
"description": "Send using a saved template — a user template (tpl_…) or a base template (base_…). Its subject/html/text seed every message; explicit subject/html/text here override them."
},
"templateData": {
"type": "object",
"description": "Shared default merge values for every recipient; a recipient's own templateData overrides these key-by-key. HTML values are auto-escaped.",
"additionalProperties": {
"type": [
"string",
"number",
"boolean",
"null"
]
}
},
"headers": {
"type": "object",
"description": "Shared extra raw MIME headers for every message, applied after threading headers (caller wins); a recipient's own headers override these key-by-key.",
"additionalProperties": {
"type": "string"
}
},
"replyTo": {
"type": "string"
},
"inReplyTo": {
"type": "string",
"description": "Thread every message under this Message-ID."
},
"attachments": {
"type": "array",
"description": "Attached to every message in the batch. Same shape as send()'s attachments.",
"items": {
"type": "object",
"required": [
"filename"
],
"properties": {
"filename": {
"type": "string"
},
"url": {
"type": "string",
"description": "Fetch the file from this URL at send time and attach it. Any URL works; an uploadAttachment() URL is the secure, recommended choice. Prefer this over `content` for large files."
},
"content": {
"type": "string",
"description": "Inline file bytes as base64. Simple for tiny files, but re-uploaded on every send — use `url` (see uploadAttachment) for anything large."
},
"contentType": {
"type": "string",
"description": "MIME type; defaults to application/octet-stream."
}
}
}
},
"scheduledAt": {
"type": [
"string",
"number"
],
"description": "Send later: ISO 8601, simple relative natural language (\"in 2 hours\"), or a ms-epoch. A future time parks every message with the scheduler (each gets its own ssnd_… id, individually cancelable); omitted or past sends now."
},
"trackOpens": {
"type": "boolean",
"description": "Open-tracking override for every message in the batch (HTML only). Omitted → the from-domain's default applies."
},
"trackClicks": {
"type": "boolean",
"description": "Click-tracking override for every message in the batch (HTML only): http(s) links are rewritten to a signed redirect that records the click, then 302s to the destination. Omitted → the from-domain's default applies."
}
}
} Response body
batch-send-response.jsonPer-recipient outcomes, in the same order as the request's recipients[]. The call returns 200 whenever the batch itself was accepted — check each result's status: a batch can partially succeed (e.g. one address suppressed, the rest sent). Every non-failed result carries its own message id.
| Field | Type | Notes |
|---|---|---|
| results req | object[] | One entry per recipients[] entry, same order. |
| results[].to req | string | The recipient this result is for (echoed from the request). |
| results[].id | string | The message id (msg_…), or the scheduled-send id (ssnd_…) when parked for later. Absent on failure. |
| results[].status req | string | sent | scheduled | failed. |
| results[].error | string | Human-readable reason, present only when status is failed. |
| results[].code | string | Machine-readable reason (e.g. recipient_suppressed, daily_limit), present only when status is failed. |
| sent req | integer | Count of results with status sent. |
| scheduled req | integer | Count of results with status scheduled. |
| failed req | integer | Count of results with status failed. |
An actual response, recorded from the conformance suite.
{
"results": [
{
"to": "Ada Lovelace <ada@example.com>",
"id": "msg_batch1",
"status": "sent"
},
{
"to": "grace@example.com",
"id": "msg_batch2",
"status": "sent"
}
],
"sent": 2,
"scheduled": 0,
"failed": 0
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/batch-send-response.json",
"title": "Batch send response body",
"description": "Per-recipient outcomes, in the same order as the request's recipients[]. The call returns 200 whenever the batch itself was accepted — check each result's status: a batch can partially succeed (e.g. one address suppressed, the rest sent). Every non-failed result carries its own message id.",
"type": "object",
"required": [
"results",
"sent",
"scheduled",
"failed"
],
"properties": {
"results": {
"type": "array",
"description": "One entry per recipients[] entry, same order.",
"items": {
"type": "object",
"required": [
"to",
"status"
],
"properties": {
"to": {
"type": "string",
"description": "The recipient this result is for (echoed from the request)."
},
"id": {
"type": "string",
"description": "The message id (msg_…), or the scheduled-send id (ssnd_…) when parked for later. Absent on failure."
},
"status": {
"type": "string",
"description": "sent | scheduled | failed.",
"examples": [
"sent"
]
},
"error": {
"type": "string",
"description": "Human-readable reason, present only when status is failed."
},
"code": {
"type": "string",
"description": "Machine-readable reason (e.g. recipient_suppressed, daily_limit), present only when status is failed."
}
}
}
},
"sent": {
"type": "integer",
"description": "Count of results with status sent."
},
"scheduled": {
"type": "integer",
"description": "Count of results with status scheduled."
},
"failed": {
"type": "integer",
"description": "Count of results with status failed."
}
}
} POST /v1/attachments Upload a file to MailKite storage and get back a secure, time-limited URL. Reference the returned `url` as an attachment in send() (`{ filename, url }`) or link it inline in your HTML — instead of base64-inlining large files on every send. Give the file ONE of four ways: a local `path` (read and streamed as raw bytes by the CLI/SDK/local MCP), a remote `url` (MailKite fetches and re-hosts it), base64 `content`, or — over raw HTTP — the file bytes as the POST body with `?filename=`. `retentionDays` (7/30/90/365, default 7) sets how long the file and URL live.
Upload a file to MailKite storage and get back a secure, time-limited URL. Reference the returned `url` as an attachment in send() (`{ filename, url }`) or link it inline in your HTML — instead of base64-inlining large files on every send. Give the file ONE of four ways: a local `path` (read and streamed as raw bytes by the CLI/SDK/local MCP), a remote `url` (MailKite fetches and re-hosts it), base64 `content`, or — over raw HTTP — the file bytes as the POST body with `?filename=`. `retentionDays` (7/30/90/365, default 7) sets how long the file and URL live.
Request body
upload-attachment-request.jsonProvide the file ONE of four ways: `path` (a local file the SDK/CLI/MCP reads and streams as raw bytes), `url` (a remote file MailKite fetches and re-hosts), `content` (base64-encoded bytes), or — at the HTTP layer — the raw file bytes as the POST body with `?filename=`. `path`/raw-bytes go up as a real binary upload (like an S3/R2 PUT); `url`/`content` go up as JSON.
| Field | Type | Notes |
|---|---|---|
| filename | string | The file's name, e.g. "invoice.pdf". Shown to recipients on download. Optional when it can be derived from `path` or `url`. |
| path | string | Local filesystem path to the file. Read client-side by the CLI, SDKs, and the local MCP server, then uploaded as raw bytes. Not available on the hosted MCP (no filesystem). |
| url | string | A remote http(s) URL. MailKite fetches it and re-hosts the bytes under your account. Max 25 MB. |
| content | string | The file bytes, base64-encoded. The lowest-common-denominator fallback when you can't send a path, URL, or raw bytes. |
| contentType | string | MIME type, e.g. "application/pdf". Defaults to application/octet-stream (or is inferred from the file extension / fetched response). |
| retentionDays | 7 · 30 · 90 · 365 | How long the file (and its signed URL) stays valid. One of 7, 30, 90, 365. Defaults to 7. |
The smallest body that makes this call — every other field is optional.
{
"filename": "po.pdf",
"content": "JVBERi0xLjQK",
"contentType": "application/pdf"
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/upload-attachment-request.json",
"title": "Upload attachment request body",
"description": "Provide the file ONE of four ways: `path` (a local file the SDK/CLI/MCP reads and streams as raw bytes), `url` (a remote file MailKite fetches and re-hosts), `content` (base64-encoded bytes), or — at the HTTP layer — the raw file bytes as the POST body with `?filename=`. `path`/raw-bytes go up as a real binary upload (like an S3/R2 PUT); `url`/`content` go up as JSON.",
"type": "object",
"additionalProperties": false,
"properties": {
"filename": {
"type": "string",
"description": "The file's name, e.g. \"invoice.pdf\". Shown to recipients on download. Optional when it can be derived from `path` or `url`."
},
"path": {
"type": "string",
"description": "Local filesystem path to the file. Read client-side by the CLI, SDKs, and the local MCP server, then uploaded as raw bytes. Not available on the hosted MCP (no filesystem)."
},
"url": {
"type": "string",
"description": "A remote http(s) URL. MailKite fetches it and re-hosts the bytes under your account. Max 25 MB."
},
"content": {
"type": "string",
"description": "The file bytes, base64-encoded. The lowest-common-denominator fallback when you can't send a path, URL, or raw bytes."
},
"contentType": {
"type": "string",
"description": "MIME type, e.g. \"application/pdf\". Defaults to application/octet-stream (or is inferred from the file extension / fetched response)."
},
"retentionDays": {
"type": "integer",
"enum": [
7,
30,
90,
365
],
"description": "How long the file (and its signed URL) stays valid. One of 7, 30, 90, 365. Defaults to 7."
}
}
} Response body
upload-attachment-response.json| Field | Type | Notes |
|---|---|---|
| id | string | Storage key for the uploaded file. |
| url | string | Secure, time-limited URL. Pass it as an attachment's `url` in send(), or link it inline in your HTML. |
| filename | string | |
| contentType | string | |
| size | integer | Stored size in bytes. |
| expiresAt | string | ISO-8601 timestamp when the file and URL expire. |
An actual response, recorded from the conformance suite.
{
"id": "7d/usr_demo/0a1b2c3d/po.pdf",
"url": "https://api.mailkite.dev/up/7d/usr_demo/0a1b2c3d/po.pdf?exp=1799999999&sig=deadbeef",
"filename": "po.pdf",
"contentType": "application/pdf",
"size": 9,
"expiresAt": "2026-07-02T00:00:00.000Z"
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/upload-attachment-response.json",
"title": "Upload attachment response",
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Storage key for the uploaded file."
},
"url": {
"type": "string",
"description": "Secure, time-limited URL. Pass it as an attachment's `url` in send(), or link it inline in your HTML."
},
"filename": {
"type": "string"
},
"contentType": {
"type": "string"
},
"size": {
"type": "integer",
"description": "Stored size in bytes."
},
"expiresAt": {
"type": "string",
"description": "ISO-8601 timestamp when the file and URL expire."
}
}
}