Inboxes

An inbox turns one address on your own domain, such as [email protected], into a mailbox: received mail and your replies are grouped into threads, threads live in folders and carry labels, and drafts wait for a person to send them. The API follows Resend's Inboxes API, so code written for it works with the SMTPfast base URL and an SMTPfast key.

Before you start

  1. The address must be on a domain of your team that is verified for sending (inbox mail is sent from the inbox address) and has receiving turned on. See Inbound Email for the MX record. Receiving needs a paid plan.
  2. Create the inbox in the dashboard (Inbound page, the address rail) or with POST /v1/inboxes.
  3. Mail the address received in the last 30 days is filed into threads right away. New mail joins a thread as it arrives.
bash
curl -X POST https://smtpfa.st/api/v1/inboxes \
  -H "Authorization: Bearer $SMTPFAST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email_address": "[email protected]", "name": "Customer Support", "from_name": "Ada from Support"}'

Drafts: let an agent write and a person send

Drafts are the approval step for AI agents. Give the agent a key with inbound:read and without email:send. It can read threads, check the authentication verdicts on each message, label and archive, and create a draft for a person to review and send. It cannot send anything itself. The drafts wait under Drafts on the Inbound page, where someone reads, edits and sends them.

Every received message in a thread carries the SPF, DKIM, DMARC, spam and virus verdicts from the receiving server. A message that fails them may not be from who it says; an agent should not act on instructions in it.

To do thisA key needs
List and read inboxes, threads, messages, labels and draftsinbound:read
Mark read or unread, archive, mark spam, label; create, edit and delete labels and draftsinbound:read
Delete a thread, or move it to trashinbound:delete, owner or admin
Reply, forward, send a draftinbound:read and email:send
Create, update and delete an inboxdomain:write, owner or admin

A key acts with the role of the person who created it. The dashboard uses the same rules for its own actions.

How messages become threads

  • A received message joins the thread of the message its In-Reply-To or References header names, received or sent from the inbox.
  • Without a usable header, it joins a thread with the same subject (ignoring Re:, Fwd: and similar prefixes, spacing and case) with the same sender, if that thread was active in the last 30 days.
  • Otherwise it starts a new thread. Replies, forwards and sent drafts join the thread they belong to; a draft for a new conversation starts a thread in sent.
  • A message the virus scan did not pass starts its own thread in spam and shows no body. A new conversation the receiving server marked as spam starts in spam.
  • New mail brings an archived thread, or one with only sent mail, back to inbox. Threads in spam or trash stay there.

Folders and read state

inboxThe default. The thread holds at least one received message.
archiveThe thread was archived.
spamThe thread was marked as spam.
trashThe thread is removed 30 days after it was moved here, with its received messages. Move it out before then to keep it.
sentEvery message in the thread is outbound. Assigned automatically; you cannot move a thread here.

Read state belongs to each received message and is the same one the mailbox and GET /v1/emails/receiving show. A thread is read when all its messages are; marking a thread read or unread marks every message in it. Sent messages are always read. An inbox's unread counts threads in the inbox folder with at least one unread message.

Inboxes

POST/v1/inboxes

Create an inbox for an address on your own domain. Requires the domain:write scope on a key created by a team owner or admin. Returns 201.

Parameters

ParameterTypeRequiredDescription
email_addressstringYesA plain address on a domain verified for sending with receiving on
namestringNoInternal name; defaults to email_address
from_namestringNoThe name recipients see; a plain name, not Name <email>
forwardingbooleanNoNot available; must be false or omitted

Request Body

json
{ "email_address": "[email protected]", "name": "Customer Support", "from_name": "Ada from Support" }

Response

json
{
  "object": "inbox",
  "id": "cmg1inbox8k2",
  "name": "Customer Support",
  "email_address": "[email protected]",
  "domain_id": "dom_xyz789",
  "receiving_address": null,
  "from_name": "Ada from Support",
  "unread": 3,
  "drafts": 1,
  "last_received": "2026-10-03T09:14:11.229Z",
  "created_at": "2026-10-01T08:00:00.000Z"
}
GET/v1/inboxes

