Skip to content
View as Markdown

Retrieve a conversation ​

GET /v1/inbox/conversations/{conversation_id}

Returns a conversation with its messages, oldest first.

Scope: inbox:read

Access: The key's member must be able to use the campaign's email inbox in DialEcho (its message and email permissions) and see the contact; other conversations are left out.

messages are the thread's stored inbound and outbound emails (drafts are not included), ordered by created_at (then id), oldest first, up to limit at a time. When has_more is true, ask for the next page with messages_cursor set to next_messages_cursor. The first page fixes the window, so a message that arrives while you page waits for your next read. Every page returns the conversation again.

When the thread was opened by an email blast or a real-estate offer that is not itself a stored message, that first email is returned separately as origin_message (is_synthetic: true), so you can see what the contact answered. Its id is not a UUID (blast-job-<uuid> or offer-send-<uuid>) and it cannot be replied to. origin_status says whether there is one: available; not_applicable when the thread has no such origin or the first email is already in messages; or unavailable when it could not be loaded (the rest of the response is still complete).

The conversation must be one the key's member can see, otherwise 404.

Path parameters ​

NameTypeRequiredDescription
conversation_idUUIDYesThe conversation.

Query parameters ​

NameTypeRequiredDescription
limitintegerNoMessages per page. 1 to 100. Default: 100.
messages_cursorstringNoThe next_messages_cursor of the previous page of this conversation, unchanged. Up to 4,096 characters.

Response ​

Status: 200 OK

NameTypeDescription
conversationobjectThe conversation.
conversation.idUUIDThe conversation.
conversation.campaign_idUUIDThe conversation's campaign.
conversation.domainstringWhich inbox it is in: a contact campaign's (contact) or a real-estate offer campaign's (realestate). One of contact, realestate.
conversation.contact_idUUID | nullThe CRM contact, or null when it is not linked to one.
conversation.subjectstring | nullThe thread subject.
conversation.participant_emailstring | nullThe other party's address, following the member's email visibility.
conversation.statusstringThe conversation status shown in the inbox. Archiving sets closed and restoring sets open; a suppressed conversation stays suppressed. One of open, needs_review, booked, not_interested, closed, suppressed.
conversation.ai_statusstringWhere the AI email agent is with the thread, independent of status. Archiving sets disabled. One of pending, queued, drafted, sent, auto_replied, rejected, needs_review, disabled.
conversation.updated_attimestampWhen the conversation last changed, read and archive changes included (RFC 3339, UTC).
conversation.last_message_attimestamp | nullTime of its latest message.
conversation.last_inbound_attimestamp | nullTime of its latest inbound message.
conversation.readbooleantrue when it was read at or after its latest inbound message. A conversation without inbound messages is read.
conversation.archivedbooleantrue while it is archived.
conversation.reply_identitiesobjectThe two senders a reply can use (its send_as), as they are now. A preview, not a reservation: the sender is checked again when you reply.
conversation.reply_identities.campaign_mailboxobjectThe campaign mailbox the thread came from: the mailbox or account that sent the email the contact answered.
conversation.reply_identities.campaign_mailbox.emailstring | nullThe sender address; null when none is set up.
conversation.reply_identities.campaign_mailbox.availablebooleanWhether a reply can be sent from it now.
conversation.reply_identities.campaign_mailbox.reasonstringOnly present when available is false: not_configured (no usable sender is set up) or reconnect_required (the account or mailbox must be reconnected in DialEcho). One of not_configured, reconnect_required.
conversation.reply_identities.reply_addressobjectThe campaign's connected reply address, set in the campaign's email settings. On a real-estate conversation it is available only when the thread is already on that connected account.
conversation.reply_identities.reply_address.emailstring | nullThe sender address; null when none is set up.
conversation.reply_identities.reply_address.availablebooleanWhether a reply can be sent from it now.
conversation.reply_identities.reply_address.reasonstringOnly present when available is false: not_configured (no usable sender is set up) or reconnect_required (the account or mailbox must be reconnected in DialEcho). One of not_configured, reconnect_required.
messagesarray of objectsThis page of messages, oldest first.
messages.idUUIDThe message. Pass an inbound message's id as in_reply_to_message_id to answer it.
messages.conversation_idUUIDIts conversation.
messages.campaign_idUUIDThe conversation's campaign.
messages.domainstringThe conversation's inbox. One of contact, realestate.
messages.directionstringinbound from the contact, outbound to them. One of inbound, outbound.
messages.kindstring | nullhuman_reply: a person wrote back. auto_reply: an auto-responder, such as an out-of-office. manual_reply: sent by your team or through this API. ai_reply: sent by the AI email agent. null when the message was never classified, as with older messages and origin_message. One of human_reply, auto_reply, manual_reply, ai_reply.
messages.from_emailstring | nullThe sender, following the member's email visibility.
messages.to_emailsarray of stringsThe recipients. Addresses the member's role hides are left out.
messages.cc_emailsarray of stringsThe CC recipients, the same way.
messages.subjectstring | nullThe subject.
messages.textstring | nullThe plain-text body. Restricted when the member's role masks messages.
messages.htmlstring | nullThe HTML body exactly as stored, or null (also when the role masks messages). Sanitize it before you display it and never execute it.
messages.attachmentsarray of objectsWhat was attached; there are no download links. Empty when the role masks messages. A detail that is not known is null.
messages.attachments.filenamestring | nullThe file name.
messages.attachments.content_typestring | nullThe MIME type, for example application/pdf.
messages.attachments.size_bytesinteger | nullThe size in bytes.
messages.created_attimestamp | nullWhen DialEcho stored the message (RFC 3339, UTC). The inbound feed is ordered by it.
messages.received_attimestamp | nullWhen the mail provider received an inbound message.
messages.sent_attimestamp | nullWhen an outbound message was sent.
messages.is_syntheticbooleantrue only for a conversation's origin_message, which is not a stored message.
messages.external_replyobject | nullSet on replies sent through this API; null on every other message.
messages.external_reply.api_key_idUUID | nullThe API key that sent it.
messages.external_reply.organization_member_idUUID | nullThe key's team member.
messages.external_reply.automatedbooleanAlways true: sent by an integration, not typed by a person.
messages.external_reply.in_reply_to_message_idUUID | nullThe inbound message it answered.
origin_messageobject | nullThe email that opened the thread when it is not a stored message, with the same attributes as a message and is_synthetic: true; otherwise null.
origin_statusstringWhether there is an origin_message. One of available, not_applicable, unavailable.
next_messages_cursorstring | nullPass it back as messages_cursor for the next page of messages; null when there is none.
has_morebooleantrue when more messages follow.

