# 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

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `conversation_id` | `UUID` | Yes | The conversation. |

## Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | `integer` | No | Messages per page. 1 to 100. Default: `100`. |
| `messages_cursor` | `string` | No | The `next_messages_cursor` of the previous page of this conversation, unchanged. Up to 4,096 characters. |

## Response

**Status:** `200 OK`

| Name | Type | Description |
| --- | --- | --- |
| `conversation` | `object` | The conversation. |
| `conversation.id` | `UUID` | The conversation. |
| `conversation.campaign_id` | `UUID` | The conversation's campaign. |
| `conversation.domain` | `string` | Which inbox it is in: a contact campaign's (`contact`) or a real-estate offer campaign's (`realestate`). One of `contact`, `realestate`. |
| `conversation.contact_id` | `UUID \| null` | The CRM contact, or `null` when it is not linked to one. |
| `conversation.subject` | `string \| null` | The thread subject. |
| `conversation.participant_email` | `string \| null` | The other party's address, following the member's email visibility. |
| `conversation.status` | `string` | The 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_status` | `string` | Where 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_at` | `timestamp` | When the conversation last changed, read and archive changes included (RFC 3339, UTC). |
| `conversation.last_message_at` | `timestamp \| null` | Time of its latest message. |
| `conversation.last_inbound_at` | `timestamp \| null` | Time of its latest inbound message. |
| `conversation.read` | `boolean` | `true` when it was read at or after its latest inbound message. A conversation without inbound messages is read. |
| `conversation.archived` | `boolean` | `true` while it is archived. |
| `conversation.reply_identities` | `object` | The 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_mailbox` | `object` | The campaign mailbox the thread came from: the mailbox or account that sent the email the contact answered. |
| `conversation.reply_identities.campaign_mailbox.email` | `string \| null` | The sender address; `null` when none is set up. |
| `conversation.reply_identities.campaign_mailbox.available` | `boolean` | Whether a reply can be sent from it now. |
| `conversation.reply_identities.campaign_mailbox.reason` | `string` | Only 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_address` | `object` | The 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.email` | `string \| null` | The sender address; `null` when none is set up. |
| `conversation.reply_identities.reply_address.available` | `boolean` | Whether a reply can be sent from it now. |
| `conversation.reply_identities.reply_address.reason` | `string` | Only 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`. |
| `messages` | `array of objects` | This page of messages, oldest first. |
| `messages.id` | `UUID` | The message. Pass an inbound message's `id` as `in_reply_to_message_id` to answer it. |
| `messages.conversation_id` | `UUID` | Its conversation. |
| `messages.campaign_id` | `UUID` | The conversation's campaign. |
| `messages.domain` | `string` | The conversation's inbox. One of `contact`, `realestate`. |
| `messages.direction` | `string` | `inbound` from the contact, `outbound` to them. One of `inbound`, `outbound`. |
| `messages.kind` | `string \| null` | `human_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_email` | `string \| null` | The sender, following the member's email visibility. |
| `messages.to_emails` | `array of strings` | The recipients. Addresses the member's role hides are left out. |
| `messages.cc_emails` | `array of strings` | The CC recipients, the same way. |
| `messages.subject` | `string \| null` | The subject. |
| `messages.text` | `string \| null` | The plain-text body. `Restricted` when the member's role masks messages. |
| `messages.html` | `string \| null` | The HTML body exactly as stored, or `null` (also when the role masks messages). Sanitize it before you display it and never execute it. |
| `messages.attachments` | `array of objects` | What was attached; there are no download links. Empty when the role masks messages. A detail that is not known is `null`. |
| `messages.attachments.filename` | `string \| null` | The file name. |
| `messages.attachments.content_type` | `string \| null` | The MIME type, for example `application/pdf`. |
| `messages.attachments.size_bytes` | `integer \| null` | The size in bytes. |
| `messages.created_at` | `timestamp \| null` | When DialEcho stored the message (RFC 3339, UTC). The inbound feed is ordered by it. |
| `messages.received_at` | `timestamp \| null` | When the mail provider received an inbound message. |
| `messages.sent_at` | `timestamp \| null` | When an outbound message was sent. |
| `messages.is_synthetic` | `boolean` | `true` only for a conversation's `origin_message`, which is not a stored message. |
| `messages.external_reply` | `object \| null` | Set on replies sent through this API; `null` on every other message. |
| `messages.external_reply.api_key_id` | `UUID \| null` | The API key that sent it. |
| `messages.external_reply.organization_member_id` | `UUID \| null` | The key's team member. |
| `messages.external_reply.automated` | `boolean` | Always `true`: sent by an integration, not typed by a person. |
| `messages.external_reply.in_reply_to_message_id` | `UUID \| null` | The inbound message it answered. |
| `origin_message` | `object \| null` | The 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_status` | `string` | Whether there is an `origin_message`. One of `available`, `not_applicable`, `unavailable`. |
| `next_messages_cursor` | `string \| null` | Pass it back as `messages_cursor` for the next page of messages; `null` when there is none. |
| `has_more` | `boolean` | `true` 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

| HTTP | Code | When |
| --- | --- | --- |
| 403 | `insufficient_scope` | The key does not have `inbox:read`. |
| 404 | `not_found` | The conversation does not exist, was deleted, or the key's member cannot see it. |
| 409 | `resync_required` | The cursor expired (after 30 days) or the key's or its member's access changed since it was issued. Start again without a cursor. |
| 422 | `invalid_cursor` | `messages_cursor` was not issued to this key for this conversation, or was changed. |
| 422 | `invalid_request` | `conversation_id` is not a UUID, `limit` is outside 1 to 100, or `messages_cursor` is over 4,096 characters. |
| 503 | `dependency_unavailable` | The 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](/api/#errors) in the overview.

## Related endpoints

- `GET /v1/inbox/conversations` · [List conversations](/api/inbox/list-conversations)
- `POST /v1/inbox/conversations/{conversation_id}/replies` · [Reply to a conversation](/api/inbox/reply)
- `PATCH /v1/inbox/conversations/{conversation_id}` · [Update a conversation](/api/inbox/update-conversation)