The team's inboxes, newest first. Requires the inbound:read scope. Pages with limit (1 to 100, default 20), after and before.

Response

json
{
  "object": "list",
  "has_more": false,
  "data": [
    { "id": "cmg1inbox8k2", "name": "Customer Support", "email_address": "[email protected]", "from_name": "Ada from Support", "unread": 3, "last_received": "2026-10-03T09:14:11.229Z", "domain_id": "dom_xyz789", "drafts": 1, "created_at": "2026-10-01T08:00:00.000Z" }
  ]
}
GET/v1/inboxes/:inbox_id

One inbox, by id or by its email address. Requires the inbound:read scope.

Response

json
{
  "object": "inbox",
  "id": "cmg1inbox8k2",
  "name": "Customer Support",
  "email_address": "[email protected]",
  "domain_id": "dom_xyz789",
  "receiving_address": null,
  "from_name": "Ada from Support",
  "unread": 3,
  "drafts": 1,
  "last_received": "2026-10-03T09:14:11.229Z",
  "created_at": "2026-10-01T08:00:00.000Z"
}
PATCH/v1/inboxes/:inbox_id

Change the internal name or the name recipients see. At least one of name or from_name; from_name: null clears it. Requires the domain:write scope on a key created by a team owner or admin.

Request Body

json
{ "from_name": "Acme Support" }

Response

json
{ "object": "inbox", "id": "cmg1inbox8k2" }
DELETE/v1/inboxes/:inbox_id

Delete the inbox with its threads, labels and drafts. Received messages stay in GET /v1/emails/receiving until retention removes them, and the address keeps receiving. Requires the domain:write scope on a key created by a team owner or admin.

Response

json
{ "object": "inbox", "id": "cmg1inbox8k2", "deleted": true }

Threads

GET/v1/inboxes/:inbox_id/threads

Threads in one folder, most recently active first. Requires the inbound:read scope.

Parameters

ParameterTypeRequiredDescription
folderstringNoinbox (default), archive, spam, sent or trash
querystringNoCase-insensitive text in the subject or a label name, up to 256 characters
labelstring[]NoLabel ids; threads carrying any of them. Repeat the parameter or separate with commas
limit, after, beforenumber, stringNoCursor paging; after returns older threads

Response

json
{ "object": "list", "has_more": true, "data": [{
    "id": "cmg1thr4d21",
    "subject": "Refund for order 1041",
    "from": "Ada Lovelace <[email protected]>",
    "to": ["[email protected]"],
    "cc": [],
    "bcc": [],
    "labels": [{ "id": "cmg1lbl7f9c", "name": "Urgent", "color": "crimson" }],
    "message_count": 2,
    "has_attachment": true,
    "has_draft": false,
    "read": false,
    "received_at": "2026-10-03T09:14:11.229Z",
    "folder": "inbox",
    "unread_count": 1,
    "snippet": "Could I get a refund for order 1041?"
  }] }
GET/v1/inboxes/:inbox_id/threads/:thread_id

The thread summary. Requires the inbound:read scope.

Response

json
{
  "object": "inbox_thread",
  "id": "cmg1thr4d21",
  "subject": "Refund for order 1041",
  "folder": "inbox",
  "labels": [{ "id": "cmg1lbl7f9c", "name": "Urgent", "color": "crimson" }],
  "read": false,
  "received_at": "2026-10-03T09:14:11.229Z",
  "delete_after": null
}
PATCH/v1/inboxes/:inbox_id/threads/:thread_id

Mark read or unread, move between folders, apply or remove a label. Returns the thread. Requires the inbound:read scope; moving to trash needs what DELETE needs.

Parameters

ParameterTypeRequiredDescription
readbooleanNoMarks every message read or unread
folderstringNoinbox, archive, spam or trash
label_idstringNoApply this label of the same inbox
remove_label_idstringNoRemove this label (not in Resend)

Request Body

json
{ "read": true, "folder": "archive" }
DELETE/v1/inboxes/:inbox_id/threads/:thread_id