Example request ​

curl

bash
curl "https://development.agencydevworks.ai/v1/inbox/conversations/44444444-0000-4000-8000-000000000001?limit=100" \
  -H "Authorization: Bearer dei_your_key"

JavaScript

js
const response = await fetch("https://development.agencydevworks.ai/v1/inbox/conversations/44444444-0000-4000-8000-000000000001?limit=100", {
  headers: {
    "Authorization": "Bearer dei_your_key",
  },
});
const data = await response.json();

Example response ​

Status: 200 OK

json
{
  "conversation": {
    "id": "44444444-0000-4000-8000-000000000001",
    "campaign_id": "cccccccc-0000-4000-8000-000000000001",
    "domain": "contact",
    "contact_id": "bbbbbbbb-0000-4000-8000-000000000001",
    "subject": "Your solar quote",
    "participant_email": "pat@example.com",
    "status": "open",
    "ai_status": "needs_review",
    "updated_at": "2026-11-02T16:20:02Z",
    "last_message_at": "2026-11-02T16:20:00Z",
    "last_inbound_at": "2026-11-02T16:20:00Z",
    "read": false,
    "archived": false,
    "reply_identities": {
      "campaign_mailbox": { "email": "jordan@outreach.example.com", "available": true },
      "reply_address": { "email": "jordan@example.com", "available": true }
    }
  },
  "messages": [
    {
      "id": "66666666-0000-4000-8000-000000000001",
      "conversation_id": "44444444-0000-4000-8000-000000000001",
      "campaign_id": "cccccccc-0000-4000-8000-000000000001",
      "domain": "contact",
      "direction": "outbound",
      "kind": "ai_reply",
      "from_email": "jordan@outreach.example.com",
      "to_emails": ["pat@example.com"],
      "cc_emails": [],
      "subject": "Re: Your solar quote",
      "text": "Happy to help. Which day works for a call this week?",
      "html": "<p>Happy to help. Which day works for a call this week?</p>",
      "attachments": [],
      "created_at": "2026-11-02T15:02:11Z",
      "received_at": null,
      "sent_at": "2026-11-02T15:02:10Z",
      "is_synthetic": false,
      "external_reply": null
    },
    {
      "id": "66666666-0000-4000-8000-000000000002",
      "conversation_id": "44444444-0000-4000-8000-000000000001",
      "campaign_id": "cccccccc-0000-4000-8000-000000000001",
      "domain": "contact",
      "direction": "inbound",
      "kind": "human_reply",
      "from_email": "pat@example.com",
      "to_emails": ["jordan@outreach.example.com"],
      "cc_emails": [],
      "subject": "Re: Your solar quote",
      "text": "Is Thursday afternoon available?",
      "html": "<p>Is Thursday afternoon available?</p>",
      "attachments": [{ "filename": "electric-bill.pdf", "content_type": "application/pdf", "size_bytes": 48213 }],
      "created_at": "2026-11-02T16:20:02Z",
      "received_at": "2026-11-02T16:20:00Z",
      "sent_at": null,
      "is_synthetic": false,
      "external_reply": null
    }
  ],
  "origin_message": null,
  "origin_status": "not_applicable",
  "next_messages_cursor": null,
  "has_more": false
}

Errors ​

HTTPCodeWhen
403insufficient_scopeThe key does not have inbox:read.
404not_foundThe conversation does not exist, was deleted, or the key's member cannot see it.
409resync_requiredThe cursor expired (after 30 days) or the key's or its member's access changed since it was issued. Start again without a cursor.
422invalid_cursormessages_cursor was not issued to this key for this conversation, or was changed.
422invalid_requestconversation_id is not a UUID, limit is outside 1 to 100, or messages_cursor is over 4,096 characters.
503dependency_unavailableThe inbox or the access check is temporarily unavailable. Retry with backoff.

Every endpoint can also answer 401 invalid_api_key, 429 rate_limited and 503 dependency_unavailable. See Errors in the overview.