Skip to content
Documentation
pozyskajpacjentaDocs

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, Messenger 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:

  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:
ScopeLets the key
conversations:readlist conversations, read one with its patient, read its messages
conversations:writesend 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
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 or from Meta Business Suite 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.

Enquiries of merged leads

The same patient sometimes reaches the clinic twice, for example a form and later the chat, and the clinic merges the two leads into one. GET /conversations/{id} lists the enquiries of the merged leads in mergedEnquiries (since API 1.18.0), oldest first: the merged lead's leadId, the text the patient wrote, its source, channel (phone, in_person or missed_call for a lead the clinic entered or a missed call, otherwise null) and createdAt. Show them in the conversation at their own time, as the clinic's inbox does. They are read from the merged leads while the merge lasts and never copied: when the clinic undoes a merge, that enquiry leaves the list and is the other lead's own lead.message again.

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:

ChannelA message goes out when
smsthe patient has a phone number and the clinic has its SMS provider connected; an SMS carries the first 480 characters
emailthe patient has an e-mail address
whatsappthe clinic has WhatsApp connected and the patient wrote on WhatsApp in the last 24 hours
instagramthe clinic has Instagram connected and the patient wrote on Instagram in the last 24 hours
messengerthe clinic has its Facebook Page connected and the patient wrote on Messenger in the last 24 hours (since API 1.15.0)
chatthe 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
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:

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, fb_not_configured, fb_no_user_thread, fb_window_closed, template_not_found, template_channel, address_required, address_unknown and chat_disabled (the chat on the clinic's site is off, so the patient would not see the message). 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.

The patient's addresses

A patient may write from several numbers, e-mail addresses or Messenger and Instagram accounts. GET /conversations/{id} lists them in identities (since API 1.16.0): the channel, the address, whether it is the lead's own (primary: reminders and other automated messages go there), whether the patient wrote from it (verified), whether it opted out (optedOut) and which one a message goes to by default (replyDefault).

A message goes to the address the patient last wrote from on that channel, as a reply from the clinic's inbox does. To pick another, pass it as to:

Request body
{
  "channel": "sms",
  "text": "Dziękujemy, do zobaczenia w czwartek o 10:00.",
  "to": "+48600100200"
}

An SMS STOP opts out that number only: nothing goes to it, and the patient's other numbers are not affected. When the address the patient last wrote from cannot take the message (it opted out, or the clinic removed it from the patient's card), the answer is 409 opted_out or address_required, and you pass another address in to; an address that is not the patient's answers address_unknown.

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.

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

In the SDK

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.