Move the thread to trash; it is removed 30 days later. deleted is false when it was already in trash. Requires the inbound:delete scope on a key created by a team owner or admin.

Response

json
{ "object": "inbox_thread", "id": "cmg1thr4d21", "deleted": true }
GET/v1/inboxes/:inbox_id/threads/:thread_id/emails

The messages of a thread, received and sent, oldest first; after returns newer ones. For a long thread, latest=true returns the newest page first (not in Resend); page back with before. Reading does not mark them read. Requires the inbound:read scope.

Response

json
{ "object": "list", "has_more": false, "data": [{
    "id": "cmg1dlv5b1a",
    "direction": "inbound",
    "from": "Ada Lovelace <[email protected]>",
    "to": ["[email protected]"],
    "cc": [],
    "bcc": [],
    "reply_to": ["[email protected]"],
    "subject": "Refund for order 1041",
    "message_id": "<[email protected]>",
    "html": "<p>Could I get a refund for order 1041?</p>",
    "text": "Could I get a refund for order 1041?",
    "attachments": [{ "id": "att_9d4f2b81", "filename": "receipt.pdf", "size": 20841, "content_type": "application/pdf" }],
    "read": false,
    "received_at": "2026-10-03T09:14:11.229Z",
    "status": "delivered",
    "verdicts": { "spf": "PASS", "dkim": "PASS", "dmarc": "PASS", "spam": "PASS", "virus": "PASS" },
    "in_reply_to": null
  }] }
GET/v1/inboxes/:inbox_id/threads/:thread_id/emails/:email_id

One message. For a received message the id is also its id under /v1/emails/receiving, so its attachments download through GET /v1/emails/receiving/:id/attachments. Requires the inbound:read scope.

Response

json
{
  "id": "cmg1dlv5b1a",
  "direction": "inbound",
  "from": "Ada Lovelace <[email protected]>",
  "to": ["[email protected]"],
  "cc": [],
  "bcc": [],
  "reply_to": ["[email protected]"],
  "subject": "Refund for order 1041",
  "message_id": "<[email protected]>",
  "html": "<p>Could I get a refund for order 1041?</p>",
  "text": "Could I get a refund for order 1041?",
  "attachments": [{ "id": "att_9d4f2b81", "filename": "receipt.pdf", "size": 20841, "content_type": "application/pdf" }],
  "read": false,
  "received_at": "2026-10-03T09:14:11.229Z",
  "status": "delivered",
  "verdicts": { "spf": "PASS", "dkim": "PASS", "dmarc": "PASS", "spam": "PASS", "virus": "PASS" },
  "in_reply_to": null
}

Reply and forward

Both send real email at once, from the inbox address, through the normal send pipeline: the verified domain, the suppression list, plan limits, credits and the safety checks all apply, and an Idempotency-Key header makes a retry safe. Both need the inbound:read and email:send scopes. To have a person approve the text first, create a draft instead.

POST/v1/inboxes/:inbox_id/threads/:thread_id/emails/:email_id/reply

Reply to one message. It goes to the message's Reply-To or From (or, for a sent message, to its recipients) with In-Reply-To and References set. Add cc and bcc; 50 recipients at most. subject defaults to Re: plus the thread subject.

Parameters

ParameterTypeRequiredDescription
html / textstringYesAt least one
cc, bccstring | string[]NoNot copied from the message
subjectstringNoUp to 900 characters

Request Body

json
{ "text": "Refund issued for order 1041.", "cc": ["[email protected]"] }

Response

json
{
  "id": "cmg1eml6a0c",
  "email_id": "cmg1eml6a0c",
  "direction": "outbound",
  "from": "Ada from Support <[email protected]>",
  "to": ["[email protected]"],
  "cc": ["[email protected]"],
  "bcc": [],
  "reply_to": [],
  "subject": "Re: Refund for order 1041",
  "message_id": null,
  "html": null,
  "text": "Refund issued for order 1041.",
  "attachments": [],
  "read": true,
  "received_at": "2026-10-03T09:25:04.110Z",
  "status": "queued",
  "thread_id": "cmg1thr4d21"
}
POST/v1/inboxes/:inbox_id/threads/:thread_id/emails/:email_id/forward

