# Conversations

Read the clinic's conversations with patients and reply on their channels, by the rules of the clinic's inbox.

A conversation is the clinic's thread with one patient: every message on e-mail, SMS, WhatsApp, Instagram and the chat on the clinic's site, in one place, with the patient's lead. This page covers access, reading conversations and messages, replying, and keeping a copy in step with [event webhooks](https://pozyskajpacjenta.pl/docs/webhooks#event-webhooks). A webhook event carries ids only; the text is read here.

## Before you start

Conversations hold names, phone numbers and what patients wrote, so a key reads them only when the clinic says so:

1. Conversations in the API are switched on for the clinic (we switch them on at the clinic's request).
2. In **Ustawienia → Integracje → Klucz API → Rozmowy z pacjentami** the clinic ticks what its key may do:

| Scope | Lets the key |
| --- | --- |
| `conversations:read` | list conversations, read one with its patient, read its messages |
| `conversations:write` | send a message to the patient |

A key without the scope gets 403 with `code: "missing_scope"` and the missing `scope`; without the switch, `feature_disabled`. Everything else the key does (site, content, leads, booking, media) needs no scope and stays as it was. The scopes stay when the clinic generates a new key. More in [API keys](https://pozyskajpacjenta.pl/docs/concepts/api-keys).

Every read and every send is written to the clinic's audit log with the key that made it. Call these endpoints from your server only, and keep what they return out of browsers and logs.

## Read conversations

[GET /conversations](https://pozyskajpacjenta.pl/docs/reference/conversations/list-conversations) lists the clinic's conversations, newest activity first, each with its patient (`lead`: name, phone, e-mail, the enquiry they sent, the stage in the clinic's CRM).

```bash cURL
curl "https://app.pozyskajpacjenta.pl/api/v1/conversations?status=open&limit=20" \
  -H "Authorization: Bearer $PP_API_KEY"
```

Filter with `status` (`open` or `closed`), `location` (an id from [GET /locations](https://pozyskajpacjenta.pl/docs/reference/content/list-locations); the primary location also takes conversations whose location is not known), `channel` (of the newest message) and `updated_since`. Pass `nextCursor` as `cursor` for the next page; it is `null` on the last one.

To keep a copy in your system in step, note the time when a sync starts and pass it as `updated_since` next time: you get every conversation with a new message, a read, a change of status or a change of its patient's lead since then. Delivery receipts do not count as a change; they arrive as the `message.status` webhook.

A lead the clinic archived leaves the API with its conversation, as it leaves the clinic's inbox.

## Read messages

[GET /conversations/{id}/messages](https://pozyskajpacjenta.pl/docs/reference/conversations/list-messages) returns the messages newest first; `nextCursor` pages to older ones. `direction` is `inbound` for the patient, `outbound` for the clinic, and `outbound_echo` when the clinic replied from the WhatsApp or Instagram app itself. The patient's first chat message is the enquiry of a chat lead: it is in `lead.message` of the conversation, not in this list.

When a `message.received` webhook arrives, its `conversationId` and `messageId` lead straight here: the new message is on the first page.

## Reply

Replies leave through the same path as a reply from the clinic's inbox: the clinic's own SMS, e-mail and Meta accounts, stored in the conversation, announced by the `message.sent` webhook. So do the inbox's rules:

| Channel | A message goes out when |
| --- | --- |
| `sms` | the patient has a phone number and the clinic has its SMS provider connected; an SMS carries the first 480 characters |
| `email` | the patient has an e-mail address |
| `whatsapp` | the clinic has WhatsApp connected and the patient wrote on WhatsApp in the last 24 hours |
| `instagram` | the clinic has Instagram connected and the patient wrote on Instagram in the last 24 hours |
| `chat` | the patient wrote on the chat on the clinic's site |

A patient who opted out of a channel gets nothing on it. Check first: [GET /conversations/{id}](https://pozyskajpacjenta.pl/docs/reference/conversations/get-conversation) lists every channel with `available` and, when it is `false`, the `reason` a send would get.

```bash cURL
curl -X POST "https://app.pozyskajpacjenta.pl/api/v1/conversations/Vb3nQ8sLk2PzX5wRt9YcE/messages" \
  -H "Authorization: Bearer $PP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "channel": "sms", "text": "Dziękujemy, do zobaczenia w czwartek o 10:00." }'
```

The answer is 201 with the stored message. When the rules say no, the answer is 409 and nothing was sent:

```json 409 Conflict
{
  "error": "Pacjent wypisał się z tego kanału.",
  "code": "opted_out"
}
```

Codes you can get: `opted_out`, `lead_archived`, `no_phone`, `no_email`, `no_smsapi_token`, `email_domain_not_configured`, `chat_not_started`, `meta_platform_disabled`, `wa_not_configured`, `wa_window_closed`, `ig_not_configured`, `ig_no_user_id`, `ig_no_user_thread`, `ig_window_closed`, `template_not_found` and `template_channel`. More may be added: treat an unknown code as a refusal. When the channel's provider does not take the message, the answer is 502 `send_failed`; the message stays in the conversation as `failed`, and `retryable` says whether a later attempt may work.

### Templates

The clinic's reply templates come with [GET /conversations/{id}](https://pozyskajpacjenta.pl/docs/reference/conversations/get-conversation), filled in for the patient (their first name, the clinic's address, phone and booking link). Send one with `templateId` instead of `text`; a template made for other channels answers 409 `template_channel`.

```json Request body
{
  "channel": "sms",
  "templateId": "nHv4l_ALArlouEEfxHLOO"
}
```

## In the SDK

```ts TypeScript
import { isSendRefused, PozyskajPacjentaClient } from "@pozyskajpacjenta/sdk";

const client = new PozyskajPacjentaClient({
  apiKey: process.env.PP_API_KEY ?? "",
  baseUrl: "https://app.pozyskajpacjenta.pl",
});

const { conversation, channels } = await client.getConversation("Vb3nQ8sLk2PzX5wRt9YcE");
if (channels.find((c) => c.channel === "sms")?.available) {
  try {
    await client.sendMessage(conversation.id, { channel: "sms", text: "Oddzwonimy dziś do 16:00." });
  } catch (error) {
    if (isSendRefused(error)) console.log(error.code);
    else throw error;
  }
}
```

Every endpoint, field and recorded response: [Conversations in the API reference](https://pozyskajpacjenta.pl/docs/reference/conversations).
