Documentation / API Reference

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-Unsubscribe header 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 tracking value 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 no id. error.code is one of validation_failed, forbidden (sender domain), bad_request or send_failed; error.details is present only for validation_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_variables mode). Exceeding the limit rejects the whole request with 400 bad_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-Key applies 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