Forward one message with the original quoted under a forwarded-message banner. to is required. html or text is a note above the original. subject defaults to Fwd: plus the thread subject. The forward joins the thread.

Parameters

ParameterTypeRequiredDescription
tostring | string[]YesRecipients; 50 at most with cc and bcc
cc, bccstring | string[]NoOptional
html / textstringNoA note above the original
subjectstringNoUp to 900 characters

Request Body

json
{ "to": ["[email protected]"], "text": "Flagging this refund request for you." }

Labels

Named, colored tags on one inbox. Colors: cyan, teal, grass, lime, yellow, orange, iris, plum, crimson, bronze and mauve (the default). Names are unique per inbox, ignoring case. All label calls need the inbound:read scope.

POST/v1/inboxes/:inbox_id/labels

Create a label. Returns 201.

Request Body

json
{ "name": "Urgent", "color": "crimson" }

Response

json
{ "object": "inbox_label", "id": "cmg1lbl7f9c", "name": "Urgent", "color": "crimson", "created_at": "2026-10-03T09:07:42.881Z" }
GET/v1/inboxes/:inbox_id/labels

Every label on the inbox, oldest first.

Response

json
{ "object": "list", "data": [{ "id": "cmg1lbl7f9c", "name": "Urgent", "color": "crimson", "created_at": "2026-10-03T09:07:42.881Z" }] }
PATCH/v1/inboxes/:inbox_id/labels/:label_id

Rename or recolor a label.

Request Body

json
{ "color": "orange" }

Response

json
{ "object": "inbox_label", "id": "cmg1lbl7f9c" }
DELETE/v1/inboxes/:inbox_id/labels/:label_id

Remove the label from the inbox and from every thread. No message is removed.

Response

json
{ "object": "inbox_label", "id": "cmg1lbl7f9c", "deleted": true }

Drafts

Create a draft for a person to review and send. Creating, editing and deleting drafts needs only the inbound:read scope; sending one needs email:send as well.

POST/v1/inboxes/:inbox_id/drafts

Create a draft. Omit thread_id for a new conversation; send thread_id and reply_to_email_id together for a reply (recipients are not copied, set to before sending). At least one field must be non-empty; 50 recipients at most. Returns 201, or 200 when an unsent reply draft for that message already exists.

Parameters

ParameterTypeRequiredDescription
to, cc, bccstring | string[] | nullNoRecipients
subjectstring | nullNoUp to 900 characters
html, textstring | nullNoThe body
thread_idstringNoReply drafts: the thread
reply_to_email_idstringNoReply drafts: the message being answered

Request Body

json
{ "thread_id": "cmg1thr4d21", "reply_to_email_id": "cmg1dlv5b1a", "to": ["[email protected]"], "text": "Refund issued for order 1041." }

Response

json
{
  "object": "inbox_draft",
  "id": "cmg1drf3a1e",
  "type": "reply",
  "to": ["[email protected]"],
  "cc": [],
  "bcc": [],
  "subject": null,
  "html": null,
  "text": "Refund issued for order 1041.",
  "thread_id": "cmg1thr4d21",
  "reply_to_email_id": "cmg1dlv5b1a",
  "email_id": null,
  "created_at": "2026-10-03T09:20:04.110Z",
  "updated_at": "2026-10-03T09:20:04.110Z",
  "created_via": "api",
  "sent_at": null,
  "revision": 0
}
GET/v1/inboxes/:inbox_id/drafts

Unsent drafts, most recently updated first, with a snippet of each body.

Response

json
{ "object": "list", "has_more": false, "data": [{ "id": "cmg1drf3a1e", "type": "reply", "to": ["[email protected]"], "cc": [], "bcc": [], "subject": null, "snippet": "Refund issued for order 1041.", "thread_id": "cmg1thr4d21", "reply_to_email_id": "cmg1dlv5b1a", "updated_at": "2026-10-03T09:20:04.110Z", "created_via": "api", "created_at": "2026-10-03T09:20:04.110Z" }] }
GET/v1/inboxes/:inbox_id/drafts/:draft_id

