> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dialtu.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Sync WhatsApp conversations

> Pull conversation history and transcripts into your own systems, incrementally

The two WhatsApp endpoints are **read-only**: they let you list conversations with their
transcripts and drill into a single conversation's full message history — for reporting,
quality review, or keeping a copy in your own CRM.

* **List WhatsApp conversations** — `GET /api/v2/public/whatsapp/conversations`
* **List messages in a WhatsApp conversation** — `GET /api/v2/public/whatsapp/conversations/{session_id}/messages`

The endpoint pages document every parameter and field; this guide is about using them well.

## Concepts

* A **conversation** (a *session*) is one bounded WhatsApp conversation window with a single
  customer phone number. A customer accumulates several conversations over time as windows
  open and close.
* Each conversation belongs to a **client** (when the number is matched to a contact) and
  contains an ordered list of **messages**.
* Conversations carry a **status** (`new`, `ai_handling`, `human_handling`, `resolved`,
  `expired`), a **direction** (`inbound` / `outbound`), an optional **disposition** tag and an
  optional **sentiment**.

### Who sent what

Three fields answer three different questions — do not confuse them:

| Field               | Level        | Answers                                                                                                            |
| ------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------ |
| `messages[].sender` | message      | **Who sent this message** — the specific AI agent, team member, or the client.                                     |
| `participants`      | conversation | **Who replied in this conversation** — every AI agent and team member that sent at least one message, with counts. |
| `assigned_users`    | customer     | **Who owns this customer** — their Salesflow pipeline assignee(s). Says nothing about who replied.                 |

A single conversation routinely mixes several people and one or more AI agents (a bot handles
the opening, a person takes over, another person closes it), which is why `participants`
exists separately from `assigned_users`.

`participants` is always complete — it covers the whole conversation even when you request
`include_messages=false` or the transcript is truncated. Only identified senders appear;
messages synced from the WhatsApp Business app carry no attributable sender and contribute
to no entry.

## Common requests

<Tabs>
  <Tab title="Metadata-only browse">
    The fastest way to see what exists. `participants` is still fully populated.

    ```bash theme={null}
    curl -H "Authorization: Bearer pk_xxx" \
      "https://api.dialtu.com/api/v2/public/whatsapp/conversations?limit=20&include_messages=false"
    ```
  </Tab>

  <Tab title="Full transcripts, newest first">
    ```bash theme={null}
    curl -H "Authorization: Bearer pk_xxx" \
      "https://api.dialtu.com/api/v2/public/whatsapp/conversations?limit=10&sort_by=-created_at"
    ```
  </Tab>

  <Tab title="Filter by client and disposition">
    Repeat `disposition_tag_ids` for an OR filter; add `include_uncategorized=true` to also get
    conversations with no disposition.

    ```bash theme={null}
    curl -H "Authorization: Bearer pk_xxx" \
      "https://api.dialtu.com/api/v2/public/whatsapp/conversations?client_id=2&disposition_tag_ids=1&disposition_tag_ids=2&include_uncategorized=true"
    ```
  </Tab>

  <Tab title="Messages per team member">
    Because `participants` is populated without transcripts, per-agent and per-person
    reporting is cheap:

    ```bash theme={null}
    curl -H "Authorization: Bearer pk_xxx" \
      "https://api.dialtu.com/api/v2/public/whatsapp/conversations?limit=50&include_messages=false&created_after=2026-06-01T00:00:00Z" \
    | jq '[.data[].participants.users[]]
          | group_by(.id)
          | map({ id: .[0].id, name: .[0].name, messages: (map(.message_count) | add) })
          | sort_by(-.messages)'
    ```
  </Tab>
</Tabs>

## Incremental sync

Do not re-download everything on each run:

<Steps>
  <Step title="Store the newest updated_at you have seen">
    Every conversation carries `updated_at`, which moves whenever a message arrives or the
    status, disposition or summary changes.
  </Step>

  <Step title="Ask only for what changed">
    On the next run pass that timestamp as `updated_after` and sort with `sort_by=updated_at`
    for predictable ordering:

    ```bash theme={null}
    curl -H "Authorization: Bearer pk_xxx" \
      "https://api.dialtu.com/api/v2/public/whatsapp/conversations?updated_after=2026-06-30T00:00:00Z&sort_by=updated_at"
    ```
  </Step>

  <Step title="Page through the result">
    Keep the same filters and increase `offset` by `limit` until you have read `count` items.
  </Step>
</Steps>

## Long conversations

Each conversation embeds at most `max_messages_per_session` messages (default 500, oldest
first). A longer transcript is truncated and flagged with `has_more_messages: true` — fetch the
rest with the messages endpoint, paging with `limit` / `offset`:

```bash theme={null}
curl -H "Authorization: Bearer pk_xxx" \
  "https://api.dialtu.com/api/v2/public/whatsapp/conversations/60/messages?limit=100&offset=0"
```

The messages endpoint also returns the conversation's full `participants` roster, so you never
need a second call to the list endpoint just for that.

## Sample conversation

```json theme={null}
{
  "session_id": 61,
  "status": "ai_handling",
  "direction": "inbound",
  "customer_phone_number": "+573013449768",
  "client": { "id": 2, "name": "Christian Gallo", "first_name": "Christian", "last_name": "Gallo", "phone": "3013449768", "email": null, "company": null },
  "disposition": null,
  "sentiment": null,
  "summary": "",
  "message_count": 2,
  "has_more_messages": false,
  "created_at": "2026-06-30T15:39:33.437Z",
  "updated_at": "2026-06-30T15:39:39.080Z",
  "last_message_at": "2026-06-30T15:39:51.309Z",
  "messages": [
    {
      "id": 561, "sender_type": "customer",
      "sender": { "type": "client", "id": 2, "name": "Christian Gallo", "email": null },
      "message_type": "text",
      "content": "Hola, ¿qué horarios hay disponibles?",
      "timestamp": "2026-06-30T15:39:33.482Z",
      "media": null, "reply_to_id": null
    },
    {
      "id": 562, "sender_type": "ai",
      "sender": { "type": "ai_agent", "id": 7, "name": "Sofia", "email": null },
      "message_type": "text",
      "content": "¡Hola! Tenemos disponibilidad mañana a las 10:00.",
      "timestamp": "2026-06-30T15:39:39.080Z",
      "media": null, "reply_to_id": 561
    }
  ],
  "assigned_users": [ { "id": 42, "name": "Ana Perez", "email": "ana@acme.com" } ],
  "participants": {
    "ai_agents": [ { "id": 7, "name": "Sofia", "message_count": 1, "first_message_at": "2026-06-30T15:39:39.080Z", "last_message_at": "2026-06-30T15:39:39.080Z" } ],
    "users": []
  }
}
```

## Notes and limits

* **Read-only.** These endpoints never modify conversations.
* **Media files are not downloadable** — only `kind`, `mime`, `filename` and `caption` are
  returned.
* Internal fields (model, tokens, cost, storage keys) are never exposed.
* **Additive changes.** New fields may be added (`sender` and `participants` arrived this way,
  next to an unchanged `sender_type`). Ignore fields you do not recognise.
* No rate limit is enforced today; page with `limit` ≤ 50 rather than looping on maximal
  payloads.
