Sending Emails

6 min read

This guide covers everything you need to know about sending emails through the MailingAPI.

Note: Sending is done through the REST API only. MailingAPI does not offer SMTP submission or relay for customers.

Basic email

The simplest email requires four fields:

curl -X POST https://api.mailingapi.com/v1/messages/send \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "notifications@yourdomain.com",
    "to": "user@example.com",
    "subject": "Your order has shipped",
    "html": "<h1>Order Shipped</h1><p>Your package is on the way!</p>",
    "text": "Order Shipped\n\nYour package is on the way!"
  }'

Required fields:

  • from — Sender address (must match your verified domain)
  • to — Recipient address
  • subject — Email subject line
  • html or text — Email body (at least one required)

Sender details

From address

The from field can be just an email or include a name:

{
  "from": "hello@yourdomain.com"
}
{
  "from": "Your Company <hello@yourdomain.com>"
}

Reply-To address

Set a different reply address using the Reply-To header:

{
  "from": "notifications@yourdomain.com",
  "headers": {
    "Reply-To": "support@yourdomain.com"
  }
}

Recipients

Single recipient

{
  "to": "user@example.com"
}

With name:

{
  "to": "John Doe <john@example.com>"
}

Multiple recipients

For the same email to multiple people (they see each other):

{
  "to": ["alice@example.com", "bob@example.com"]
}

CC and BCC

{
  "to": "primary@example.com",
  "cc": ["manager@example.com"],
  "bcc": ["archive@yourdomain.com"]
}

to, cc and bcc together may have at most 50 recipients, and each recipient counts towards your daily and monthly limits. Addresses on your suppression list are skipped in cc and bcc too.

Note: For personalized emails to multiple recipients, use batch sending instead.

Email content

HTML and plain text

Always provide both for maximum deliverability:

{
  "html": "<h1>Welcome!</h1><p>Thanks for signing up.</p>",
  "text": "Welcome!\n\nThanks for signing up."
}

If you only provide HTML, we automatically generate a text version.

Dynamic variables

Use template variables for personalization:

{
  "to": "john@example.com",
  "subject": "Welcome, {{name}}!",
  "html": "<p>Hi {{name}}, your account is ready.</p>",
  "template_data": {
    "name": "John"
  }
}

Supported syntax:

  • Mustache: {{variable}}
  • Mailgun-style: %recipient.variable%

Attachments

Add files to your email (max 10 files, 10 MB each after Base64 decoding). Each attachment needs filename and content (Base64). Invalid attachments are rejected with 422 validation_failed — they are never silently skipped. The whole request body may be at most 25 MB; Base64 inflates files by ~33%, so in practice about 18 MB of attachments fit in one request (e.g. one 10 MB file plus a few small ones). A larger body is rejected with HTTP 413:

{
  "attachments": [
    {
      "filename": "invoice.pdf",
      "content": "base64-encoded-content-here",
      "content_type": "application/pdf"
    }
  ]
}

For inline images (displayed in HTML):

{
  "attachments": [
    {
      "filename": "logo.png",
      "content": "base64-encoded-content",
      "content_type": "image/png",
      "content_id": "logo"
    }
  ],
  "html": "<img src=\"cid:logo\" alt=\"Logo\">"
}

Custom headers

Add custom email headers:

{
  "headers": {
    "X-Campaign-ID": "welcome-series-2024",
    "X-Customer-ID": "cust_123"
  }
}

Limits: max 20 custom headers; name only letters, digits and hyphens (max 128 characters); value a string of max 1000 characters with no line breaks (CR/LF). Exceeding a limit returns 422 validation_failed.

Reserved headers (set automatically, 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.

Tracking

Control open and click tracking per message:

{
  "tracking": {
    "opens": true,
    "clicks": true
  }
}

Default: both enabled (inherits from domain settings).

Transactional vs marketing emails

When both opens and clicks are disabled, the email is treated as transactional:

  • No tracking pixel (open tracking)
  • No link rewriting (click tracking)
  • No List-Unsubscribe header — this header signals to Gmail that the email is promotional/bulk, which hurts inbox placement for transactional messages

Disable tracking for:

  • Password resets and security alerts
  • Order confirmations and receipts
  • System notifications (new login, account changes)
  • 1:1 automated notifications

Keep tracking enabled for:

  • Marketing campaigns and newsletters
  • Promotional offers
  • Product updates and announcements

Example — transactional notification without tracking:

{
  "from": "alerts@yourdomain.com",
  "to": "user@example.com",
  "subject": "Your order #12345 has shipped",
  "html": "<p>Your package is on the way!</p>",
  "tracking": {
    "opens": false,
    "clicks": false
  }
}

Scheduled sending

Send emails at a specific time:

{
  "scheduled_at": "2024-12-25T09:00:00Z"
}
  • Use ISO 8601 format with timezone
  • Maximum 7 days in advance
  • Cancel with DELETE /v1/messages/{id}/schedule

List scheduled messages:

curl https://api.mailingapi.com/v1/messages/scheduled \
  -H "Authorization: Bearer $API_KEY"

Batch sending

Send personalized emails to multiple recipients efficiently:

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"}
    }
  }'

Or send completely different messages:

{
  "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>"
    }
  ]
}

Limits:

  • Maximum 1,000 messages per request (or 1,000 recipients in recipient_variables mode); more returns 400 bad_request for the whole request
  • Each message is validated and processed independently
  • Partial failures don’t block others — the request still returns HTTP 202, so always check errors in the response (see Response)
  • Idempotency-Key applies to the whole batch as one unit — see Idempotency

Using templates

Send using a pre-defined template:

{
  "from": "hello@yourdomain.com",
  "to": "user@example.com",
  "template_id": "tmpl_welcome_email",
  "template_data": {
    "name": "John",
    "activation_link": "https://example.com/activate/abc123"
  }
}

See Templates guide for creating and managing templates.

Tags and metadata

Add tags for filtering in analytics:

{
  "tags": ["welcome", "onboarding", "2024"]
}

Add metadata for your own tracking:

{
  "metadata": {
    "user_id": "usr_123",
    "campaign": "black-friday"
  }
}

Metadata is returned in webhooks and message details.

Limits (exceeding any returns 422 validation_failed with details for that field — nothing is silently dropped):

  • tags: max 10, each 1–64 characters
  • metadata: JSON object, max 50 keys, max 2 KB when serialized as JSON
  • subject: 1–998 characters; scheduled_at: at most 7 days ahead

Response

Successful send returns:

{
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "status": "queued",
    "from": "hello@yourdomain.com",
    "to": "user@example.com",
    "subject": "Your order has shipped",
    "created_at": "2024-01-15T10:30:00Z"
  }
}

Batch send returns HTTP 202 whenever the request itself is valid — even if some or all messages were rejected. errors is a count; check it and inspect messages (in request order, each with its index). Entry status is queued or error; rejected entries have no id, but carry an error object (code is validation_failed, forbidden, bad_request or send_failed; details only for validation_failed):

{
  "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"}
    ]
  }
}

Pre-send validation

Check your email before sending:

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",
    "html": "<p>Content here</p>"
  }'

Returns spam score, potential issues, and recommendations.

Best practices

  1. Always include text version — Some clients don’t render HTML
  2. Use templates for consistency — Avoid inline HTML when possible
  3. Set reply-to appropriately — Don’t use no-reply@ if you want responses
  4. Track with metadata — Link emails to your internal systems
  5. Test before production — Use /analyze endpoint first
  6. Handle bounces — Monitor webhooks and clean your lists

Next steps