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 -
htmlortext— 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-Unsubscribeheader — 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_variablesmode); more returns400bad_requestfor the whole request - Each message is validated and processed independently
-
Partial failures don’t block others — the request still returns HTTP
202, so always checkerrorsin the response (see Response) -
Idempotency-Keyapplies 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
- Always include text version — Some clients don’t render HTML
- Use templates for consistency — Avoid inline HTML when possible
- Set reply-to appropriately — Don’t use no-reply@ if you want responses
- Track with metadata — Link emails to your internal systems
-
Test before production — Use
/analyzeendpoint first - Handle bounces — Monitor webhooks and clean your lists
Next steps
- Set up your domain for production sending
- Configure webhooks for delivery notifications
- Create templates for reusable emails