Transactional email API
Send, batch, schedule, reschedule, cancel and look up transactional email through the Reggio Digital API, with attachments and test addresses.
Every call on this page needs a key with the email:send ability, on an account with a transactional email plan and at least one verified sending domain. The dashboard's Send API page shows your account’s endpoint and verified sending domain, and makes a new key when you need one.
Sending one email
POST /api/v1/emails
curl -X POST https://reggiodigital.com/api/v1/emails \
-H "Authorization: Bearer $REGGIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "hello@yourdomain.com",
"to": [
"success@simulator.amazonses.com"
],
"subject": "Your receipt",
"html": "<p>Thanks for your order.</p>",
"text": "Thanks for your order.",
"tags": {
"kind": "receipt",
"order": "1042"
}
}'
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.reggio.key'))
->acceptJson()
->post('https://reggiodigital.com/api/v1/emails', [
'from' => 'hello@yourdomain.com',
'to' => [
'success@simulator.amazonses.com',
],
'subject' => 'Your receipt',
'html' => '<p>Thanks for your order.</p>',
'text' => 'Thanks for your order.',
'tags' => [
'kind' => 'receipt',
'order' => '1042',
],
]);
if ($response->failed()) {
throw new RuntimeException($response->json('error.message'));
}
$body = $response->json();
$payload = [
'from' => 'hello@yourdomain.com',
'to' => [
'success@simulator.amazonses.com',
],
'subject' => 'Your receipt',
'html' => '<p>Thanks for your order.</p>',
'text' => 'Thanks for your order.',
'tags' => [
'kind' => 'receipt',
'order' => '1042',
],
];
$ch = curl_init('https://reggiodigital.com/api/v1/emails');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer '.getenv('REGGIO_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$body = json_decode((string) curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($status >= 400) {
throw new RuntimeException($body['error']['message']);
}
const response = await fetch("https://reggiodigital.com/api/v1/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.REGGIO_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"from": "hello@yourdomain.com",
"to": [
"success@simulator.amazonses.com"
],
"subject": "Your receipt",
"html": "<p>Thanks for your order.</p>",
"text": "Thanks for your order.",
"tags": {
"kind": "receipt",
"order": "1042"
}
}),
});
const body = await response.json();
if (!response.ok) {
throw new Error(body.error.message);
}
A message we accept answers 202:
{
"id": "0100019a1b2c3d4e-5f6a7b8c-1234-5678-9abc-def012345678-000000",
"to": ["success@simulator.amazonses.com"],
"suppressed": []
}
Store the id. It is how you look the message up later and how our webhooks refer to it. Treat it as an opaque string: a message sent now and a message held for later have ids of different shapes.
to is the recipients we are sending to. suppressed is any recipient we skipped because we have stopped sending to that address after a hard bounce, a spam complaint or an unsubscribe. A skipped address is not an error, so check the list rather than treating the send as failed. If every recipient is suppressed, the send is refused with a 422.
The fields
| Field | Required | What it is |
|---|---|---|
from |
Yes | An address on one of the account's verified sending domains, on its own. A name in front of it, as in Acme <hello@yourdomain.com>, is refused with a 422. |
to |
Yes | A list of up to 50 addresses. Always a list, even for one recipient. |
subject |
Without a template | The subject line, up to 998 characters. |
html |
Without a template, html or text |
The formatted body. |
text |
Without a template, html or text |
The plain body. Send both when you can. |
template_id |
Instead of a body | A published template. Leave out subject, html and text. |
variables |
With a template | The values the template fills in, as name and value pairs. |
reply_to |
No | Where replies go, if not the from address. |
cc, bcc |
No | Lists of up to 50 addresses each. |
tags |
No | Up to 20 name and value pairs for your own use, such as {"kind": "receipt", "order": "1042"}. Names use letters, numbers, dashes, underscores, colons and dots; values are strings up to 128 characters. They come back when you look a message up and in webhooks. |
headers |
No | Up to 10 extra email headers. Any name starting with X-, plus List-Unsubscribe and List-Unsubscribe-Post. Values cannot contain line breaks. |
attachments |
No | Up to 20 files. See attachments. |
scheduled_at |
No | A time up to 30 days ahead to send it instead of now. See sending later. |
If Gmail threads a run of similar receipts into one conversation, set X-Entity-Ref-ID in headers to something unique per message.
Attachments
Give each attachment a filename and either content, the file as base64, or path, an https link we download it from. A link saves your server holding a large file in memory.
curl -X POST https://reggiodigital.com/api/v1/emails \
-H "Authorization: Bearer $REGGIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "hello@yourdomain.com",
"to": [
"success@simulator.amazonses.com"
],
"subject": "Your receipt",
"html": "<p>Thanks for your order.</p><img src=\"cid:logo\" alt=\"\">",
"text": "Thanks for your order.",
"tags": {
"kind": "receipt",
"order": "1042"
},
"attachments": [
{
"filename": "receipt-1042.txt",
"content": "T3JkZXIgMTA0MgpUb3RhbDogJDQyLjAwCg=="
},
{
"filename": "logo.png",
"content": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAMAASsJTYQAAAAASUVORK5CYII=",
"content_id": "logo"
}
]
}'
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.reggio.key'))
->acceptJson()
->post('https://reggiodigital.com/api/v1/emails', [
'from' => 'hello@yourdomain.com',
'to' => [
'success@simulator.amazonses.com',
],
'subject' => 'Your receipt',
'html' => '<p>Thanks for your order.</p><img src="cid:logo" alt="">',
'text' => 'Thanks for your order.',
'tags' => [
'kind' => 'receipt',
'order' => '1042',
],
'attachments' => [
[
'filename' => 'receipt-1042.txt',
'content' => 'T3JkZXIgMTA0MgpUb3RhbDogJDQyLjAwCg==',
],
[
'filename' => 'logo.png',
'content' => 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAMAASsJTYQAAAAASUVORK5CYII=',
'content_id' => 'logo',
],
],
]);
if ($response->failed()) {
throw new RuntimeException($response->json('error.message'));
}
$body = $response->json();
$payload = [
'from' => 'hello@yourdomain.com',
'to' => [
'success@simulator.amazonses.com',
],
'subject' => 'Your receipt',
'html' => '<p>Thanks for your order.</p><img src="cid:logo" alt="">',
'text' => 'Thanks for your order.',
'tags' => [
'kind' => 'receipt',
'order' => '1042',
],
'attachments' => [
[
'filename' => 'receipt-1042.txt',
'content' => 'T3JkZXIgMTA0MgpUb3RhbDogJDQyLjAwCg==',
],
[
'filename' => 'logo.png',
'content' => 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAMAASsJTYQAAAAASUVORK5CYII=',
'content_id' => 'logo',
],
],
];
$ch = curl_init('https://reggiodigital.com/api/v1/emails');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer '.getenv('REGGIO_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$body = json_decode((string) curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($status >= 400) {
throw new RuntimeException($body['error']['message']);
}
const response = await fetch("https://reggiodigital.com/api/v1/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.REGGIO_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"from": "hello@yourdomain.com",
"to": [
"success@simulator.amazonses.com"
],
"subject": "Your receipt",
"html": "<p>Thanks for your order.</p><img src=\"cid:logo\" alt=\"\">",
"text": "Thanks for your order.",
"tags": {
"kind": "receipt",
"order": "1042"
},
"attachments": [
{
"filename": "receipt-1042.txt",
"content": "T3JkZXIgMTA0MgpUb3RhbDogJDQyLjAwCg=="
},
{
"filename": "logo.png",
"content": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAMAASsJTYQAAAAASUVORK5CYII=",
"content_id": "logo"
}
]
}),
});
const body = await response.json();
if (!response.ok) {
throw new Error(body.error.message);
}
- A
pathmust be https and reachable from the public internet. Links into private networks are refused. - Add a
content_idand the file becomes an inline image. Point at it from your HTML with<img src="cid:logo">. - Set
content_typeto the file's type, such asapplication/pdf. Without it the file is sent asapplication/octet-stream, which some mail apps will not preview. - Keep the whole message under 28MB. Anything bigger is refused with a
413; send a link to the file instead. - Batches cannot carry attachments.
Sending later
Give scheduled_at a time with a timezone, up to 30 days ahead:
curl -X POST https://reggiodigital.com/api/v1/emails \
-H "Authorization: Bearer $REGGIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "hello@yourdomain.com",
"to": [
"success@simulator.amazonses.com"
],
"subject": "Your receipt",
"html": "<p>Thanks for your order.</p>",
"text": "Thanks for your order.",
"tags": {
"kind": "receipt",
"order": "1042"
},
"scheduled_at": "2026-10-31T09:00:00-04:00"
}'
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.reggio.key'))
->acceptJson()
->post('https://reggiodigital.com/api/v1/emails', [
'from' => 'hello@yourdomain.com',
'to' => [
'success@simulator.amazonses.com',
],
'subject' => 'Your receipt',
'html' => '<p>Thanks for your order.</p>',
'text' => 'Thanks for your order.',
'tags' => [
'kind' => 'receipt',
'order' => '1042',
],
'scheduled_at' => '2026-10-31T09:00:00-04:00',
]);
if ($response->failed()) {
throw new RuntimeException($response->json('error.message'));
}
$body = $response->json();
$payload = [
'from' => 'hello@yourdomain.com',
'to' => [
'success@simulator.amazonses.com',
],
'subject' => 'Your receipt',
'html' => '<p>Thanks for your order.</p>',
'text' => 'Thanks for your order.',
'tags' => [
'kind' => 'receipt',
'order' => '1042',
],
'scheduled_at' => '2026-10-31T09:00:00-04:00',
];
$ch = curl_init('https://reggiodigital.com/api/v1/emails');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer '.getenv('REGGIO_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$body = json_decode((string) curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($status >= 400) {
throw new RuntimeException($body['error']['message']);
}
const response = await fetch("https://reggiodigital.com/api/v1/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.REGGIO_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"from": "hello@yourdomain.com",
"to": [
"success@simulator.amazonses.com"
],
"subject": "Your receipt",
"html": "<p>Thanks for your order.</p>",
"text": "Thanks for your order.",
"tags": {
"kind": "receipt",
"order": "1042"
},
"scheduled_at": "2026-10-31T09:00:00-04:00"
}),
});
const body = await response.json();
if (!response.ok) {
throw new Error(body.error.message);
}
We check everything about the message straight away, so a problem comes back on this request rather than at send time. The response has an id starting with sched_ and the scheduled_at we recorded, in UTC. That id keeps working after the message has gone. A scheduled message does not count against the monthly allowance until it is sent.
Changing the time
PATCH /api/v1/emails/{id} with a new scheduled_at. The message keeps its id, and only one copy ever goes out.
curl -X PATCH https://reggiodigital.com/api/v1/emails/sched_123 \
-H "Authorization: Bearer $REGGIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"scheduled_at": "2026-11-01T14:30:00-05:00"
}'
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.reggio.key'))
->acceptJson()
->patch('https://reggiodigital.com/api/v1/emails/sched_123', [
'scheduled_at' => '2026-11-01T14:30:00-05:00',
]);
if ($response->failed()) {
throw new RuntimeException($response->json('error.message'));
}
$body = $response->json();
$payload = [
'scheduled_at' => '2026-11-01T14:30:00-05:00',
];
$ch = curl_init('https://reggiodigital.com/api/v1/emails/sched_123');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'PATCH',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer '.getenv('REGGIO_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$body = json_decode((string) curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($status >= 400) {
throw new RuntimeException($body['error']['message']);
}
const response = await fetch("https://reggiodigital.com/api/v1/emails/sched_123", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.REGGIO_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"scheduled_at": "2026-11-01T14:30:00-05:00"
}),
});
const body = await response.json();
if (!response.ok) {
throw new Error(body.error.message);
}
Cancelling
DELETE /api/v1/emails/{id}. This is permanent, and the response has "status": "canceled".
curl -X DELETE https://reggiodigital.com/api/v1/emails/sched_123 \
-H "Authorization: Bearer $REGGIO_API_KEY"
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.reggio.key'))
->acceptJson()
->delete('https://reggiodigital.com/api/v1/emails/sched_123');
if ($response->failed()) {
throw new RuntimeException($response->json('error.message'));
}
$body = $response->json();
$ch = curl_init('https://reggiodigital.com/api/v1/emails/sched_123');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer '.getenv('REGGIO_API_KEY'),
],
]);
$body = json_decode((string) curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($status >= 400) {
throw new RuntimeException($body['error']['message']);
}
const response = await fetch("https://reggiodigital.com/api/v1/emails/sched_123", {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.REGGIO_API_KEY}`,
},
});
const body = await response.json();
if (!response.ok) {
throw new Error(body.error.message);
}
A message that has already gone out, or that we have started sending, can be neither moved nor cancelled. Moving it answers 409 with not_reschedulable, and cancelling it answers 409 with not_cancellable.
Sending a batch
POST /api/v1/emails/batch accepts up to 100 messages under emails. Each takes the same fields as a single send, except attachments and scheduled_at.
curl -X POST https://reggiodigital.com/api/v1/emails/batch \
-H "Authorization: Bearer $REGGIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"emails": [
{
"from": "hello@yourdomain.com",
"to": [
"success@simulator.amazonses.com"
],
"subject": "Your table is ready",
"text": "See you soon."
},
{
"from": "hello@yourdomain.com",
"to": [
"bounce@simulator.amazonses.com"
],
"subject": "Your table is ready",
"text": "See you soon."
}
]
}'
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.reggio.key'))
->acceptJson()
->post('https://reggiodigital.com/api/v1/emails/batch', [
'emails' => [
[
'from' => 'hello@yourdomain.com',
'to' => [
'success@simulator.amazonses.com',
],
'subject' => 'Your table is ready',
'text' => 'See you soon.',
],
[
'from' => 'hello@yourdomain.com',
'to' => [
'bounce@simulator.amazonses.com',
],
'subject' => 'Your table is ready',
'text' => 'See you soon.',
],
],
]);
if ($response->failed()) {
throw new RuntimeException($response->json('error.message'));
}
$body = $response->json();
$payload = [
'emails' => [
[
'from' => 'hello@yourdomain.com',
'to' => [
'success@simulator.amazonses.com',
],
'subject' => 'Your table is ready',
'text' => 'See you soon.',
],
[
'from' => 'hello@yourdomain.com',
'to' => [
'bounce@simulator.amazonses.com',
],
'subject' => 'Your table is ready',
'text' => 'See you soon.',
],
],
];
$ch = curl_init('https://reggiodigital.com/api/v1/emails/batch');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer '.getenv('REGGIO_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$body = json_decode((string) curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($status >= 400) {
throw new RuntimeException($body['error']['message']);
}
const response = await fetch("https://reggiodigital.com/api/v1/emails/batch", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.REGGIO_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"emails": [
{
"from": "hello@yourdomain.com",
"to": [
"success@simulator.amazonses.com"
],
"subject": "Your table is ready",
"text": "See you soon."
},
{
"from": "hello@yourdomain.com",
"to": [
"bounce@simulator.amazonses.com"
],
"subject": "Your table is ready",
"text": "See you soon."
}
]
}),
});
const body = await response.json();
if (!response.ok) {
throw new Error(body.error.message);
}
Every message is checked before any is accepted, so the whole batch is taken or none of it is. A problem names the message by position, as in emails.1.to. The response lists one result per message, in the order you sent them:
{
"data": [
{ "id": "sched_981", "suppressed": [] },
{ "id": "sched_982", "suppressed": [] }
]
}
Each message goes out within moments. Its id starts as sched_ and works for lookups straight away.
Not sending twice
Add an Idempotency-Key header to any send you might retry. A repeat within 24 hours gets the first answer back and sends nothing. More in idempotency.
curl -X POST https://reggiodigital.com/api/v1/emails \
-H "Authorization: Bearer $REGGIO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: receipt-1042" \
-d '{
"from": "hello@yourdomain.com",
"to": [
"success@simulator.amazonses.com"
],
"subject": "Your receipt",
"html": "<p>Thanks for your order.</p>",
"text": "Thanks for your order.",
"tags": {
"kind": "receipt",
"order": "1042"
}
}'
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.reggio.key'))
->acceptJson()
->withHeaders([
'Idempotency-Key' => 'receipt-1042',
])
->post('https://reggiodigital.com/api/v1/emails', [
'from' => 'hello@yourdomain.com',
'to' => [
'success@simulator.amazonses.com',
],
'subject' => 'Your receipt',
'html' => '<p>Thanks for your order.</p>',
'text' => 'Thanks for your order.',
'tags' => [
'kind' => 'receipt',
'order' => '1042',
],
]);
if ($response->failed()) {
throw new RuntimeException($response->json('error.message'));
}
$body = $response->json();
$payload = [
'from' => 'hello@yourdomain.com',
'to' => [
'success@simulator.amazonses.com',
],
'subject' => 'Your receipt',
'html' => '<p>Thanks for your order.</p>',
'text' => 'Thanks for your order.',
'tags' => [
'kind' => 'receipt',
'order' => '1042',
],
];
$ch = curl_init('https://reggiodigital.com/api/v1/emails');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer '.getenv('REGGIO_API_KEY'),
'Content-Type: application/json',
'Idempotency-Key: receipt-1042',
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$body = json_decode((string) curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($status >= 400) {
throw new RuntimeException($body['error']['message']);
}
const response = await fetch("https://reggiodigital.com/api/v1/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.REGGIO_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "receipt-1042",
},
body: JSON.stringify({
"from": "hello@yourdomain.com",
"to": [
"success@simulator.amazonses.com"
],
"subject": "Your receipt",
"html": "<p>Thanks for your order.</p>",
"text": "Thanks for your order.",
"tags": {
"kind": "receipt",
"order": "1042"
}
}),
});
const body = await response.json();
if (!response.ok) {
throw new Error(body.error.message);
}
Checking what happened
GET /api/v1/emails/{id} returns a message's current state for 30 days after it was sent.
curl https://reggiodigital.com/api/v1/emails/MESSAGE_ID \
-H "Authorization: Bearer $REGGIO_API_KEY"
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.reggio.key'))
->acceptJson()
->get('https://reggiodigital.com/api/v1/emails/MESSAGE_ID');
if ($response->failed()) {
throw new RuntimeException($response->json('error.message'));
}
$body = $response->json();
$ch = curl_init('https://reggiodigital.com/api/v1/emails/MESSAGE_ID');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer '.getenv('REGGIO_API_KEY'),
],
]);
$body = json_decode((string) curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($status >= 400) {
throw new RuntimeException($body['error']['message']);
}
const response = await fetch("https://reggiodigital.com/api/v1/emails/MESSAGE_ID", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.REGGIO_API_KEY}`,
},
});
const body = await response.json();
if (!response.ok) {
throw new Error(body.error.message);
}
{
"id": "0100019a1b2c3d4e-5f6a7b8c-1234-5678-9abc-def012345678-000000",
"from": "hello@yourdomain.com",
"to": ["customer@example.com"],
"subject": "Your receipt",
"tags": { "kind": "receipt", "order": "1042" },
"status": "bounced",
"status_description": "The receiving server refused this message and sent it back.",
"scheduled_at": null,
"sent_at": "2026-09-14T15:04:05+00:00",
"delivered_at": null,
"bounced_at": "2026-09-14T15:04:07+00:00",
"complained_at": null,
"bounce": {
"type": "Transient",
"subtype": "MailboxFull",
"permanent": false,
"summary": "Their mailbox is full.",
"what_to_do": "Nothing is wrong with the address. It may work again once they clear space, so it is worth trying later.",
"diagnostic": "552 5.2.2 Mailbox full"
},
"events": [
{ "type": "sent", "at": "2026-09-14T15:04:05+00:00", "detail": null },
{ "type": "bounced", "at": "2026-09-14T15:04:07+00:00", "detail": "552 5.2.2 Mailbox full" }
]
}
status is one of scheduled, queued, sent, delivered, delayed, bounced, complained, rejected, failed or canceled. Delivery is not instant: a message read back straight away usually says sent, and delivered follows within a minute or two. bounce is null unless the message bounced.
Rather than polling, most integrations use webhooks.
Testing without emailing anyone
These addresses behave in a fixed way and nothing sent to them leaves the mail network, so you can build bounce and complaint handling before a real customer is involved. They are metered and logged like any other send, and show up on the account's Sent page.
| Send to | What happens |
|---|---|
success@simulator.amazonses.com |
Delivered. |
bounce@simulator.amazonses.com |
A hard bounce, as if the mailbox did not exist. |
complaint@simulator.amazonses.com |
Delivered, then marked as spam. |
suppressionlist@simulator.amazonses.com |
Refused because the address is on a block list. |
Limits
- 120 requests a minute per account, shared with the newsletter API. See rate limits.
- Each send and each batch returns
X-Reggio-Monthly-QuotaandX-Reggio-Monthly-Remaining. Every recipient counts, includingccandbcc. - An account that reaches its sending ceiling gets
429withsend_rejecteduntil it is lifted. - 50 addresses per
to,ccorbcc; 100 messages per batch; 20 attachments; 28MB per message; 30 days ahead for scheduling; 30 days of lookups.