Emails
Send one email, or up to 100 in a batch, and read what happened to each one. Every email is either transactional (one person's action triggered it: resets, receipts, invites) or marketing (updates, tips, onboarding). Marketing email always carries an unsubscribe link and respects topic opt-outs and daily limits; transactional email is never blocked by them.
With a test key nothing leaves Mailpark: the email is rendered, logged as delivered and put in the test inbox.
type Message = {
id: string; // msg_…
to: string; from: string; subject: string;
type: "transactional" | "marketing";
template: string | null; template_version: number | null;
status: "queued" | "sent" | "delivered" | "bounced" | "complained"
| "suppressed" | "failed" | "held" | "cancelled";
environment: "test" | "live";
contact_id: string | null; broadcast_id: string | null; enrollment_id: string | null;
error_reason: string | null; // plain language
opened: boolean; clicked: boolean;
opened_at: string | null;
opened_likely_automatic: boolean; // only machine pre-loads so far (Apple MPP, scanners)
created_at: string; updated_at: string;
timeline?: { at: string; kind: string; detail: string | null }[]; // GET /v1/emails/{id}
html?: string; // GET /v1/emails/{id}
};Send an email
/v1/emailsSends one email to one recipient, from a template or from inline HTML. Returns 202 straight away; the email is sent from a queue.
emails:sendBody
tostringrequiredOne email address.
templatestringA template key, e.g.
password-reset. Or sendsubjectandhtmlinstead.subjectstringRequired without a template. Overrides the template's subject.
htmlstringInline HTML, with
{{variables}}, when you don't use a template.dataobjectValues for the template's
{{variables}}.typestringtransactionalormarketing. Defaults to the template's type, elsetransactional.topicstringMarketing only: the topic a recipient can unsubscribe from.
from_namestringDisplay name, e.g.
Jane from Acme.reply_tostringWhere replies go.
- Set an
Idempotency-Keyso a retry never sends twice. The SDK sets one automatically; pass your own (e.g.password-reset:<user_id>:<token>) to make retries safe across processes. - A suppressed or unsubscribed recipient is not an error: you get
202withstatus: "suppressed"and anerror_reason, and the message is logged. - Test keys finish immediately with
delivered. Addresses containingbounce,softorcomplainsimulate those outcomes. - Never loop single sends to email many people. Draft a broadcast instead.
curl -X POST https://api.mailpark.io/v1/emails \
-H "Authorization: Bearer $MAILPARK_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: password-reset:user_123:abc123" \
-d '{
"to": "jane@example.com",
"template": "password-reset",
"data": {
"reset_url": "https://acme.dev/reset/abc123"
}
}'{
"id": "msg_01JB4Q7Z2K",
"status": "queued",
"environment": "live",
"next_steps": [
"Check delivery with GET /v1/emails/msg_01JB4Q7Z2K/explain."
]
}Send a batch
/v1/emails/batchUp to 100 emails in one request, each with its own recipient and data. One bad email doesn't fail the batch: it comes back with status: "rejected" and an error.
emails:sendBody
emailsobject[]requiredUp to 100 emails, each shaped like the body of Send an email.
curl -X POST https://api.mailpark.io/v1/emails/batch \
-H "Authorization: Bearer $MAILPARK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"emails": [
{
"to": "jane@example.com",
"template": "receipt",
"data": {
"amount": "$29.00"
}
},
{
"to": "sam@example.com",
"template": "receipt",
"data": {
"amount": "$99.00"
}
}
]
}'{
"data": [
{
"id": "msg_01JB4Q8A1M",
"status": "queued"
},
{
"id": "msg_01JB4Q8A1N",
"status": "queued"
}
]
}List emails
/v1/emailsThe send log, newest first. Filter by recipient, status, type, day or broadcast.
logs:readQuery parameters
tostringRecipient address.
statusstringqueued,sent,delivered,bounced,complained,suppressed,failed,heldorcancelled.typestringtransactionalormarketing.sincestringISO-8601 time; only emails created after it.
daystringYYYY-MM-DD, in the account's timezone.broadcast_idstringOnly emails from this broadcast.
openedbooleantrueorfalse.limitintegerHow many to return, 1–100. Default 50.
cursorstringThe
next_cursorfrom the previous page.
curl 'https://api.mailpark.io/v1/emails?to=jane%40example.com' \
-H "Authorization: Bearer $MAILPARK_API_KEY"{
"data": [
{
"id": "msg_01JB4Q7Z2K",
"to": "jane@example.com",
"from": "Acme <noreply@mail.acme.dev>",
"subject": "Reset your Acme password",
"type": "transactional",
"template": "password-reset",
"template_version": 3,
"status": "delivered",
"environment": "live",
"contact_id": "con_7Yt1",
"broadcast_id": null,
"enrollment_id": null,
"error_reason": null,
"opened": true,
"clicked": false,
"opened_at": "2026-10-22T10:04:40Z",
"opened_likely_automatic": false,
"created_at": "2026-10-22T10:02:11Z",
"updated_at": "2026-10-22T10:02:14Z"
}
],
"next_cursor": null
}Get an email
/v1/emails/{id}One email with its full timeline (queued, sent, delivered, opened…) and the rendered html.
logs:readPath parameters
idstringrequiredThe message id,
msg_….
curl https://api.mailpark.io/v1/emails/msg_01JB4Q7Z2K \
-H "Authorization: Bearer $MAILPARK_API_KEY"{
"id": "msg_01JB4Q7Z2K",
"to": "jane@example.com",
"from": "Acme <noreply@mail.acme.dev>",
"subject": "Reset your Acme password",
"type": "transactional",
"template": "password-reset",
"template_version": 3,
"status": "delivered",
"environment": "live",
"contact_id": "con_7Yt1",
"broadcast_id": null,
"enrollment_id": null,
"error_reason": null,
"opened": true,
"clicked": false,
"opened_at": "2026-10-22T10:04:40Z",
"opened_likely_automatic": false,
"created_at": "2026-10-22T10:02:11Z",
"updated_at": "2026-10-22T10:02:14Z",
"timeline": [
{
"at": "2026-10-22T10:02:11Z",
"kind": "created",
"detail": null
},
{
"at": "2026-10-22T10:02:11Z",
"kind": "queued",
"detail": "Transactional lane"
},
{
"at": "2026-10-22T10:02:12Z",
"kind": "sent",
"detail": "From mail.acme.dev"
},
{
"at": "2026-10-22T10:02:14Z",
"kind": "delivered",
"detail": "Accepted by gmail-smtp-in.l.google.com"
},
{
"at": "2026-10-22T10:04:40Z",
"kind": "opened",
"detail": null,
"likely_automatic": false
}
],
"html": "<!doctype html><html>…</html>"
}Explain a delivery
/v1/emails/{id}/explainAnswers "why didn't they get it?" in plain language: a summary, a verdict, each stage the email went through, and what to fix.
logs:readPath parameters
idstringrequiredThe message id,
msg_….
verdictisdelivered,in_progress,not_deliveredorblocked. Each step'sstatusisok,warning,error,pendingorskipped.
curl https://api.mailpark.io/v1/emails/msg_01JB4Q7Z2K/explain \
-H "Authorization: Bearer $MAILPARK_API_KEY"{
"message_id": "msg_01JB4Q7Z2K",
"summary": "Jane's password reset email was delivered to Gmail at 10:02 and opened at 10:04.",
"verdict": "delivered",
"steps": [
{
"label": "Your app asked Mailpark to send it",
"status": "ok",
"detail": "POST /v1/emails at 10:02:11 with template password-reset."
},
{
"label": "Recipient allowed",
"status": "ok",
"detail": "jane@example.com is not suppressed or unsubscribed."
},
{
"label": "Sent from your domain",
"status": "ok",
"detail": "mail.acme.dev, signed with DKIM."
},
{
"label": "Accepted by the inbox provider",
"status": "ok",
"detail": "Gmail accepted it at 10:02:14."
},
{
"label": "Opened",
"status": "ok",
"detail": "First opened at 10:04:40."
}
],
"fix": null,
"next_steps": []
}