Send Email
POST /v1/emailsQueues an email from one of your verified domains. The request returns once the email is validated and stored. A worker sends it moments later and retries transient failures. For many emails at once, use Send Batch.
Request headers
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer followed by your API key. |
Idempotency-Key | No | Makes the request safe to retry. See Idempotency. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
from | string | Yes | Sender. Either address@domain.com or Name <address@domain.com>. The domain must be registered and verified on your account. |
to | string | string[] | Yes | 1 to 50 recipient addresses. |
cc | string | string[] | No | 1 to 50 CC addresses. |
bcc | string | string[] | No | 1 to 50 BCC addresses. |
reply_to | string | string[] | No | Reply-To address(es). |
subject | string | Yes | 1 to 998 characters. |
html | string | Yes* | HTML body, up to 1,000,000 characters. |
text | string | Yes* | Plain-text body, up to 1,000,000 characters. |
marketing | boolean | No | Marks this as a marketing email: {{{unsubscribe_url}}} in the body becomes a hosted unsubscribe link, List-Unsubscribe headers are added, and unsubscribed recipients are skipped. See Marketing Email. Defaults to false. |
tags | array | No | Up to 10 { "name", "value" } pairs for filtering in the dashboard and List Emails, e.g. { "name": "campaign", "value": "outreach-1" }. Letters, digits, _ and -, up to 256 characters each. Names must be unique per email. Tags are not sent to the recipient. |
headers | object | No | Custom message headers as { "Header-Name": "value" }, up to 20. See Custom headers. |
scheduled_at | string | No | Send at this time instead of now. ISO 8601 with an offset, up to 30 days ahead. See Schedule Email. |
attachments | array | No | Up to 20 files, 30 MB in total. Each has a filename and either content (base64) or path (public URL). See Attachments. |
* At least one of html or text is required. You can provide both.
Example
Node.js
import { Retransmit } from "retransmit.dev";
const retransmit = new Retransmit(process.env.RETRANSMIT_API_KEY);
const { data, error } = await retransmit.emails.send({
from: "Acme <hello@yourdomain.com>",
to: ["user@example.com"],
replyTo: "support@yourdomain.com",
subject: "Your receipt",
html: "<p>Thanks for your order!</p>",
text: "Thanks for your order!",
tags: [{ name: "category", value: "receipt" }],
});The SDK uses camelCase field names (replyTo). The HTTP API uses reply_to.
Custom headers
Retransmit sets every header a message needs. headers adds your own on top,
which is useful in two cases:
- Stop Gmail threading. Gmail groups emails with the same subject into one
conversation. Give each email a unique
X-Entity-Ref-IDand Gmail shows them separately. - Point
List-Unsubscribeat your own page on a transactional email. Marketing emails already carry hosted unsubscribe headers, see Marketing Email, and those take precedence.
Node.js
await retransmit.emails.send({
from: "Acme <hello@yourdomain.com>",
to: "user@example.com",
subject: "Your order shipped",
html: "<p>Order #4821 is on its way.</p>",
headers: {
"X-Entity-Ref-ID": "order_4821",
},
});Rules:
- Up to 20 headers per email.
- Names are printable ASCII without
:, up to 126 characters. Header names are case-insensitive, so a name may only appear once. - Values are a single line, up to 870 characters.
- Headers Retransmit or the provider set are rejected with a
validation_error:From,Sender,To,Cc,Bcc,Reply-To,Subject,Date,Message-ID,Return-Path,MIME-Version,Content-Type,Content-Transfer-Encoding,Content-Disposition,DKIM-Signature,Receivedand theResent-*family.
The headers you sent are returned by Get Email. Batch emails
accept the same headers field.
Scheduling
Pass scheduled_at to send later instead of now:
Node.js
await retransmit.emails.send({
from: "Acme <hello@yourdomain.com>",
to: "user@example.com",
subject: "Your trial ends tomorrow",
html: "<p>Here is what happens next.</p>",
scheduledAt: new Date("2026-09-20T09:00:00Z").toISOString(),
});A scheduled email can be moved or stopped until it goes out. See Schedule Email for the rules, rescheduling and cancelling.
Idempotency
A request can succeed on our side and still fail on yours: a timeout, a
dropped connection, a crashed worker. Retrying it blindly sends the email
twice. Send an Idempotency-Key header and retries become safe. Retransmit
remembers each key for 24 hours. A second request with the same key and the
same body gets the first response back, with the same id, and nothing is
queued again.
Use a value that identifies this exact email, such as a UUID or
<event>/<entity-id>. A key like welcome-user/123 means the welcome email
for user 123 goes out once, however many times that event fires.
Node.js
await retransmit.emails.send(
{
from: "Acme <hello@yourdomain.com>",
to: "user@example.com",
subject: "Welcome to Acme",
html: "<p>Glad to have you.</p>",
},
{ idempotencyKey: "welcome-user/123" },
);How it behaves:
- Keys are 1 to 256 characters and are scoped to your organization, so every API key you own shares the same keyspace.
- A replayed response carries the header
Idempotent-Replayed: true. The body is identical to the original, including theid. - The same key with a different body is rejected with
409invalid_idempotent_request. Change the key or send the original body. - A retry that arrives while the first request is still running gets
409concurrent_idempotent_requests. Wait a moment and retry. - Only a successful
202is remembered. If the first request failed with a4xxor5xx, the key is free again and a retry runs normally. - Keys expire after 24 hours. Reusing one after that starts a new send.
Send Batch accepts the same header for the whole batch.
Response
202 Accepted
{
"id": "em_xxxxxxxxxxxx",
"status": "queued",
"created_at": "2026-09-01T12:00:00.000Z",
"scheduled_at": null
}Keep the id. It identifies the email in Get Email and in
webhook events (emailId). The status moves from queued to sent when the
worker hands it to the provider, then follows delivery events. If every retry
fails, the status becomes failed with the provider error, and an
email.failed webhook fires.
With scheduled_at, the status is scheduled and scheduled_at echoes the
time you asked for:
{
"id": "em_xxxxxxxxxxxx",
"status": "scheduled",
"created_at": "2026-09-01T12:00:00.000Z",
"scheduled_at": "2026-09-20T09:00:00.000Z"
}Errors
| Status | Code | Cause |
|---|---|---|
400 | invalid_json | Body is not valid JSON. |
400 | invalid_idempotency_key | Idempotency-Key is empty or longer than 256 characters. |
413 | payload_too_large | The request body is over 43 MB. Attachments may total 30 MB before base64 encoding. |
422 | validation_error | A field failed validation. The message names the field, for example a scheduled_at in the past. |
422 | invalid_attachment | An attachment has a blocked file type, is over the size budget, or its path could not be fetched. |
403 | domain_not_found | The from domain is not registered on your account. |
403 | domain_not_verified | The from domain exists but is not verified yet. |
409 | invalid_idempotent_request | Idempotency-Key was already used with a different body. |
409 | concurrent_idempotent_requests | The first request with this Idempotency-Key is still running. Retry shortly. |
401 | unauthorized | Missing, malformed, or revoked API key. |