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 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.
- 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 verified for sending with receiving on |
| 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_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. |