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
- The address must be on a domain of your team that has receiving turned on and can send (inbox mail is sent from the inbox address): a domain verified for sending, or a receiving subdomain of a domain you verified. See Inbound Email for the MX record. Receiving needs a paid plan.
- Create the inbox in the dashboard (Inbound page, the address rail) or with POST /v1/inboxes.
- Mail the address received in the last 30 days is filed into threads right away. New mail joins a thread as it arrives.
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 this | A key needs |
|---|---|
| List and read inboxes, threads, messages, labels and drafts | inbound:read |
| Mark read or unread, archive, mark spam, label; create, edit and delete labels and drafts | inbound:read |
| Delete a thread, or move it to trash | inbound:delete, owner or admin |
| Reply, forward, send a draft | inbound:read and email:send |
| Create, update and delete an inbox | domain: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
| inbox | The default. The thread holds at least one received message. |
| archive | The thread was archived. |
| spam | The thread was marked as spam. |
| trash | The thread is removed 30 days after it was moved here, with its received messages. Move it out before then to keep it. |
| sent | Every 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
/v1/inboxesCreate 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
| Parameter | Type | Required | Description |
|---|---|---|---|
| email_address | string | Yes | A plain address on a domain with receiving on that can send: verified, or a receiving subdomain of a verified domain |
| name | string | No | Internal name; defaults to email_address |
| from_name | string | No | The name recipients see; a plain name, not Name <email> |
| forwarding | boolean | No | Not available; must be false or omitted |
Request Body
{ "email_address": "[email protected]", "name": "Customer Support", "from_name": "Ada from Support" }Response
{
"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"
}/v1/inboxesThe team's inboxes, newest first. Requires the inbound:read scope. Pages with limit (1 to 100, default 20), after and before.
Response
{
"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" }
]
}/v1/inboxes/:inbox_idOne inbox, by id or by its email address. Requires the inbound:read scope.
Response
{
"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"
}/v1/inboxes/:inbox_idChange 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
{ "from_name": "Acme Support" }Response
{ "object": "inbox", "id": "cmg1inbox8k2" }/v1/inboxes/:inbox_idDelete 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
{ "object": "inbox", "id": "cmg1inbox8k2", "deleted": true }Threads
/v1/inboxes/:inbox_id/threadsThreads in one folder, most recently active first. Requires the inbound:read scope.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| folder | string | No | inbox (default), archive, spam, sent or trash |
| query | string | No | Case-insensitive text in the subject or a label name, up to 256 characters |
| label | string[] | No | Label ids; threads carrying any of them. Repeat the parameter or separate with commas |
| limit, after, before | number, string | No | Cursor paging; after returns older threads |
Response
{ "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?"
}] }/v1/inboxes/:inbox_id/threads/:thread_idThe thread summary. Requires the inbound:read scope.
Response
{
"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
}/v1/inboxes/:inbox_id/threads/:thread_idMark 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
| Parameter | Type | Required | Description |
|---|---|---|---|
| read | boolean | No | Marks every message read or unread |
| folder | string | No | inbox, archive, spam or trash |
| label_id | string | No | Apply this label of the same inbox |
| remove_label_id | string | No | Remove this label (not in Resend) |
Request Body
{ "read": true, "folder": "archive" }/v1/inboxes/:inbox_id/threads/:thread_idMove 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
{ "object": "inbox_thread", "id": "cmg1thr4d21", "deleted": true }/v1/inboxes/:inbox_id/threads/:thread_id/emailsThe 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
{ "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
}] }/v1/inboxes/:inbox_id/threads/:thread_id/emails/:email_idOne 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
{
"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.
/v1/inboxes/:inbox_id/threads/:thread_id/emails/:email_id/replyReply 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
| Parameter | Type | Required | Description |
|---|---|---|---|
| html / text | string | Yes | At least one |
| cc, bcc | string | string[] | No | Not copied from the message |
| subject | string | No | Up to 900 characters |
Request Body
{ "text": "Refund issued for order 1041.", "cc": ["[email protected]"] }Response
{
"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"
}/v1/inboxes/:inbox_id/threads/:thread_id/emails/:email_id/forwardForward 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
| Parameter | Type | Required | Description |
|---|---|---|---|
| to | string | string[] | Yes | Recipients; 50 at most with cc and bcc |
| cc, bcc | string | string[] | No | Optional |
| html / text | string | No | A note above the original |
| subject | string | No | Up to 900 characters |
Request Body
{ "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.
/v1/inboxes/:inbox_id/labelsCreate a label. Returns 201.
Request Body
{ "name": "Urgent", "color": "crimson" }Response
{ "object": "inbox_label", "id": "cmg1lbl7f9c", "name": "Urgent", "color": "crimson", "created_at": "2026-10-03T09:07:42.881Z" }/v1/inboxes/:inbox_id/labelsEvery label on the inbox, oldest first.
Response
{ "object": "list", "data": [{ "id": "cmg1lbl7f9c", "name": "Urgent", "color": "crimson", "created_at": "2026-10-03T09:07:42.881Z" }] }/v1/inboxes/:inbox_id/labels/:label_idOne label, in the same shape as the list. Not in Resend.
Response
{ "object": "inbox_label", "id": "cmg1lbl7f9c", "name": "Urgent", "color": "crimson", "created_at": "2026-10-03T09:07:42.881Z" }/v1/inboxes/:inbox_id/labels/:label_idRename or recolor a label.
Request Body
{ "color": "orange" }Response
{ "object": "inbox_label", "id": "cmg1lbl7f9c" }/v1/inboxes/:inbox_id/labels/:label_idRemove the label from the inbox and from every thread. No message is removed.
Response
{ "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.
/v1/inboxes/:inbox_id/draftsCreate 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
| Parameter | Type | Required | Description |
|---|---|---|---|
| to, cc, bcc | string | string[] | null | No | Recipients |
| subject | string | null | No | Up to 900 characters |
| html, text | string | null | No | The body |
| thread_id | string | No | Reply drafts: the thread |
| reply_to_email_id | string | No | Reply drafts: the message being answered |
Request Body
{ "thread_id": "cmg1thr4d21", "reply_to_email_id": "cmg1dlv5b1a", "to": ["[email protected]"], "text": "Refund issued for order 1041." }Response
{
"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
}/v1/inboxes/:inbox_id/draftsUnsent drafts, most recently updated first, with a snippet of each body.
Response
{ "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" }] }/v1/inboxes/:inbox_id/drafts/:draft_idOne draft, sent or not.
Response
{
"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
}/v1/inboxes/:inbox_id/drafts/:draft_idOmitted 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
{ "subject": "Your refund", "cc": null, "revision": 0 }Response
{
"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
}/v1/inboxes/:inbox_id/drafts/:draft_idDiscard a draft.
Response
{ "object": "inbox_draft", "id": "cmg1drf3a1e", "deleted": true }/v1/inboxes/:inbox_id/drafts/:draft_id/sendSend 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
{ "revision": 3 }Response
{ "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:
| forwarding | Not 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. |
| subject | Up to 900 characters on replies, forwards and drafts, the limit of the send path. Resend allows 2000. |
| Reply to and from | A 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 attachments | Attachments 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 drafts | Creating 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 ids | Errors use SMTPfast's shape, { "error": "..." }. Ids are SMTPfast ids, not UUIDs. |
| Added fields | Thread 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. |