# Lead conversations

Read messages in both directions within one campaign and check the period for which the data is complete.

New features are enabled for your organization on request.

## What the conversation includes

`GET /v1/leads/{id}/messages?view=conversation` returns available messages starting with our first confirmed message to this lead in this campaign, including that first message. It covers campaign steps, manual outbound messages and individual incoming messages, including attachments without text.

A lead is one contact's participation in one campaign. Earlier conversations and messages belonging to another campaign are excluded. If a message cannot be attributed to this conversation, the API does not guess. A lead may have several dialogs with different accounts; completeness for each is in `meta.dialogs`.

The first available copy of a message is saved. Edits and deletions are not tracked. A message deleted before it could be saved may be missing even from a checked range. `complete` does not mean that every message that ever existed has been recovered.

## Request and message fields

You need `messages:read` and "Message text" enabled on the key. Without `view`, the route keeps its existing response format and cursors.

```bash
curl -G https://api.melioralab.io/v1/leads/5a7c3e92-1f4b-4d68-9c2e-7b0a6d4f8e31/messages \
  -H "Authorization: Bearer $MELIORA_API_KEY" \
  -d view=conversation -d limit=50
```

The response contains `object`, `data`, `has_more`, `next_cursor` and `meta`. Each message includes:

- `id`: a stable identifier. Merge repeated results by this ID.
- `workspace_id`, `campaign_id`, `lead_id`, `account_id`: the message context.
- `direction`: `inbound` or `outbound`; `author`: `campaign`, `manager`, `lead` or `unknown`.
- `state`: `sent`, `received` or `unknown`. An unknown outcome does not mean the send failed.
- `sent_at`: the message time or `null`; `observed_at`: when the message was saved.
- `media_kind`: the content kind; `attachment`: an attachment description or `null`.
- `text`, `text_truncated`, `text_unavailable`, `text_unavailable_reason`: saved text and its availability.

Text is limited to 4000 characters. `text_truncated=true` means it was shortened, `false` means it is complete, and `null` means this is unknown. When text is unavailable, `text=null`; the reason is `not_saved`, `ownership_unproven`, `source_conflict` or `purged`. Text availability and completeness of the message set are separate measures.

See the [conversation reference](https://docs.melioralab.io/en/reference/messages/#listLeadMessages) for full schemas and fictional responses.

## Attachments

`attachment` contains only `{kind, caption}`. Kinds are `photo`, `video`, `voice`, `video_note`, `file`, `sticker` and `other`. Audio files use `file`; unsupported kinds use `other`. A caption is limited to 4000 characters or is `null`; text availability and truncation indicators also apply to the caption. When text is unavailable, the caption is also `null`.

Files, download links, filenames and contact card contents are not returned.

## Reading completeness

`meta.completeness` describes the lead's available range as a whole. `meta.dialogs[].completeness` describes each dialog separately.

- `overall`, `inbound`, `outbound`: overall completeness and completeness in each direction. Values are `complete`, `partial` and `unknown`.
- `boundary_at`: the time of our first confirmed campaign message. This is the inclusive start of the range.
- `checked_at`: when the range was checked; `checked_through`: the inclusive time through which it was checked. These are different values. `null` means a value is unknown.
- `available_from`, `available_to`: the boundaries of available saved data. They do not prove completeness by themselves.
- `observed_at`: when completeness information was read.
- `gaps`: up to 50 potentially incomplete intervals with `id`, `from`, `to` and `state`. An open interval has `to=null`.
- `gaps_truncated=true`: the gap list was shortened. An empty gap list does not prove completeness.

`complete` means both directions were checked from the beginning through `checked_through`, as of `checked_at`. Messages arriving after the check are outside that promise. `partial` means a known part is incomplete, such as a gap or an unavailable period. `unknown` means there is not enough evidence, for example because the starting boundary has not been established.

A gap has state `open`, `recovering`, `recovered`, `unrecoverable` or `unknown`. `recovered` means that particular gap is closed; still check `overall` for the complete picture.

## Handling partial data

1. Save the returned messages and the incomplete state. Do not treat a missing message as proof that it never existed.
2. Read all pages in the current traversal. `has_more=false` only means there are no more pages of saved data.
3. Later, start again without `cursor`, keeping `view=conversation`, and merge messages by `id`. Compare completeness and checked boundaries for each dialog.
4. If a gap remains `unrecoverable`, keep the incomplete state. If completeness does not change for a long time, contact [support](https://docs.melioralab.io/en/support/) with the `Request-Id`.

For `unknown`, also preserve the uncertainty and retry later without treating the history as complete.

## Cursors and snapshot_stale

The first page fixes the data set for a traversal. Pages run from earlier to later messages; the cursor continues the same data set. A message added later, including one with an earlier timestamp, appears in a new traversal.

Send `view=conversation` on every page. You may change `limit`: the default is 50 and the maximum is 100. A cursor is bound to the lead, view and key scope; a cursor from the existing view cannot be used here. Cursors last 15 minutes. On 410 `cursor_expired`, start again. On 400 `cursor_mismatch`, check the parameters and scope.

If any page returns `meta.snapshot_stale=true`, new facts appeared after the traversal began. Finish the current traversal, then start again without a cursor and merge results by `id`. `snapshot_stale=false` does not replace checking `meta.completeness`.

## Incoming events and webhooks

`message.received` means one individual incoming message, including one without text. `data.message.id` matches the conversation message ID. `data.backfilled=true` indicates a recovered gap; the message keeps its original timestamp. Events do not replace reading the available conversation.

For the feed, select the [2b catalog](https://docs.melioralab.io/en/events-polling/#catalog-2b) or `type=message.received`. In the feed, request text and captions with `include_text=true`. [New webhooks](https://docs.melioralab.io/en/webhooks/#new-event-types) carry metadata only; read text through this API route.
