Send a batch of messages
/api/v1/messages/batchOne shared template and a list of recipients, each with their own variables.
Up to 500 recipients per call.
A partial failure is not an error. Every recipient is processed independently, so
one suppressed or malformed address does not stop the rest — the call still returns
200 and the per-recipient outcome is in results. Check failed rather than the
status code, or you will silently lose mail you believe you sent.
This is for transactional mail going to many people at once. For marketing sends to a list or segment, use campaigns instead: they add unsubscribe handling, review and reporting that this endpoint deliberately does not.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
Idempotency-Key | header | string | Send a unique value — a UUID is ideal — to make this request safe to retry. If we have already answered a request with the same key and the same body, you get that exact response back with `Idempotent-Replayed: true` and nothing is sent a second time. Reusing a key with a DIFFERENT body is a 409, because answering the first response to a second message would silently swallow it. Keys are scoped to your organization and honoured for 24 hours. A 5xx does not record a key: we cannot say whether the message left, so your retry genuinely retries. Omit the header and nothing changes. |
Request body
messagesarray<object>requiredtostringrequiredvariablesobject
fromstringrequiredsubjectstringhtmlstringtextstringtemplate_idstringtagsarray<string>
Request
/api/v1/messages/batchcurl -X POST 'https://app.mailyte.com/api/v1/messages/batch' \
-H 'Authorization: Bearer mk_live_YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"from": "hello@yourdomain.com",
"subject": "Your weekly summary",
"template_id": "01JBT8XQ2M9WYC3K4F6R7S8T9V",
"messages": [
{
"to": "ada@example.com",
"variables": {
"first_name": "Ada"
}
},
{
"to": "grace@example.com",
"variables": {
"first_name": "Grace"
}
}
]
}'const response = await fetch('https://app.mailyte.com/api/v1/messages/batch', {
method: 'POST',
headers: {
Authorization: 'Bearer mk_live_YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
"from": "hello@yourdomain.com",
"subject": "Your weekly summary",
"template_id": "01JBT8XQ2M9WYC3K4F6R7S8T9V",
"messages": [
{
"to": "ada@example.com",
"variables": {
"first_name": "Ada"
}
},
{
"to": "grace@example.com",
"variables": {
"first_name": "Grace"
}
}
]
}),
});
const { data } = await response.json();import requests
response = requests.post(
"https://app.mailyte.com/api/v1/messages/batch",
headers={"Authorization": "Bearer mk_live_YOUR_API_KEY"},
json={
"from": "hello@yourdomain.com",
"subject": "Your weekly summary",
"template_id": "01JBT8XQ2M9WYC3K4F6R7S8T9V",
"messages": [
{
"to": "ada@example.com",
"variables": {
"first_name": "Ada"
}
},
{
"to": "grace@example.com",
"variables": {
"first_name": "Grace"
}
}
]
},
)
data = response.json()["data"]<?php
$response = Http::withToken('mk_live_YOUR_API_KEY')
->post('https://app.mailyte.com/api/v1/messages/batch', [
'from' => 'hello@yourdomain.com',
'subject' => 'Your weekly summary',
'template_id' => '01JBT8XQ2M9WYC3K4F6R7S8T9V',
'messages' => [
[
'to' => 'ada@example.com',
'variables' => [
'first_name' => 'Ada',
],
],
[
'to' => 'grace@example.com',
'variables' => [
'first_name' => 'Grace',
],
],
],
]);
$data = $response->json('data');require "net/http"
require "json"
uri = URI("https://app.mailyte.com/api/v1/messages/batch")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer mk_live_YOUR_API_KEY"
request["Content-Type"] = "application/json"
request.body = {
"from": "hello@yourdomain.com",
"subject": "Your weekly summary",
"template_id": "01JBT8XQ2M9WYC3K4F6R7S8T9V",
"messages": [
{
"to": "ada@example.com",
"variables": {
"first_name": "Ada"
}
},
{
"to": "grace@example.com",
"variables": {
"first_name": "Grace"
}
}
]
}.to_json
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }Response
The batch was processed. Individual recipients may still have failed — check `failed`.
dataobjectobjectstringbatchsentintegerRows where `accepted` is true.
failedintegerRows where `accepted` is false. A refused recipient is a row, never a failed request: `sent + failed` always equals `results` length.
resultsarray<object>One row per recipient, in the order submitted.
recipientstringWhich recipient this row is about -- the address you submitted, echoed back so a row can be matched to a request entry without relying on position. NULL WOULD MEAN UNKNOWN and cannot happen: `messages[].to` is required and validated as an email before any row is built, and the row is built from that value on both the accepted and the refused path. A refused row still names its recipient -- that is the point of it -- so null here is never how a failure is reported. `error` is.
acceptedbooleanThe ONLY field to branch on. Every row carries every key, so a missing `id` never has to be read as a success.
idstringThe bare ULID handle for this recipient's copy -- the same thing `POST /messages` calls `id`, resolving at `GET /email-logs/messages/{id}`. Null when `accepted` is false.
message_idstringThe RFC 5322 Message-ID for this recipient's copy, angle brackets included, for `GET /domains/{domain}/messages/{messageId}`. A different string from `id`, for a different endpoint. Null when `accepted` is false.
errorstringWhy this one recipient was refused -- a suppression, a malformed address. Null when `accepted` is true.
application/json{
"message": "Batch processed",
"code": 200,
"success": true,
"data": {
"sent": 1,
"failed": 1,
"results": [
{
"to": "ada@example.com",
"message_id": "<01JBT8XQ2M@yourdomain.com>"
},
{
"to": "grace@example.com",
"error": "Recipient is suppressed."
}
]
}
}Returned inside the standard envelope.
Errors
| Status | When |
|---|---|
401 | The API key is missing, unknown, revoked or expired. All four answer identically, on purpose: distinguishing them would confirm which keys exist. |
403 | The key is valid but may not do this: it lacks the required scope, its IP allowlist does not include you, or this endpoint does not accept API keys. |
404 | No such resource in this organization. |
422 | The request was understood but the values were not acceptable. |
429 | Too many requests, or the organization has spent its sending allowance. `Retry-After` says how long to wait. |
Every status, with what causes it and what to do, is on the error reference.