Attachments and headers
Attach files — receipts, invoices, documents, images — set the reply address, and add your own headers to trace emails in your system. This article covers the attachments, replyTo, and headers fields of POST /api/v1/public/emails.
Before you start
- A token with the
email.sendscope.
The attachments format
attachments is an array of { fileName, content } objects, one object per file:
"attachments": [
{ "fileName": "receipt.pdf", "content": "JVBERi0xLjQK..." }
]
fileName— the name with an extension; the recipient sees the file under this name.content— the base64 content, without adata:application/pdf;base64,prefix.
Limits
- No more than 20 files per email, otherwise
400 ATTACHMENT_COUNT_EXCEEDED. - Total size of all attachments — up to 15 MB after base64 decoding, otherwise
400 ATTACHMENT_TOO_LARGE. - Executable formats are not accepted —
400 ATTACHMENT_TYPE_FORBIDDEN. Both the extension infileNameand the file content are checked. - The whole request body — up to 32 MB. Base64 makes a file about a third larger.
For documents larger than a few megabytes, it's more reliable to host the file on your side and put a link in html or text — that lowers the risk of hitting the limit and speeds up delivery.
Example: a single attachment
- TypeScript
- Go
- Python
- cURL
import fs from 'fs';
const pdfBase64 = fs.readFileSync('./receipt.pdf').toString('base64');
const response = await fetch('https://api.emailmassivo.com/api/v1/public/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.API_TOKEN}`,
'Content-Type': 'application/json',
'Idempotency-Key': 'receipt-12345',
},
body: JSON.stringify({
data: {
from: { email: 'noreply@yourdomain.com' },
to: { email: 'user@example.com' },
subject: 'Your receipt',
html: '<p>Your receipt is attached.</p>',
attachments: [{ fileName: 'receipt.pdf', content: pdfBase64 }],
},
}),
});
package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"net/http"
"os"
)
func main() {
pdf, _ := os.ReadFile("./receipt.pdf")
encoded := base64.StdEncoding.EncodeToString(pdf)
payload, _ := json.Marshal(map[string]any{
"data": map[string]any{
"from": map[string]string{"email": "noreply@yourdomain.com"},
"to": map[string]string{"email": "user@example.com"},
"subject": "Your receipt",
"html": "<p>Your receipt is attached.</p>",
"attachments": []map[string]string{
{"fileName": "receipt.pdf", "content": encoded},
},
},
})
req, _ := http.NewRequest("POST", "https://api.emailmassivo.com/api/v1/public/emails", bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+os.Getenv("API_TOKEN"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "receipt-12345")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
}
import base64
import os
import requests
with open('./receipt.pdf', 'rb') as f:
pdf_base64 = base64.b64encode(f.read()).decode('ascii')
response = requests.post(
'https://api.emailmassivo.com/api/v1/public/emails',
headers={
'Authorization': f'Bearer {os.environ["API_TOKEN"]}',
'Idempotency-Key': 'receipt-12345',
},
json={
'data': {
'from': {'email': 'noreply@yourdomain.com'},
'to': {'email': 'user@example.com'},
'subject': 'Your receipt',
'html': '<p>Your receipt is attached.</p>',
'attachments': [{'fileName': 'receipt.pdf', 'content': pdf_base64}],
},
},
)
PDF_BASE64=$(base64 < ./receipt.pdf | tr -d '\n')
printf '{"data": {"from": {"email": "noreply@yourdomain.com"}, "to": {"email": "user@example.com"}, "subject": "Your receipt", "html": "<p>Your receipt is attached.</p>", "attachments": [{"fileName": "receipt.pdf", "content": "%s"}]}}' "$PDF_BASE64" > body.json
curl -X POST https://api.emailmassivo.com/api/v1/public/emails \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: receipt-12345" \
--data-binary @body.json
The body is built in the body.json file: the base64 of a large file doesn't fit into a command-line argument.
Reply address (replyTo)
replyTo is the address or addresses the recipient's mail client replies to:
"replyTo": [
{ "email": "support@shop.com", "name": "Support" },
{ "email": "orders@shop.com" }
]
- An object or an array, up to 5 addresses. They don't count toward the 50-recipient limit.
- EmailMassivo writes the addresses into the email's
Reply-Toheader, comma-separated; non-ASCII names are encoded automatically. - The domain of
replyToaddresses doesn't need to be verified.
You can't pass Reply-To through headers — use the replyTo field.
Your own headers (headers)
headers is an object { "Header-Name": "value" }, values are strings. Use your own headers for metadata that helps you find the email in your system's logs:
"headers": {
"X-Order-Id": "12345",
"X-Campaign": "checkout-confirmation",
"List-ID": "orders.shop.com"
}
System headers are forbidden
Headers that EmailMassivo generates can't be passed in headers — in any letter case. A request with them is rejected with 400 HEADER_NOT_ALLOWED; details[] has one item per such header with field: "headers.<name>".
| Purpose | Headers |
|---|---|
| Addressing | From, To, Cc, Bcc, Sender, Return-Path, Reply-To (use the replyTo field) |
| Linking emails and delivery reports | Message-ID, References, In-Reply-To |
| Signature and transport | DKIM-Signature, Received, Date |
| Format | Content-Type, MIME-Version, Content-Transfer-Encoding |
| Unsubscribe and feedback | List-Unsubscribe, List-Unsubscribe-Post, Feedback-ID, Precedence, Errors-To, X-Complaints-To |
| Service | X-SenderName-MailID, X-Mail-Id, X-Mailru-Msgtype, X-Postmaster-Msgtype |
EmailMassivo sets List-Unsubscribe and List-Unsubscribe-Post in the email itself — one-click unsubscribe works without any setup.
{
"ok": false,
"error": {
"code": "HEADER_NOT_ALLOWED",
"message": "System headers cannot be set in headers. Use the replyTo field instead of the Reply-To header.",
"details": [
{ "field": "headers.Reply-To", "reason": "HEADER_NOT_ALLOWED", "value": "support@shop.com" }
]
},
"meta": { "requestId": "01JB2W...", "timestamp": "2026-09-30T10:15:00.120Z" }
}
Handling common errors
| HTTP | error.code | Cause | Action |
|---|---|---|---|
| 400 | BAD_REQUEST with reason: INVALID_BASE64 | content isn't base64 or has a data:...;base64, prefix | Re-encode the file without the prefix |
| 400 | ATTACHMENT_COUNT_EXCEEDED | More than 20 files | Reduce the number of attachments or send several emails |
| 400 | ATTACHMENT_TOO_LARGE | More than 15 MB in total | Compress the files or link to them in the email body |
| 400 | ATTACHMENT_TYPE_FORBIDDEN | An executable or other disallowed format | Use another format |
| 400 | HEADER_NOT_ALLOWED | A system header in headers | Remove the header; for Reply-To use the replyTo field |
| 413 | PAYLOAD_TOO_LARGE | The request body is larger than 32 MB | Reduce attachments or put a link to the file in the email body |
The full list is in Send error codes.
See also
- Send a transactional email — all request fields.
- Send error codes.
- Sending keys — quota, rate, domains.