Emails

Send transactional and marketing emails and retrieve their delivery status.

SMTP Bridge

Legacy systems can submit mail over SMTP and still use the same verified domains, rate limits, suppression checks, queueing, logs, and webhooks as the HTTP API.

text
Host: smtp.smtpfa.st
Port: 587 or 2525
Security: STARTTLS
Username: smtpfast
Password: YOUR_SMTPFAST_API_KEY

The API key must include the email:send scope. Port 2525 is available for environments that block standard SMTP submission ports.

Attachments and inline images (parts with a Content-ID) go through as the API's attachments: up to 10 files, 10 MB each. A message sent over SMTP can be up to 10 MB in total.

Mail sent over SMTP is marketing unless you label it. To mark a message as transactional, add the header X-SMTPfast-Category: transactional. SMTPfast reads it and removes it before sending.

To send one message without tracking, add X-SMTPfast-Track-Clicks: false and X-SMTPfast-Track-Opens: false. They are removed before sending too.

POST/v1/emails

Send a single email. The request accepts all standard fields plus Resend-compatible fields like tags and headers.

The from address must belong to a verified domain.

Parameters

ParameterTypeRequiredDescription
fromstringYesSender email address (must be on a verified domain). Optional with a template that sets one.
tostring | string[]YesRecipient email address or array of addresses. Addresses on domains reserved for documentation and tests (example.com, example.net, example.org, .test, .example, .invalid, .localhost) are refused with a 400, in cc and bcc too. Broadcasts skip them as invalid. For a test send, use [email protected].
subjectstringYesEmail subject line. Optional with a template that sets one.
htmlstringNoHTML body of the email
textstringNoPlain text fallback body
ccstring | string[]NoCC recipients
bccstring | string[]NoBCC recipients
reply_tostring | string[]NoReply-to address(es)
tagsarrayNoArray of { name, value } pairs for categorization
headersobjectNoCustom email headers as key-value pairs
scheduled_atstringNoISO 8601 time to send at. Must be in the future and within 30 days.
attachmentsarrayNoUp to 10 files. Each needs filename and base64 content, and may set content_type, content_id and inline. 10MB per file, 25MB per message.
templateobjectNoSend a published template instead of html and text: { id, variables }. id is the template id or alias. from, subject and reply_to then default to the template's. See the Templates page.
categorystringNo"transactional" or "marketing". Defaults to marketing. Label receipts, password resets and other transactional mail "transactional" so a marketing opt-out does not block them. Any other value is a 400. See Marketing and transactional email below.
track_clicksbooleanNofalse sends this email's links exactly as written; true routes them through the click tracker. Left out, your account's tracking settings apply. Not in Resend, which sets tracking per domain. See Tracking per email below.
track_opensbooleanNofalse sends this email without the open-tracking pixel; true adds it. Left out, your account's tracking settings apply. Not in Resend.

Request Body

json
{
  "from": "[email protected]",
  "to": ["[email protected]"],
  "cc": ["[email protected]"],
  "reply_to": "[email protected]",
  "subject": "Welcome!",
  "html": "<h1>Hello!</h1><p>Welcome to our platform.</p>",
  "text": "Hello! Welcome to our platform.",
  "tags": [
    { "name": "category", "value": "onboarding" }
  ],
  "headers": {
    "X-Entity-Ref-ID": "abc-123"
  }
}

Response

json
{
  "id": "email_abc123"
}

Marketing and transactional email

Every email is either marketing or transactional. Email is marketing unless you label it transactional, so unlabelled mail is sent as it always was. The category decides two things:

  • Who it reaches. When a recipient unsubscribes, they stop getting marketing email from you, but email you label transactional still reaches them. Hard bounces and complaints block both, and so do manual suppressions by default; a manual suppression you add with "scope": "marketing" blocks marketing email only. See Suppressions.
  • The unsubscribe header. Marketing email carries the one-click List-Unsubscribe and List-Unsubscribe-Post headers that Gmail and Yahoo ask bulk senders for. Transactional email has neither.

How the category is chosen:

  1. An email is transactional only when you label it so: "category": "transactional" in an API, batch or template send, or the header X-SMTPfast-Category: transactional over SMTP. If you pass both, the field wins.
  2. An email whose html or text contains an unsubscribe placeholder ({{unsubscribe_url}} or {{{RESEND_UNSUBSCRIBE_URL}}}) is always marketing, even when labelled transactional, so the link in it can stop it. For a template send, the rendered body is checked.
  3. Everything else is marketing: unlabelled email, broadcasts, broadcast test sends and signup form emails.

Label password resets, receipts and other transactional mail with "category": "transactional" (or the X-SMTPfast-Category header) so a marketing opt-out does not block them. Unlabelled mail is treated as marketing, as before. The template's own category label does not affect this.

