Skip to Content
EmailSend Email

Send Email

POST /v1/emails

Queues 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

HeaderRequiredDescription
AuthorizationYesBearer followed by your API key.
Idempotency-KeyNoMakes the request safe to retry. See Idempotency.

Request body

FieldTypeRequiredDescription
fromstringYesSender. Either address@domain.com or Name <address@domain.com>. The domain must be registered and verified on your account.
tostring | string[]Yes1 to 50 recipient addresses.
ccstring | string[]No1 to 50 CC addresses.
bccstring | string[]No1 to 50 BCC addresses.
reply_tostring | string[]NoReply-To address(es).
subjectstringYes1 to 998 characters.
htmlstringYes*HTML body, up to 1,000,000 characters.
textstringYes*Plain-text body, up to 1,000,000 characters.
marketingbooleanNoMarks 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.
tagsarrayNoUp 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.
headersobjectNoCustom message headers as { "Header-Name": "value" }, up to 20. See Custom headers.
scheduled_atstringNoSend at this time instead of now. ISO 8601 with an offset, up to 30 days ahead. See Schedule Email.
attachmentsarrayNoUp 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

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-ID and Gmail shows them separately.
  • Point List-Unsubscribe at your own page on a transactional email. Marketing emails already carry hosted unsubscribe headers, see Marketing Email, and those take precedence.
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, Received and the Resent-* 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:

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.

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 the id.
  • The same key with a different body is rejected with 409 invalid_idempotent_request. Change the key or send the original body.
  • A retry that arrives while the first request is still running gets 409 concurrent_idempotent_requests. Wait a moment and retry.
  • Only a successful 202 is remembered. If the first request failed with a 4xx or 5xx, 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

StatusCodeCause
400invalid_jsonBody is not valid JSON.
400invalid_idempotency_keyIdempotency-Key is empty or longer than 256 characters.
413payload_too_largeThe request body is over 43 MB. Attachments may total 30 MB before base64 encoding.
422validation_errorA field failed validation. The message names the field, for example a scheduled_at in the past.
422invalid_attachmentAn attachment has a blocked file type, is over the size budget, or its path could not be fetched.
403domain_not_foundThe from domain is not registered on your account.
403domain_not_verifiedThe from domain exists but is not verified yet.
409invalid_idempotent_requestIdempotency-Key was already used with a different body.
409concurrent_idempotent_requestsThe first request with this Idempotency-Key is still running. Retry shortly.
401unauthorizedMissing, malformed, or revoked API key.
Last updated on