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 | Makes this request safe to retry. Use <app>-<ULID>, such as crm-01JBT8XQ2M9WYC3K4F6R7S8T9V; any other shape is a 422. Send the same key on every retry: a repeat gets the first answer with Idempotency-Replayed: true, a key reused for a different request is a 422, and one still running is a 409 with Retry-After. Keys last 24 hours, per organization. |
Request body
messagesarray<object>requiredThe recipients, 1 to 500, each with their own
variables.tostringrequiredThis recipient's address.
variablesobjectValues for this recipient's
{{ tags }}, like{"name": "Ada"}.
fromstringrequiredThe sender address for every message. It must be a verified sender in your organization.
subjectstringThe subject for every message. It can use
{{ tags }}. Needed unlesstemplate_idis given.htmlstringThe HTML for every message, with
{{ tags }}filled from each recipient'svariables.textstringThe plain-text version of the email.
template_idstringOptional. One of your templates to use instead of
subjectandhtml.tagsarray<string>Your own labels, returned on the delivery events for these messages.
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
acceptedis true.failedintegerRows where
acceptedis false. A refused recipient is a row, never a failed request:sent + failedalways equalsresultslength.resultsarray<object>One row per recipient, in the order submitted.
recipientstringThe recipient this row is about, as you sent it.
acceptedbooleanThe ONLY field to branch on. Every row carries every key, so a missing
idnever has to be read as a success.idstringMailyte's ID for this recipient's copy, like
idonPOST /messages. Null whenacceptedis false.message_idstringThe RFC 5322 Message-ID for this recipient's copy, angle brackets included, for
GET /domains/{domain}/messages/{messageId}. A different string fromid, for a different endpoint. Null whenacceptedis false.errorstringWhy this recipient was refused, such as a suppressed or malformed address. Null when
acceptedis 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.