Concepts
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. 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:
- Conversations in the API are switched on for the clinic (we switch them on at the clinic's request).
- 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.
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 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).
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; 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 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} lists every channel with available and, when it is false, the reason a send would get.
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:
{
"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}, 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.
{
"channel": "sms",
"templateId": "nHv4l_ALArlouEEfxHLOO"
}In the SDK
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.