Transactional email
Send one-to-one email such as receipts, password resets and notifications. The endpoint accepts Brevo's v3 payload, so existing Brevo integrations work after changing the base URL and API key.
/v3/smtp/emailPermission smtp:sendMessages are accepted, validated and queued; the response returns as soon as the message is queued. Delivery, opens, clicks and bounces are tracked afterwards — see Statuses & events.
Request body
| Parameter | Type | Description |
|---|---|---|
senderrequired | object | { name?, email }. The email must be on a verified sender domain of your brand, otherwise the request fails with 400 invalid_parameter. |
torequired | object[] | Recipients, each Object { email, name? }. Required unless every entry of messageVersions has its own to. |
cc | object[] | Carbon-copy recipients, each Object { email, name? }. |
bcc | object[] | Blind carbon-copy recipients, each Object { email, name? }. |
replyTo | object | Object { email, name? } used as the Reply-To address. |
subject | string | Subject line; may contain template tags. Required unless the template provides one. |
htmlContent | string | HTML body, rendered with the template language. Either htmlContent, textContent or templateId is required. |
textContent | string | Plain-text body (also rendered with the template language). |
templateId | integer | Numeric id of a saved template (the id returned by GET /v3/smtp/templates). Its HTML and subject are used unless you pass htmlContent / subject. Unknown ids return 404 document_not_found; inactive templates return 400. |
params | object | Values available as {{ params.KEY }} in the subject and body. Values may be strings, numbers, booleans, arrays and nested objects. |
headers | object | Custom email headers, e.g. {"X-Order-Id": "A-1077"}. Values are sent as strings. |
tags | string[] | Labels stored with the message for filtering logs and statistics in the portal. |
attachment | object[] | Files attached to every message: each item is { content, name } (base64) or { url, name? }. Up to 10 files, 5 MB each, 10 MB in total. See attachments. |
scheduledAt | string (date-time) | RFC 3339 timestamp with offset, up to 72 hours ahead, e.g. 2026-11-01T09:00:00+08:00. The request then returns 202. See scheduled sending. |
messageVersions | object[] | Up to 1000 personalised versions sent in one request. See batch sending. |
Send HTML content
curl -X POST 'https://app.snappymails.com/v3/smtp/email' \
-H "api-key: $SNAPPYMAILS_API_KEY" \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"sender": {
"name": "Acme Shop",
"email": "orders@acme.com"
},
"to": [
{
"email": "jane@example.com",
"name": "Jane Doe"
}
],
"replyTo": {
"email": "support@acme.com",
"name": "Acme Support"
},
"subject": "Order {{ params.ORDER_ID }} confirmed",
"htmlContent": "<h1>Thanks, {{ params.NAME }}!</h1><p>Your order {{ params.ORDER_ID }} is confirmed.</p>",
"textContent": "Thanks, {{ params.NAME }}! Your order {{ params.ORDER_ID }} is confirmed.",
"params": {
"NAME": "Jane",
"ORDER_ID": "A-1077"
},
"tags": [
"order-confirmation"
],
"headers": {
"X-Order-Id": "A-1077"
}
}'Send with a template
Design the template in the portal (visual builder or HTML), then reference it by its numeric id and pass the dynamic data in params. Inside the template, use {{ params.ORDER_ID }}, loops and conditions — see the template language.
curl -X POST 'https://app.snappymails.com/v3/smtp/email' \
-H "api-key: $SNAPPYMAILS_API_KEY" \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"sender": {
"name": "Acme Shop",
"email": "orders@acme.com"
},
"to": [
{
"email": "jane@example.com",
"name": "Jane Doe"
}
],
"templateId": 12,
"params": {
"ORDER_ID": "A-1077",
"TOTAL": 42.5,
"items": [
{
"name": "Coffee beans",
"qty": 2
},
{
"name": "Grinder",
"qty": 1
}
]
},
"tags": [
"order-confirmation"
]
}'Batch sending with messageVersions
messageVersions sends up to 1000 individual messages in a single call. Each version can override these top-level fields:
| Parameter | Type | Description |
|---|---|---|
torequired | object[] | Recipients of this version. |
cc / bcc | object[] | Copy recipients of this version. |
replyTo | object | Falls back to the top-level replyTo. |
params | object | Merged over the top-level params (version keys win). |
subject | string | Falls back to the top-level subject or the template subject. |
htmlContent | string | Falls back to the top-level htmlContent or the template. |
textContent | string | Falls back to the top-level textContent. |
sender, templateId, headers, tags, attachment and scheduledAt are shared by all versions. The response contains one id per version, in the same order, plus a batchId that identifies the whole request (use it to cancel or query a scheduled batch).
curl -X POST 'https://app.snappymails.com/v3/smtp/email' \
-H "api-key: $SNAPPYMAILS_API_KEY" \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"sender": {
"name": "Acme Shop",
"email": "news@acme.com"
},
"templateId": 7,
"params": {
"PROMO": "AUTUMN20"
},
"messageVersions": [
{
"to": [
{
"email": "jane@example.com",
"name": "Jane"
}
],
"params": {
"NAME": "Jane"
}
},
{
"to": [
{
"email": "omar@example.com",
"name": "Omar"
}
],
"params": {
"NAME": "Omar",
"PROMO": "VIP30"
},
"subject": "An extra treat for you, {{ params.NAME }}"
}
]
}'Attachments
Pass attachment as in Brevo. Each item has either content (standard base64) and a name, or a url (name is optional and defaults to the last path segment of the URL). The files are added to every message of the request, including each messageVersions entry.
curl -X POST 'https://app.snappymails.com/v3/smtp/email' \
-H "api-key: $SNAPPYMAILS_API_KEY" \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"sender": {
"name": "Acme Shop",
"email": "orders@acme.com"
},
"to": [
{
"email": "jane@example.com",
"name": "Jane Doe"
}
],
"subject": "Your invoice A-1077",
"htmlContent": "<p>Hi {{ params.NAME }}, your invoice is attached.</p>",
"params": {
"NAME": "Jane"
},
"attachment": [
{
"content": "JVBERi0xLjQKJcfsj6IKNSAwIG9iago8PC9MZW5ndGggNiAwIFI+PgpzdHJlYW0K",
"name": "invoice-A1077.pdf"
},
{
"url": "https://cdn.acme.com/legal/terms.pdf",
"name": "terms.pdf"
}
]
}'| Rule | Limit |
|---|---|
| Files per request | At most 10 |
| Size per file | 5 MB (decoded) |
| Total size | 10 MB (decoded); the request body may be up to 20 MB |
| URL attachments | Downloaded once when the request is made: https only, 10 s timeout, up to 5 redirects. Private and internal addresses are refused. |
| Blocked types | Executable and script extensions (exe com bat cmd scr pif msi msp msc vbs vbe js jse jar ps1 psm1 wsf wsh hta cpl reg dll sys lnk inf scf app sh apk dmg iso) and any file whose content starts with an executable signature (Windows PE, ELF, Mach-O), whatever its name. |
Problems return 400 invalid_parameter with a message naming the cause: bad base64, too many files, attachment too large, blocked file type or could not fetch attachment url. The SMTP relay accepts attachments as part of the MIME message, as before.
Scheduled sending
Set scheduledAt to send later, up to 72 hours ahead (further returns 400). A time less than 10 seconds ahead, or in the past, sends immediately. The quota for every recipient is reserved when the request is accepted (402 quota_exceeded as usual) and released if you cancel. Messages are released to the sending queue within a few seconds of scheduledAt; logs show them as QUEUED from then on. Scheduled messages are also listed in the portal under Transactional, Scheduled.
curl -X POST 'https://app.snappymails.com/v3/smtp/email' \
-H "api-key: $SNAPPYMAILS_API_KEY" \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"sender": {
"name": "Acme Clinic",
"email": "reminders@acme.com"
},
"to": [
{
"email": "jane@example.com",
"name": "Jane Doe"
}
],
"subject": "Reminder: your appointment tomorrow at 10:00",
"htmlContent": "<p>See you tomorrow, {{ params.NAME }}!</p>",
"params": {
"NAME": "Jane"
},
"scheduledAt": "2026-11-01T09:00:00+08:00"
}'{
"messageId": "<3f1c2a9e-8b7d-4d61-9a63-0c2f5e7b1d44@snappymails>"
}Cancel a scheduled email
/v3/smtp/email/{identifier}Permission smtp:sendidentifier is a messageId (with or without the <...> and @snappymails parts) or a batchId, which cancels every still-scheduled message of that request. Returns 204 No Content; 404 document_not_found when nothing matches and 400 invalid_parameter when the message is no longer scheduled (already sent or cancelled). The reserved quota is released.
curl -X DELETE 'https://app.snappymails.com/v3/smtp/email/5c6a2f0e-7d1b-4e3a-9f2c-8b1d0e4a6c11' \
-H "api-key: $SNAPPYMAILS_API_KEY" \
-H 'Accept: application/json'Get the status of a scheduled email
/v3/smtp/emailStatus/{identifier}Permission smtp:sendcurl 'https://app.snappymails.com/v3/smtp/emailStatus/3f1c2a9e-8b7d-4d61-9a63-0c2f5e7b1d44' \
-H "api-key: $SNAPPYMAILS_API_KEY" \
-H 'Accept: application/json'status is queued (waiting for scheduledAt), inProgress, processed or cancelled. A batchId returns one entry per message of the batch.
{
"scheduledAt": "2026-11-01T09:00:00+08:00",
"createdAt": "2026-10-31T08:12:40Z",
"status": "queued"
}{
"count": 2,
"batches": [
{
"scheduledAt": "2026-11-01T09:00:00+08:00",
"createdAt": "2026-10-31T08:12:40Z",
"status": "queued"
},
{
"scheduledAt": "2026-11-01T09:00:00+08:00",
"createdAt": "2026-10-31T08:12:40Z",
"status": "cancelled"
}
]
}Responses
201 Created
{
"messageId": "<3f1c2a9e-8b7d-4d61-9a63-0c2f5e7b1d44@snappymails>"
}{
"messageIds": [
"<0b6f5c1e-1d2a-4f0e-9a51-6f3c2b1d7e90@snappymails>",
"<9e4d3c2b-7a6f-4e5d-8c1b-2a3f4e5d6c7b@snappymails>"
],
"batchId": "5c6a2f0e-7d1b-4e3a-9f2c-8b1d0e4a6c11"
}202 Accepted
Returned instead of 201 when scheduledAt is more than 10 seconds ahead. The body has the same shape: messageId, or messageIds plus batchId with messageVersions.
Errors
| Status | Code | When |
|---|---|---|
400 | invalid_parameter | Invalid JSON, missing to / subject / content, invalid email address, unverified sender domain, inactive template, template syntax error, more than 1000 messageVersions, scheduledAt more than 72 hours ahead, invalid or blocked attachment. |
401 | unauthorized | Missing or invalid API key. |
402 | quota_exceeded | Monthly quota exhausted. Each recipient (to, cc and bcc) counts as one email. |
403 | permission_denied | Key without smtp:send. |
403 | email_not_verified | The brand owner has not verified their email yet. |
404 | document_not_found | templateId does not exist. |
429 | too_many_requests | Rate limit exceeded; see rate limits. |
Alias: POST /v1/tx/send
/v1/tx/sendPermission smtp:send/v1/tx/send is an alias of /v3/smtp/email with exactly the same body, responses and errors. Use whichever path fits your integration.
Good practices
- Always send a
textContentalongsidehtmlContentfor better deliverability. - Store the returned
messageIdto correlate with logs and with webhook events (message-id). - Prefer
urlattachments for large shared files (terms, catalogues) to keep request bodies small. - Use
tagsconsistently (e.g.password-reset,receipt) to compare performance per flow.
Migrating from Brevo
https://api.brevo.com with your SnappyMails base URL and use a SnappyMails API key. Re-create your templates (or import their HTML) so the templateId values exist in your brand.