Sending

Email

One message per request. Give it a body, or give it a template and the values to fill it with.

Who it is from

from takes a bare address or a display name: Acme <hello@yourdomain.com>. The domain must be one you have verified. Pass from_name instead if you would rather keep the two apart. Workspaces have a limit of one verified sending domain by default; contact support to raise it if your application sends from multiple domains.

Who it goes to

to, cc and bcc each take one address or a list. Addresses are lowercased before anything else happens. A recipient on your suppression list is dropped rather than sent to, and comes back in suppressed_recipients on the receipt — the send is not an error, it just did not go to that person. Hard bounces, spam complaints, and one-click unsubscribes land on your suppression list automatically. List, import, and (for hard bounces and manual entries) remove them through GET/POST /v1/suppressions and DELETE /v1/suppressions/:id. Complaints and unsubscribes cannot be cleared — those are opt-outs.

Body or template

Send subject with html, text, or both. Or name a template — by template_id or template_friendly_name, not both — and pass variables. A template brings its own subject. Every direct call to POST /v1/emails requires an Idempotency-Key header.

Send a templatecurl -X POST https://api.transmit.dev/v1/emails \
  -H "Authorization: Bearer $TRANSMIT_API_KEY" \
  -H "Idempotency-Key: order-1042" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Acme <hello@yourdomain.com>",
    "to": ["you@example.com"],
    "template_friendly_name": "order-shipped",
    "variables": { "name": "Dana", "tracking": "1Z999AA10123456784" }
  }'

The submission receipt

A successful send returns 202 Accepted with the submission receipt:

202 Accepted{
  "id": "0f9f8e7d-6c5b-4a39-8271-1a2b3c4d5e6f",
  "from": "Acme <hello@yourdomain.com>",
  "to": ["you@example.com"],
  "subject": "Order shipped",
  "status": "queued",
  "credits_consumed": 1,
  "suppressed_recipients": [],
  "scheduled_for": null,
  "created_at": "2026-09-03T09:41:02.118Z"
}

Keep the returned id: this is the message identifier for your send. Downstream delivery, bounce, open, and click events delivered to your webhook correlate back to this send as data.email_id.

Attachments

Up to twenty per message, two ways. Small files ride inline: a filename, base64 content and a MIME type. The send request is capped at 4.5 MB, and base64 adds a third, so inline suits files under about 3 MB. Anything larger is uploaded first: reserve a slot with POST /v1/attachments, PUT the raw bytes to the URL it returns, then send with upload_id in place of content. One upload can be attached to any number of sends for 24 hours. Either way, set disposition: "inline" and a contentId to reference a file from your HTML. We do not fetch attachments from a URL: what you upload is what goes.

Upload, then send# 1. Reserve a slot
curl -X POST https://api.transmit.dev/v1/attachments \
  -H "Authorization: Bearer $TRANSMIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "filename": "invoice-1042.pdf", "type": "application/pdf", "size_bytes": 184320 }'

# 2. PUT the file to the upload_url with the headers the response gives you
curl -X PUT "$UPLOAD_URL" -H "Content-Type: application/pdf" --data-binary @invoice-1042.pdf

# 3. Send, naming the upload
curl -X POST https://api.transmit.dev/v1/emails \
  -H "Authorization: Bearer $TRANSMIT_API_KEY" \
  -H "Idempotency-Key: invoice-1042" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Acme <billing@yourdomain.com>",
    "to": ["you@example.com"],
    "subject": "Invoice 1042",
    "text": "Attached.",
    "attachments": [{ "upload_id": "$UPLOAD_ID" }]
  }'

A file is at most 7 MB, and the whole message at most 10 MB once encoded; a send over that is refused with 413. Attachment data also counts against a monthly fair-use allowance, measured per recipient, because it is the one thing a provider bills by the byte. It is generous, and a send that would cross it is refused with attachment_allowance_exceeded rather than silently costing more. If your use is legitimately heavy, write in and we will raise it. Attachments are kept for thirty days after the send, then removed; the send record keeps the filename and size.

Tags

Pass tags as an array of up to 50 string labels (max 256 characters each). Use them to classify messages (for example, ["receipt", "order-1042"]). Tags are attached to downstream delivery telemetry.

Headers and one-click unsubscribe

headers takes a map of extra header names to values, for things like a ticket reference your inbound parser keys on. A handful are ours to write and are refused with reserved_header rather than overwritten. Chief among them are List-Unsubscribe and List-Unsubscribe-Post: broadcast deliveries carry both with an RFC 8058 one-click token minted for that recipient. Transactional deliveries do not — a one-click unsubscribe on an eSign link or password reset must not suppress later transactional mail. When a recipient unsubscribes from broadcast mail, Transmit records the suppression and fires an unsubscribed webhook. The full list of reserved headers is Bcc, Cc, Content-Type, Date, DKIM-Signature, From, List-Unsubscribe, List-Unsubscribe-Post, Message-ID, Received, Reply-To, Return-Path, Subject, To, plus anything beginning X-SES-.

Scheduling

scheduled_for takes an ISO 8601 instant with an offset. Credits are charged when we accept the message, not when it leaves, so a scheduled send is already paid for and the receipt says "status": "scheduled".

Tracking

track_opens and track_clicks default to on, and can be turned off per message. Opens and clicks arrive as webhook events and feed the analytics endpoint.

Sandbox

A test key (tr_test_…) simulates delivery on the same API. Nothing is handed to SES, no credits are charged, and a workspace without credits is not emailed about it. Choose the recipient to pick the outcome:

  • success@example.com — delivered
  • bounce@example.com — permanent bounce (bounce_type: Permanent)
  • transient@example.com or ooto@example.com — soft bounce (bounce_type: Transient), not added to the suppression list
  • click@example.com — delivered, then a click on https://example.com
  • complaint@example.com — complaint
  • unsubscribe@example.com — delivered, then an unsubscribed webhook (the one-click path). Send on the broadcast stream to match live List-Unsubscribe mail. Sandbox does not write a suppression.
  • transactional@example.com — delivered. Use with message_stream: "transactional" for mail that has no List-Unsubscribe headers.

Streams, rate limits, and warmup

message_stream defaults to transactional. Use broadcast for marketing mail and campaigns.

Transactional and broadcast mail share the same sending IP infrastructure, but are queued separately so a marketing send cannot delay a password reset. Automated per-domain warmup schedules apply exclusively to the broadcast stream: transactional sends bypass domain warmup quotas entirely. A new domain on warmup starts at 50 broadcast emails on day 1 and doubles over 12 days to 80,000 per day. Exceeding that day's quota returns 429 warmup_quota_exceeded.

Standard workspace limits are 10 sends per second, 10,000 per day, and 250,000 per month (alongside an API token rate limit of 120 requests per 10 seconds). If you are preparing for a burst campaign across multiple sites or migrating a pre-warmed domain, contact support in advance to adjust limits.