Messages & threads

Read incoming email as structured messages, follow conversations as threads, and reply from the same address.

Every email that reaches one of your inboxes becomes a structured message — parsed sender, subject, both bodies, attachment metadata — with no raw MIME to handle. A message that draws replies becomes a thread, so the whole conversation stays in one place.

You can work with inbound mail two ways: subscribe to the message.received webhook and act the moment mail arrives, or pull messages on demand with the endpoints on this page. Every read uses the messages:read scope.

List messages

GET /v1/messages returns messages newest-first — across every inbox, or filtered to one.

curl "https://api.dairo.app/v1/messages?inboxId=7c9e6679-7425-40de-944b-e07fc1f90ae7&direction=inbound" \
  -H "Authorization: Bearer $DAIRO_API_KEY"
{
  "object": "list",
  "data": [
    {
      "object": "message",
      "id": "8a2fdc3b-51e2-4f6a-9c11-2b7e4d9a6f3c",
      "channel": "email",
      "inboxId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "threadId": "5f2d8c1a-7b3e-4a90-8d2c-6e1f4b7a9c05",
      "direction": "inbound",
      "status": "received",
      "from": { "address": "customer@example.com", "name": "Ada Lang" },
      "to": ["support@yourapp.com"],
      "subject": "Re: Your July invoice",
      "textPreview": "Thanks — one question about the line items…",
      "hasHtml": true,
      "hasAttachments": false,
      "receivedAt": "2026-07-10T14:32:07+00:00"
    }
  ],
  "pagination": { "nextCursor": null, "hasMore": false }
}
ParameterWhat it does
inboxIdOnly messages on one inbox.
threadIdOnly messages in one thread.
directioninbound or outbound.
channelOnly one channel: email, a2a, telegram, slack, or voice.
limitPage size, 1–100. Defaults to 25.
cursorThe pagination.nextCursor from the previous page — the pattern is on Paging through lists.

direction=outbound with no channel filter returns the outbound send ledger — per-recipient delivery status, bounces, and complaints — described in Track delivery. That view honors inboxId and limit; cursor or threadId on it is a 400. Add an explicit channel, e.g. channel=email&direction=outbound, to page the message shape shown here.

Fetch one message

List rows carry a textPreview. Fetch a single message for the full record — both bodies, parsed addresses, and attachment metadata.

curl https://api.dairo.app/v1/messages/8a2fdc3b-51e2-4f6a-9c11-2b7e4d9a6f3c \
  -H "Authorization: Bearer $DAIRO_API_KEY"

Message fields

FieldWhat it is
idStable message identifier.
channelHow the message traveled — email, a2a, telegram, slack, or voice (an answered phone call).
inboxId / threadIdThe inbox that handled the message, and the conversation it belongs to.
directioninbound or outbound.
statusreceived on inbound mail; sent on your outbound copies.
fromParsed sender: { "address", "name" }.
to / cc / bccRecipient addresses.
subjectThe subject line.
textPreviewThe first 160 characters of the text body — enough to scan a list.
hasHtml / hasAttachmentsWhether the full record carries an HTML body or attachments.
textBody / htmlBodyThe full bodies, returned when you fetch one message.
attachmentsAttachment metadata: id, filename, contentType, sizeBytes.
structured / extractionStatusTyped extraction results on inboxes with a schema — see Extract structured data.
channelMetadataChannel-specific metadata; its fields depend on the channel.
receivedAt / createdAtTimestamps.

Follow a conversation with threads

A thread is one conversation on an inbox: a message and the replies it collected. GET /v1/threads lists threads by most recent activity; GET /v1/threads/{threadId} returns one thread with its messages oldest-first, up to 100.

curl "https://api.dairo.app/v1/threads?inboxId=7c9e6679-7425-40de-944b-e07fc1f90ae7" \
  -H "Authorization: Bearer $DAIRO_API_KEY"

curl https://api.dairo.app/v1/threads/5f2d8c1a-7b3e-4a90-8d2c-6e1f4b7a9c05 \
  -H "Authorization: Bearer $DAIRO_API_KEY"
{
  "object": "thread",
  "id": "5f2d8c1a-7b3e-4a90-8d2c-6e1f4b7a9c05",
  "inboxId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "channel": "email",
  "subject": "Your July invoice",
  "messageCount": 2,
  "lastMessageAt": "2026-07-10T14:32:07+00:00",
  "lastMessagePreview": "Thanks — one question about the line items…",
  "messages": [
    { "object": "message", "id": "0c47a9e2-3d5b-4f81-8e6a-1b9d2c4f7a30", "direction": "outbound", "subject": "Your July invoice" },
    { "object": "message", "id": "8a2fdc3b-51e2-4f6a-9c11-2b7e4d9a6f3c", "direction": "inbound", "subject": "Re: Your July invoice" }
  ]
}

Reply in a thread

When you email someone and they answer, the reply lands in the thread of the message it answers — Dairo connects it through standard email reply headers. To answer, send from the same inbox with replyToMessageId set to the message you are answering: Dairo sets the reply headers from it and files your reply in the same thread. Keep the subject with a Re: prefix so the recipient's mail client keeps the conversation together.

cURL
curl -X POST https://api.dairo.app/v1/messages \
  -H "Authorization: Bearer $DAIRO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "inboxId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "to": "customer@example.com",
    "subject": "Re: Your July invoice",
    "text": "Good catch — the second line item is the annual discount.",
    "replyToMessageId": "8a2fdc3b-51e2-4f6a-9c11-2b7e4d9a6f3c"
  }'

A replyToMessageId you cannot see is a 404. To reply to mail outside Dairo, pass the raw inReplyTo Message-ID (and optionally references) instead.

Handle attachments

Files on inbound mail arrive as metadata only; when you need one, you mint a short-lived download link or fetch the bytes. Inbound files come from arbitrary senders, so treat them as untrusted — the endpoints and the handling rules live on Attachments.

Delete messages

POST /v1/messages/batch-delete removes up to 1,000 messages in one call and needs the messages:read scope. The response reports per-id results — an id you don't own is returned under failed and never aborts the rest of the batch.

{
  "object": "batch_delete_result",
  "deleted": ["8a2fdc3b-51e2-4f6a-9c11-2b7e4d9a6f3c"],
  "failed": [{ "id": "00000000-0000-0000-0000-000000000000", "error": "not found" }]
}

Next steps

On this page