json
{
  "from": "[email protected]",
  "to": "[email protected]",
  "subject": "Reset your password",
  "html": "<p>Use this link to choose a new password.</p>",
  "category": "transactional"
}

What counts as transactional

Transactional email is mail the recipient needs because of something they did or an account they hold: receipts, password resets, security and account alerts. Promotions, newsletters and announcements are marketing. Labelling promotional email as transactional to reach people who unsubscribed is not allowed.

The category is stored on the email and returned as category by GET /v1/emails and GET /v1/emails/:id.

Tracking per email

Open and click tracking follow your account settings (Settings, Email Tracking), which apply to every email sent through the API and SMTP. Broadcasts have their own tracking settings. Click tracking sends each link through a redirect and records the full URL, which you can read back with GET /v1/emails/clicked-links. For a link that carries a secret, such as a password reset or sign-in token, turn tracking off so the token stays out of the redirect and the log. There are four ways, from the narrowest:

  1. One link. Add data-smtpfast-track="off" to the <a> tag. That link goes out exactly as written, the others in the email are still tracked, and the attribute is removed before sending.
  2. One email, over the API. "track_clicks": false and "track_opens": false on POST /v1/emails or on a batch item. true turns tracking on for that email even when your account has it off.
  3. One email, over SMTP. The headers X-SMTPfast-Track-Clicks: false and X-SMTPfast-Track-Opens: false (or true). They are removed before sending. With the API you can also pass them in headers; a field wins over its header.
  4. All transactional email. Turn off "Track transactional email" in Settings, Email Tracking. Email labelled transactional then goes out untracked unless the email itself asks for tracking.
html
<p>Hi Ada, use this button to choose a new password:</p>
<a data-smtpfast-track="off" href="https://yourapp.com/reset?token=8f3a...">Reset password</a>
<p>Questions? <a href="https://yourapp.com/help">Visit the help center</a></p>

Click tracking rewrites links written as <a href="https://..."> in the HTML part, whatever the link text says. A URL written as plain text, and the plain-text part of an email, are never rewritten; a mail app may still turn such a URL into a link. Any value other than true or false in a field or header is refused with a 400; a null field is the same as leaving it out. The fields apply to POST /v1/emails and batch items. Replies and forwards sent from an inbox follow your account settings, and the per-link attribute works in them too.

POST/v1/emails/batch

Send up to 100 emails in a single request. The body is a JSON array of email objects, each accepting the same fields as the single-send endpoint (from, to, subject, html/text or template, cc, bcc, reply_to, tags, headers, scheduled_at, category). Each email is queued and dispatched independently.

Validation runs on every item before any are queued, so a malformed item rejects the entire batch with a 400. Recipients on your suppression list are dropped per row; if every recipient on a single row is suppressed, that row is recorded with status "failed" while the rest of the batch still sends. The whole batch counts against your monthly quota up-front. The batch is accepted whole or not at all: a 4xx means nothing was queued, and there is no partial success. Send an Idempotency-Key header to make retries safe: after a 5xx or a lost response, repeat the request with the same key and body within 24 hours and you get the first answer if the batch was accepted, or a fresh attempt if it was not. The same key with a different body is a 409. The response also carries data, one { id } per item in request order, the shape Resend returns.

Request Body

json
[
  {
    "from": "[email protected]",
    "to": "[email protected]",
    "subject": "Welcome!",
    "html": "<h1>Hello User 1!</h1>",
    "tags": [{ "name": "campaign", "value": "welcome" }]
  },
  {
    "from": "[email protected]",
    "to": ["[email protected]", "[email protected]"],
    "subject": "Weekly digest",
    "html": "<h1>Your week</h1>",
    "scheduled_at": "2026-04-10T14:00:00Z"
  }
]

Response

json
{
  "batch_id": "batch_abc123",
  "count": 2,
  "emails": [
    { "id": "email_abc123", "status": "queued" },
    { "id": "email_def456", "status": "scheduled" }
  ],
  "data": [
    { "id": "email_abc123" },
    { "id": "email_def456" }
  ]
}
GET/v1/emails

List sent emails, newest first. Bodies are not included; fetch one by id for those. Requires the email:read scope.

Parameters

ParameterTypeRequiredDescription
limitintegerNoPage size, 1-100. Defaults to 50.
pageintegerNo1-based page number. Ignored when a cursor is given.
afterstringNoReturn emails older than this id. Not included in the result. Use either after or before, not both.
beforestringNoReturn emails newer than this id.
statusstringNoFilter by current status, e.g. delivered, bounced, scheduled.
tostringNoExact recipient address.

Response

