Messages API
10 min read
The Messages API lets you send transactional emails, track delivery status, and manage scheduled messages.
Send email
Send a single email message.
POST /v1/messages/send
Request body
| Field | Type | Required | Description |
|---|---|---|---|
from |
string | Yes | Sender address (must match verified domain) |
to |
string/array | Yes | Recipient address(es) |
subject |
string | Yes | Email subject line |
html |
string | * | HTML body (* at least one of html/text required) |
text |
string | * | Plain text body |
cc |
array | No | CC recipients |
bcc |
array | No | BCC recipients |
headers |
object | No | Custom email headers |
| cc, bcc | Arrays of email addresses. to, cc and bcc together: max 50 recipients. Every recipient counts towards the daily and monthly limits. |
| attachments | array | No | File attachments |
| template_id | string | No | Template ID to use |
| template_data | object | No | Template variables |
| tags | array | No | Tags for analytics |
| metadata | object | No | Custom metadata |
| tracking | object \| boolean | No | Tracking settings (false disables both) |
| transactional | boolean | No | Declares transactional mail — same effect as disabling both trackings |
| scheduled_at | string | No | ISO 8601 datetime for scheduled send |
| optimize_send_time | boolean | No | Optimize delivery timing |
Example request
curl -X POST https://api.mailingapi.com/v1/messages/send \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "Your Company <hello@yourdomain.com>",
"to": "John Doe <john@example.com>",
"subject": "Your order has shipped",
"html": "<h1>Order Shipped</h1><p>Hi {{name}}, your order #{{order_id}} is on the way!</p>",
"text": "Order Shipped\n\nHi {{name}}, your order #{{order_id}} is on the way!",
"template_data": {
"name": "John",
"order_id": "12345"
},
"tags": ["transactional", "shipping"],
"metadata": {
"user_id": "usr_123",
"order_id": "ord_456"
}
}'
Response
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"message_id": "<abc123@mailingapi.com>",
"from": "hello@yourdomain.com",
"to": "john@example.com",
"subject": "Your order has shipped",
"status": "queued",
"tags": ["transactional", "shipping"],
"metadata": {"user_id": "usr_123", "order_id": "ord_456"},
"created_at": "2024-01-15T10:30:00Z"
}
}
Attachments
{
"attachments": [
{
"filename": "invoice.pdf",
"content": "base64-encoded-content",
"content_type": "application/pdf"
},
{
"filename": "logo.png",
"content": "base64-encoded-content",
"content_type": "image/png",
"content_id": "logo"
}
]
}
Use content_id for inline images, then reference with <img src="cid:logo">.
Each attachment needs filename and content (Base64). An attachment that is missing either field, or exceeds the limits below, is rejected with 422 validation_failed — it is never silently skipped.
Limits
These limits apply to POST /v1/messages/send and to every message in a batch. Exceeding any of them returns 422 validation_failed with details for the offending field — nothing is silently dropped.
| Field | Limit |
|---|---|
subject |
1–998 characters |
tags |
Max 10 tags, each 1–64 characters |
metadata |
JSON object, max 50 keys, max 2 KB when serialized as JSON |
headers |
Max 20 custom headers. Name: letters, digits and hyphens only, max 128 characters. Value: string, max 1000 characters, no line breaks (CR/LF) |
attachments |
Max 10 attachments, each with filename and content (Base64), max 10 MB per file after decoding |
scheduled_at |
At most 7 days ahead |
Reserved headers are set automatically and are rejected if you pass them: From, To, Cc, Bcc, Subject, Date, MIME-Version, DKIM-Signature, Return-Path, Sender, Content-Type, Content-Transfer-Encoding. Reply-To is allowed. Message-ID is accepted but ignored: MailingAPI always assigns its own value. For threaded replies use In-Reply-To and References.
The whole request body may be at most 25 MB. Base64 inflates files by about 33%, so in practice about 18 MB of attachments fit in one request (for example one 10 MB file plus a few small ones). A larger body is rejected with HTTP 413.
Example of a limit violation:
{
"error": {
"code": "validation_failed",
"message": "Błąd walidacji",
"details": {
"tags": ["should have at most 10 item(s)"]
}
}
}
Tracking options
Control open and click tracking per message:
{
"tracking": {
"opens": true,
"clicks": true
}
}
| Field | Type | Default | Description |
|---|---|---|---|
opens |
boolean |
true |
Track email opens (inserts a tracking pixel) |
clicks |
boolean |
true |
Track link clicks (rewrites URLs through tracking domain) |
When both opens and clicks are set to false, the email is treated as transactional:
- No tracking pixel is inserted
- Links are not rewritten
-
List-Unsubscribeheader is not added (prevents Gmail from classifying the email as promotional/bulk)
Best practice: Disable tracking for transactional notifications (password resets, order confirmations, system alerts) to improve inbox placement. Keep tracking enabled for marketing and newsletter emails.
{
"from": "alerts@yourdomain.com",
"to": "user@example.com",
"subject": "New login detected",
"html": "<p>A new login was detected on your account.</p>",
"tracking": {
"opens": false,
"clicks": false
}
}
Shorthands
tracking also accepts a plain boolean, and transactional states the intent
directly. All three of these are equivalent:
{ "tracking": { "opens": false, "clicks": false } }
{ "tracking": false }
{ "transactional": true }
transactional takes precedence over tracking — it is the more specific
declaration, so {"transactional": true, "tracking": {"opens": true}} still
sends with tracking off.
Anything else is an error. A
trackingvalue that is neither an object nor a boolean (for example"tracking": "off") returns 400. Until 2026-07 such values were silently ignored, so a caller could believe tracking was off while the pixel kept shipping. If you integrated before that date, check that you are sending one of the three shapes above.
Why this matters beyond deliverability
Click tracking rewrites every href to https://api.mailingapi.com/c/<id>.
If your links carry credentials — invitation tokens, magic links, one-time
password-reset URLs — those credentials travel through, and are logged by, this
service. An outage of the redirect also breaks links already delivered. For
that class of mail, turn tracking off rather than treating it as a spam-folder
tuning knob.
Batch send
Send multiple personalized emails in one request.
POST /v1/messages/batch
Using recipient variables
curl -X POST https://api.mailingapi.com/v1/messages/batch \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "hello@yourdomain.com",
"subject": "Welcome, {{name}}!",
"html": "<p>Hi {{name}}, your code is {{code}}.</p>",
"recipient_variables": {
"alice@example.com": {"name": "Alice", "code": "ABC123"},
"bob@example.com": {"name": "Bob", "code": "XYZ789"}
}
}'
Using messages array
curl -X POST https://api.mailingapi.com/v1/messages/batch \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{
"from": "hello@yourdomain.com",
"to": "alice@example.com",
"subject": "Your invoice",
"html": "<p>Invoice #001 attached.</p>"
},
{
"from": "hello@yourdomain.com",
"to": "bob@example.com",
"subject": "Your receipt",
"html": "<p>Receipt #002 attached.</p>"
}
]
}'
Response
The batch endpoint returns HTTP 202 whenever the request itself is valid — even if some or all messages were rejected. Always check data.errors.
| Field | Description |
|---|---|
data.total |
Number of messages in the request |
data.queued |
Number of messages accepted into the send queue |
data.errors |
Number (integer count, not a list) of rejected messages |
data.messages |
One entry per message, in request order, each with its index |
Each entry has status equal to "queued" or "error":
-
Queued:
{"index": 0, "status": "queued", "id": "<uuid>", "to": "a@example.com"} -
Rejected:
{"index": 1, "status": "error", "to": "b@example.com", "error": {...}}— rejected entries have noid.error.codeis one ofvalidation_failed,forbidden(sender domain),bad_requestorsend_failed;error.detailsis present only forvalidation_failed.
Example with two accepted messages and one rejected:
{
"data": {
"total": 3,
"queued": 2,
"errors": 1,
"messages": [
{"index": 0, "status": "queued", "id": "550e8400-e29b-41d4-a716-446655440000", "to": "alice@example.com"},
{
"index": 1,
"status": "error",
"to": "bob@example.com",
"error": {
"code": "validation_failed",
"message": "subject: can't be blank",
"details": {"subject": ["can't be blank"]}
}
},
{"index": 2, "status": "queued", "id": "550e8400-e29b-41d4-a716-446655440002", "to": "carol@example.com"}
]
}
}
Batch limits
-
Maximum 1,000 messages per request (or 1,000 recipients in
recipient_variablesmode). Exceeding the limit rejects the whole request with400bad_request. - Each message is validated and processed independently — a rejected message does not block the others.
-
Every message is subject to the same field limits as
/send. -
Idempotency-Keyapplies to the whole batch as one unit — see Idempotency.
Get message
Retrieve details of a sent message.
GET /v1/messages/{message_id}
Example request
curl https://api.mailingapi.com/v1/messages/msg_1a2b3c4d5e6f \
-H "Authorization: Bearer $API_KEY"
Response
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"message_id": "<abc123@mailingapi.com>",
"from": "hello@yourdomain.com",
"to": "john@example.com",
"subject": "Your order has shipped",
"status": "delivered",
"sent_at": "2024-01-15T10:30:02Z",
"delivered_at": "2024-01-15T10:30:05Z",
"opened_at": "2024-01-15T11:45:00Z",
"tags": ["transactional", "shipping"],
"metadata": {"user_id": "usr_123", "order_id": "ord_456"},
"created_at": "2024-01-15T10:30:00Z"
}
}
Message statuses
| Status | Description |
|---|---|
pending |
Message created, not yet queued |
queued |
Message accepted, waiting to send |
sent |
Message sent to recipient server |
delivered |
Message accepted by recipient server |
bounced |
Message rejected by recipient server |
failed |
Permanent failure |
List messages
Retrieve a list of sent messages.
GET /v1/messages
Query parameters
| Parameter | Type | Description |
|---|---|---|
limit |
integer | Results per page (default: 50, max: 100) |
offset |
integer | Number of results to skip (default: 0) |
status |
string | Filter by status |
to |
string | Filter by recipient email |
since |
string | Start date (ISO 8601) |
until |
string | End date (ISO 8601) |
Example request
curl "https://api.mailingapi.com/v1/messages?status=delivered&since=2024-01-01T00:00:00Z&limit=50" \
-H "Authorization: Bearer $API_KEY"
Response
{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"from": "hello@yourdomain.com",
"to": "john@example.com",
"subject": "Your order has shipped",
"status": "delivered",
"created_at": "2024-01-15T10:30:00Z"
}
],
"meta": {
"count": 1,
"total": 1250,
"limit": 50,
"offset": 0,
"has_more": true
}
}
Analyze message
Validate an email before sending. Returns spam score and potential issues.
POST /v1/messages/analyze
Example request
curl -X POST https://api.mailingapi.com/v1/messages/analyze \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "hello@yourdomain.com",
"to": "user@example.com",
"subject": "Test email",
"html": "<p>Content here</p>"
}'
Response
{
"data": {
"score": 85,
"grade": "good",
"issues_count": 1,
"critical_count": 0,
"high_count": 0,
"medium_count": 1,
"low_count": 0,
"can_send": true,
"recommendations": [
"Consider adding a text version for better deliverability"
],
"issues": [
{
"type": "content",
"subtype": "missing_text",
"severity": "medium",
"detail": "No plain text version",
"penalty": 5
}
]
}
}
Schedule message
Schedule an email for future delivery. Use scheduled_at in send request:
curl -X POST https://api.mailingapi.com/v1/messages/send \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "hello@yourdomain.com",
"to": "user@example.com",
"subject": "Holiday greeting",
"html": "<p>Happy holidays!</p>",
"scheduled_at": "2024-12-25T09:00:00Z"
}'
List scheduled messages
GET /v1/messages/scheduled
Response
{
"data": [
{
"id": "msg_abc123",
"to": "user@example.com",
"subject": "Holiday greeting",
"scheduled_at": "2024-12-25T09:00:00Z",
"status": "scheduled"
}
]
}
Cancel scheduled message
DELETE /v1/messages/{message_id}/schedule
Example request
curl -X DELETE https://api.mailingapi.com/v1/messages/msg_abc123/schedule \
-H "Authorization: Bearer $API_KEY"
Response
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "cancelled",
"created_at": "2024-01-15T10:30:00Z"
}
}
Message events
Get the event history for a message.
GET /v1/messages/{message_id}/events
Example request
curl https://api.mailingapi.com/v1/messages/550e8400-e29b-41d4-a716-446655440000/events \
-H "Authorization: Bearer $API_KEY"
Response
{
"data": [
{"id": "evt_1", "type": "queued", "timestamp": "2024-01-15T10:29:55Z", "metadata": {}},
{"id": "evt_2", "type": "sent", "timestamp": "2024-01-15T10:30:00Z", "metadata": {}},
{"id": "evt_3", "type": "delivered", "timestamp": "2024-01-15T10:30:05Z", "metadata": {}},
{"id": "evt_4", "type": "opened", "timestamp": "2024-01-15T11:00:00Z", "metadata": {"user_agent": "Mozilla/5.0"}}
]
}
Event types
| Type | Description |
|---|---|
queued |
Message entered the send queue |
sent |
Message handed off to recipient server |
delivered |
Recipient server accepted the message |
opened |
Recipient opened the email |
clicked |
Recipient clicked a link |
bounced |
Message was rejected |
complained |
Recipient marked as spam |
unsubscribed |
Recipient unsubscribed |
Error codes
Every error has a stable, machine-readable error.code. Branch on code, never on message. See Error Handling for the format and the full list of codes.
Codes you can get from the Messages API:
| HTTP |
code |
Meaning | Retry? |
|---|---|---|---|
400 |
bad_request |
Malformed request, e.g. invalid sender address, batch over 1000, missing messages / recipient_variables |
No |
400 |
invalid_idempotency_key |
Idempotency-Key is empty or longer than 255 characters |
No |
401 |
missing_auth, invalid_auth, invalid_key, insufficient_permissions, ip_not_allowed |
Authentication problem | No |
403 |
forbidden |
E.g. the sender is not from the API key’s domain | No |
403 |
organization_not_active, domain_not_verified, api_key_without_domain, daily_limit_reached, monthly_limit_reached, ip_blocked |
Account or plan state | No — fix the account state first |
404 |
not_found |
Message not found | No |
409 |
idempotency_request_in_progress |
Same Idempotency-Key is still processing |
Yes, after Retry-After seconds |
413 |
— | Request body larger than 25 MB | No — send less data |
422 |
validation_failed |
Field validation failed (including the limits), see details |
No |
422 |
idempotency_key_reused |
Same key, different body | No |
429 |
rate_limit_exceeded |
Per-second / per-minute API limit |
Yes, after Retry-After seconds |
429 |
daily_limit_exceeded |
Domain daily send limit |
Yes, after Retry-After seconds |
429 |
recipient_rate_limited |
Max 10 emails/hour and 50/day to the same recipient per organization |
Yes, after Retry-After seconds |
500 |
internal_error |
Server error |
Yes, exponential backoff, reuse the same Idempotency-Key |