One draft, sent or not.

Response

json
{
  "object": "inbox_draft",
  "id": "cmg1drf3a1e",
  "type": "reply",
  "to": ["[email protected]"],
  "cc": [],
  "bcc": [],
  "subject": null,
  "html": null,
  "text": "Refund issued for order 1041.",
  "thread_id": "cmg1thr4d21",
  "reply_to_email_id": "cmg1dlv5b1a",
  "email_id": null,
  "created_at": "2026-10-03T09:20:04.110Z",
  "updated_at": "2026-10-03T09:20:04.110Z",
  "created_via": "api",
  "sent_at": null,
  "revision": 0
}
PATCH/v1/inboxes/:inbox_id/drafts/:draft_id

Omitted fields stay; null clears one. The draft must keep at least one non-empty field. A sent draft, or one being sent, returns 409. Pass revision to change it only if nobody else did since you read it (not in Resend).

Request Body

json
{ "subject": "Your refund", "cc": null, "revision": 0 }

Response

json
{
  "object": "inbox_draft",
  "id": "cmg1drf3a1e",
  "type": "reply",
  "to": ["[email protected]"],
  "cc": [],
  "bcc": [],
  "subject": null,
  "html": null,
  "text": "Refund issued for order 1041.",
  "thread_id": "cmg1thr4d21",
  "reply_to_email_id": "cmg1dlv5b1a",
  "email_id": null,
  "created_at": "2026-10-03T09:20:04.110Z",
  "updated_at": "2026-10-03T09:20:04.110Z",
  "created_via": "api",
  "sent_at": null,
  "revision": 0
}
DELETE/v1/inboxes/:inbox_id/drafts/:draft_id

Discard a draft.

Response

json
{ "object": "inbox_draft", "id": "cmg1drf3a1e", "deleted": true }
POST/v1/inboxes/:inbox_id/drafts/:draft_id/send

Send the draft from the inbox address through the normal send pipeline. It needs a recipient and a body, and a subject when it is not a reply. Pass the revision the person reviewed (not in Resend): if the draft changed since, for example because the agent edited it again, nothing is sent and the answer is 409 with code draft_changed. With no body it behaves as in Resend. Sending the same draft twice queues one email; a draft that was already sent returns 409. Requires the inbound:read and email:send scopes.

Request Body

json
{ "revision": 3 }

Response

json
{ "object": "inbox_draft", "id": "cmg1drf3a1e", "thread_id": "cmg1thr4d21", "email_id": "cmg1eml6a0c" }

Resend compatibility

Paths, objects (inbox, inbox_thread, inbox_label, inbox_draft), folders, label colors, read state, draft types and cursor paging match Resend's Inboxes API. Where SMTPfast differs:

forwardingNot available. SMTPfast never hands out addresses on a shared domain, so receiving_address is always null and forwarding: true returns 422. Publish the MX record for your domain instead.
subjectUp to 900 characters on replies, forwards and drafts, the limit of the send path. Resend allows 2000.
Reply to and fromA reply goes to the sender of the message, from the inbox address, as in Resend. Passing to or from on a reply is refused with 400 instead of being ignored, so a caller never believes it changed who receives the mail.
Forward attachmentsAttachments of the original are not included in a forward. Download them from the received email and send them with POST /v1/emails if needed.
Reply draftsCreating a reply draft for a message that already has an unsent one returns that draft with 200, updated with the fields you send.
Errors and idsErrors use SMTPfast's shape, { "error": "..." }. Ids are SMTPfast ids, not UUIDs.
Added fieldsThread emails carry status and verdicts (SPF, DKIM, DMARC, spam, virus). Thread list items add folder, unread_count and snippet; threads add received_at and delete_after; drafts add created_via, sent_at, revision and thread_subject; reply and forward responses add thread_id. PATCH on a thread also takes remove_label_id; sending or updating a draft takes revision; listing thread emails takes latest=true. Nothing Resend returns is left out.