json
{
  "object": "list",
  "data": [
    {
      "id": "email_abc123",
      "from": "[email protected]",
      "to": ["[email protected]"],
      "subject": "Welcome!",
      "status": "delivered",
      "source": "api",
      "category": "marketing",
      "created_at": "2026-04-05T12:00:00Z",
      "sent_at": "2026-04-05T12:00:01Z",
      "delivered_at": "2026-04-05T12:00:03Z",
      "opened_at": null,
      "clicked_at": null,
      "bounced_at": null,
      "error_message": null
    }
  ],
  "page": 1,
  "limit": 50,
  "total": 1284,
  "has_more": true
}

To find what has not gone out yet, filter on status=scheduled. For per-event detail rather than per-email, use the event log, which needs the logs:read scope.

PATCH/v1/emails/:id

Reschedule an email that has not been sent yet. Only the send time can change; to change the content, cancel it and send again. Requires the email:send scope.

Parameters

ParameterTypeRequiredDescription
idstringYesThe email ID
scheduled_atstringYesNew send time in ISO 8601. Must be in the future and within 30 days.

Response

json
{
  "object": "email",
  "id": "email_abc123",
  "scheduled_at": "2026-04-09T09:00:00Z"
}
POST/v1/emails/:id/cancel

Stop an email that has not been sent yet. Works while it is scheduled or queued. Requires the email:send scope.

Parameters

ParameterTypeRequiredDescription
idstringYesThe email ID

Response

json
{
  "object": "email",
  "id": "email_abc123"
}

When cancelling is too late

Both of these return 409 once the send has started, and say which states they would have accepted. A message already handed to the provider cannot be recalled. Emails that belong to a broadcast are refused as well: cancel or reschedule the broadcast instead, so its recipient bookkeeping stays correct.

GET/v1/emails/:id

Retrieve the details and delivery events for a specific email.

Parameters

ParameterTypeRequiredDescription
idstringYesThe email ID returned from the send endpoint

Response

json
{
  "id": "email_abc123",
  "from": "[email protected]",
  "to": ["[email protected]"],
  "cc": [],
  "bcc": [],
  "subject": "Welcome!",
  "html": "<h1>Hello!</h1><a href='{{unsubscribe_url}}'>Unsubscribe</a>",
  "text": null,
  "created_at": "2026-04-05T12:00:00Z",
  "last_event": "unsubscribed",
  "body_cleared_at": null,
  "source": "api",
  "category": "marketing",
  "status": "delivered",
  "reply_to": null,
  "tags": [{ "name": "campaign", "value": "welcome" }],
  "message_id": "<[email protected]>",
  "ses_message_id": "0100019a1b2c3d4e-5f6a7b8c-0000-0000-0000-000000000000-000000",
  "sent_at": "2026-04-05T12:00:01Z",
  "delivered_at": "2026-04-05T12:00:03Z",
  "opened_at": null,
  "clicked_at": null,
  "bounced_at": null,
  "unsubscribed_at": "2026-04-06T08:14:22Z",
  "error_message": null,
  "events": [
    { "type": "queued", "metadata": null, "timestamp": "2026-04-05T12:00:00Z" },
    { "type": "sent", "metadata": null, "timestamp": "2026-04-05T12:00:01Z" },
    { "type": "delivered", "metadata": null, "timestamp": "2026-04-05T12:00:03Z" },
    { "type": "unsubscribed", "metadata": null, "timestamp": "2026-04-06T08:14:22Z" }
  ]
}

About unsubscribed

When the recipient hits the one-click List-Unsubscribe link (or the link from the unsubscribe placeholder) tied to a delivered message, SMTPfast records an unsubscribed event on the email and sets unsubscribed_at. The address is then suppressed for marketing email only; email you label transactional still reaches it. The top-level status field intentionally stays at its terminal delivery value (delivered, bounced and so on), because an unsubscribe is something the recipient did after delivery, not a delivery outcome. Check unsubscribed_at (or filter the events array) to detect opt-outs without losing the original delivery state.

  • html and text are the stored body. For a send through the API, with your own HTML or a template, the unsubscribe placeholder is filled in when the email goes out, so it shows here unreplaced (template variables are already filled in). Broadcast and form emails are stored with the real link. See placeholders.
  • tags are the tags given at send time, as name/value pairs; an empty array when none were given.
  • message_id is the Message-ID header on your sending domain, the value a recipient's server and bounce reports quote. body_cleared_at is set once the retention window removes the body. Each event carries its metadata, such as the bounce type or the clicked link.
  • Counts and rates across many emails are in GET /v1/emails/metrics.

Error Codes

CodeMeaning
400Invalid request body (missing required fields, malformed email)
401Missing or invalid API key
403Domain not verified or sending not allowed for this key
404Email ID not found
429Rate limit exceeded