Email threads

List conversations

GET/api/v1/email/threads

GET /email/threads
curl -X GET 'https://www.unitpost.com/api/v1/email/threads' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'User-Agent: my-app/1.0'
Response · 200
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "object": "email_thread",
      "id": "id_123",
      "subject": "Welcome to Acme",
      "last_message_at": "2026-01-01T00:00:00.000Z",
      "last_direction": "inbound",
      "snippet": "string",
      "from": "you@yourdomain.com",
      "from_name": "string",
      "inbox_address": "string",
      "message_count": 1,
      "has_attachments": false,
      "flagged": false,
      "replied": false
    }
  ],
  "next_cursor": "string"
}

Your inbox's conversations, newest activity first, as Emails → Received shows them: each received message and the replies and forwards sent from it are one conversation. Every conversation is listed: read state and Spam are the dashboard's. q searches the subject, the sender and the opening words. Page with next_cursor as after. snippet is null when the newest message is one you sent (or a copy of your own send): sent bodies aren't on the API, and q never matches a withheld snippet, so a page can be shorter than limit while has_more is true. q shares the search rate policy. Any other query key is a 422. Requires the emails:read capability.

Parameters3 fields
FieldTypeDescription
qstring · querySearch the subject, sender and opening words.
limitinteger · query1–100 (default 50).
afterstring · queryThe previous page's `next_cursor`.
Response · 200 fields13 fields
FieldTypeDescription
objectrequiredstring—
idrequiredstring—
subjectrequiredstring | null—
last_message_atrequiredstring—
last_directionrequiredenuminbound | outbound—
snippetrequiredstring | nullThe newest received message's opening words; null when the newest message is one you sent.
fromrequiredstring | nullThe other party: the newest received message's sender.
from_namerequiredstring | null—
inbox_addressrequiredstring | nullYour address it came in on (the newest received message's To).
message_countrequiredinteger—
has_attachmentsrequiredboolean—
flaggedrequiredbooleanA received message was flagged spam or virus, or failed SPF, DKIM or DMARC.
repliedrequiredbooleanA reply or forward was sent from this conversation.
200401403422

Retrieve a conversation

GET/api/v1/email/threads/{id}

GET /email/threads/{id}
curl -X GET 'https://www.unitpost.com/api/v1/email/threads/thr_123' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'User-Agent: my-app/1.0'
Response · 200
{
  "object": "email_thread",
  "id": "id_123",
  "subject": "Welcome to Acme",
  "last_message_at": "2026-01-01T00:00:00.000Z",
  "sender": {
    "address": "string"
  },
  "older_hidden": false,
  "older_cursor": "string",
  "sent_with_unitpost": false,
  "messages": [
    {
      "object": "received_email",
      "direction": "inbound",
      "id": "id_123",
      "received_at": "2026-01-01T00:00:00.000Z",
      "from": "you@yourdomain.com",
      "from_name": "string",
      "to": [
        "customer@example.com"
      ],
      "cc": [
        "string"
      ],
      "reply_to": [
        "string"
      ],
      "subject": "Welcome to Acme",
      "message_id": "message_123",
      "text": "string",
      "html": "string",
      "verdicts": {
        "spam": "string",
        "virus": "string",
        "spf": "string",
        "dkim": "string",
        "dmarc": "string"
      },
      "size": 1,
      "attachments": [
        {
          "object": "received_attachment",
          "id": "id_123",
          "filename": "string",
          "content_type": "string",
          "content_disposition": "string",
          "content_id": "content_123",
          "size": 1
        }
      ],
      "sent_by_us": false,
      "duplicate": false
    }
  ]
}

One conversation, oldest message first: what you received (bodies, reply_to, verdicts, attachments) and what you sent from it (status and recipients, no body). A long conversation comes in pages, newest first: this read holds its newest limit messages (fewer when their bodies are large), and older_cursor passed as before reads the messages before them, as the inbox's "Show earlier messages" does. Answer a message with Reply to a conversation. Requires the emails:read capability.

Parameters3 fields
FieldTypeDescription
idrequiredstring · path—
limitinteger · queryMessages per read, 1–100 (default 50). A read stays under a size budget, so big messages can make it shorter.
beforestring · queryThe previous read's `older_cursor`: read the messages before those.
Response · 200 fields9 fields
FieldTypeDescription
objectrequiredstring—
idrequiredstring—
subjectrequiredstring | null—
last_message_atrequiredstring—
senderrequiredobject | nullThe other party; null when the conversation has no received mail.
older_hiddenrequiredbooleanOlder messages exist than the ones in `messages`; read them with `before` = `older_cursor`.
older_cursorrequiredstring | nullPass as `before` to read the messages before these (the next page back). Null when nothing older remains.
sent_with_unitpostrequiredbooleanA reply from here carries the "Sent with Unitpost" line (your plan decides).
messagesrequiredobject[]Oldest first. What you sent is metadata only: no body.
200401403404422

Delete a conversation

DELETE/api/v1/email/threads/{id}

DELETE /email/threads/{id}
curl -X DELETE 'https://www.unitpost.com/api/v1/email/threads/thr_123' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'User-Agent: my-app/1.0'
Response · 200
{
  "object": "email_thread",
  "id": "id_123",
  "deleted": true,
  "deleted_messages": 0
}

Delete a conversation for good, as the inbox's Delete does. Its received messages are erased (they still count toward usage, and never come back); replies and forwards sent from it stay on your sent emails. It can't be undone. Also requires emails:read. Requires the emails:send capability.

Parameters1 field
FieldTypeDescription
idrequiredstring · path—
Response · 200 fields4 fields
FieldTypeDescription
objectrequiredenumemail_thread—
idrequiredstring—
deletedrequiredenumtrue—
deleted_messagesrequiredintegerReceived messages erased.
200401403404

Reply to a conversation

POST/api/v1/email/threads/{id}/reply

POST /email/threads/{id}/reply
curl -X POST 'https://www.unitpost.com/api/v1/email/threads/thr_123/reply' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'User-Agent: my-app/1.0' \
  -H 'Content-Type: application/json' \
  -d '{ }'
Response · 200
{
  "object": "email",
  "id": "id_123",
  "status": "queued",
  "thread_id": "thread_123"
}

Reply, reply all or forward one received message of the conversation (received_email_id), as the inbox does: your text and/or html go on top and the original is quoted under them; a reply carries In-Reply-To and References; a forward carries the attachments you list in forward_attachment_ids. The message joins the conversation and the conversation is marked read. For a reply, send to the message's reply_to addresses, else its from. It goes out now (no scheduled_at) through the same checks as any send. Pass an Idempotency-Key: a retry with it never sends twice. A copy of one of your own sends (sent_by_us) is never forwarded or quoted, since what you send isn't readable through the API: a reply to it goes without the quote, and a forward of it is a 403. Also requires emails:read (a reply quotes mail). Requires the emails:send capability.

Parameters2 fields
FieldTypeDescription
idrequiredstring · path—
Idempotency-Keystring · headerAny unique string (1–256 characters, e.g. a UUID). Retrying with the same key and the same body within 24 hours returns the first result instead of sending again; the same key with a different body is `409 invalid_idempotent_request`, and one still in flight is `409 concurrent_idempotent_requests`.
Request body13 fields
FieldTypeDescription
received_email_idrequiredstringThe received message (`inb_…`) in this conversation to answer or forward.
moderequiredenumreply | reply_all | forward`reply` (the sender), `reply_all` (everyone on it), or `forward` (to someone new, with the original's attachments you pick).
fromrequiredstringOne of your sending addresses, e.g. the address the message came in on.
torequiredstring | string[]For a reply: the message's `reply_to`, else its `from`. Up to 50.
ccstring | string[]Copy recipients, up to 50.
bccstring | string[]Blind-copy recipients, up to 50.
subjectrequiredstringUsually `Re: <their subject>` (or `Fwd:`).
textstringYour words, plain text. The original is quoted under them.
htmlstringYour words, HTML. The original is quoted under them.
include_quotebooleanQuote the original under a reply (default true; a forward always carries it). A copy of one of your own sends (`sent_by_us`) is never quoted: left out, it defaults to false there, and `true` is a `403`.
forward_attachment_idsstring[]Forward only: the original's attachments to carry over (their `id`s).
attachmentsobject[]Files to attach: `content` (base64) or `path` (an https URL), up to 20 with the carried-over ones.
trackingbooleanOpen and click tracking for this message; omitted = the sending domain's default.
Response · 200 fields4 fields
FieldTypeDescription
objectrequiredenumemail—
idrequiredstringThe sent email (`email_…`); read it with `GET /email/{id}`.
statusrequiredenumqueued | scheduled—
thread_idrequiredstring—
200400401403404409422429

Search docs and guides

Search the docs and product guides.