# Send a message

`POST https://app.pozyskajpacjenta.pl/api/v1/conversations/{id}/messages` · operationId `sendMessage` · since 1.14.0 · tag Conversations

Sends a message to the conversation's patient on the channel you choose, through the same path as a reply from the clinic's inbox: it is stored in the conversation, reaches the patient through the clinic's own channel account, and fires the `message.sent` webhook. The rules are the inbox's: a patient who opted out of a channel gets nothing on it; WhatsApp and Instagram take free text only within 24 hours of the patient's last message on that channel; the chat only after the patient wrote on it. A refusal is a 409 with its code, and nothing was sent. Send `text`, or `templateId` to send one of the clinic's templates filled in for the patient. Needs the scope conversations:write; every attempt is written to the clinic's audit log.

## Request

Authorization: `Bearer <the clinic's key>`.

### Path parameters

- `id` (`string`, required): The conversation id (`conversationId` of webhook events).

### Body (application/json)

The channel and the text or the template, as JSON.

- `channel` (`string`, required): The channel: email, sms, whatsapp, instagram or chat (the chat on the clinic's site). Pick one GET /conversations/{id} lists as available. One of `email`, `sms`, `whatsapp`, `instagram`, `chat`.
- `text` (`string`): The text, 1 to 4000 characters, trimmed. An SMS carries its first 480 characters.
- `templateId` (`string`): A template id from GET /conversations/{id} `templates`.

### cURL

```bash
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."
  }'
```

### TypeScript

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

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

try {
  const { message } = await client.sendMessage("Vb3nQ8sLk2PzX5wRt9YcE", {
    channel: "sms",
    text: "Dziękujemy, do zobaczenia w czwartek o 10:00.",
  });
  console.log(message.id);
} catch (error) {
  // 409: the inbox's rules said no (opt-out, a closed 24 h window…); nothing was sent
  if (isSendRefused(error)) console.log(error.code);
  else throw error;
}
```

### PHP

```php
<?php
$body = array(
    'channel' => 'sms',
    'text'    => 'Dziękujemy, do zobaczenia w czwartek o 10:00.',
);

$response = wp_remote_post(
    'https://app.pozyskajpacjenta.pl/api/v1/conversations/Vb3nQ8sLk2PzX5wRt9YcE/messages',
    array(
        'headers' => array(
            'Authorization' => 'Bearer ' . getenv( 'PP_API_KEY' ),
            'Content-Type'  => 'application/json',
        ),
        'body'    => wp_json_encode( $body ),
        'timeout' => 10,
    )
);
if ( is_wp_error( $response ) ) {
    error_log( $response->get_error_message() );
    return;
}
$status = wp_remote_retrieve_response_code( $response );
$data   = json_decode( wp_remote_retrieve_body( $response ), true );
```

### Request body: A template of the clinic, filled in for the patient

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

## Response

### 201: The message was sent.

- `message` (`ConversationMessage`, required): The message as stored (status sent).
  - `id` (`string`, required): Message id: the `messageId` of webhook events.
  - `conversationId` (`string`, required): The conversation it belongs to.
  - `channel` (`string`, required): The channel: email, sms, whatsapp, instagram or chat (the chat on the clinic's site). One of `email`, `sms`, `whatsapp`, `instagram`, `chat`.
  - `direction` (`string`, required): inbound: from the patient. outbound: from the clinic (the inbox, an automation or this API). outbound_echo: the clinic replied from the WhatsApp or Instagram app itself. One of `inbound`, `outbound`, `outbound_echo`.
  - `status` (`string`, required): received for every inbound message. Outbound messages move forward only: queued, sent, delivered, read; or failed. One of `queued`, `sent`, `delivered`, `read`, `failed`, `received`.
  - `subject` (`string | null`, required): E-mail subject; null on other channels.
  - `text` (`string | null`, required): The text; null for a message with attachments only.
  - `attachments` (`MessageAttachment[]`, required): Files that came with the message.
    - `filename` (`string`, required): File name.
    - `mime` (`string`, required): Media type, e.g. image/jpeg.
    - `size` (`integer`, required): Size in bytes.
  - `createdAt` (`string (date-time)`, required): When the message was stored.
  - `sentAt` (`string (date-time) | null`, required): Outbound: when the provider accepted it; null otherwise.
  - `deliveredAt` (`string (date-time) | null`, required): Outbound: when it reached the patient's device, where the channel reports it.
  - `readAt` (`string (date-time) | null`, required): Inbound: when someone at the clinic opened it. Outbound: the provider's read receipt, where the channel reports it.

Example (Sent by SMS):

```json
{
  "message": {
    "id": "gbOmbsye_UFcdrpeY4LuZ",
    "conversationId": "Vb3nQ8sLk2PzX5wRt9YcE",
    "channel": "sms",
    "direction": "outbound",
    "status": "sent",
    "subject": null,
    "text": "Dziękujemy, do zobaczenia w czwartek o 10:00.",
    "attachments": [],
    "createdAt": "2026-10-12T08:30:49.000Z",
    "sentAt": "2026-10-12T08:30:49.000Z",
    "deliveredAt": null,
    "readAt": null
  }
}
```

Example (Sent from a template):

```json
{
  "message": {
    "id": "FcGPiB1Bsve8C3tp3Agyt",
    "conversationId": "Vb3nQ8sLk2PzX5wRt9YcE",
    "channel": "sms",
    "direction": "outbound",
    "status": "sent",
    "subject": null,
    "text": "Dzień dobry Anna, wolne terminy i rezerwacja online: https://wzorcowa.pozyskajpacjenta.pl Klinika Wzorcowa",
    "attachments": [],
    "createdAt": "2026-10-12T08:30:50.000Z",
    "sentAt": "2026-10-12T08:30:50.000Z",
    "deliveredAt": null,
    "readAt": null
  }
}
```

## Errors

| Status | code | When |
| --- | --- | --- |
| 400 | `invalid_body` | Both text and templateId |
| 401 |  | No Authorization header |
| 401 |  | Unknown or rotated key |
| 403 | `missing_scope` | The key can read but not send |
| 404 | `not_found` | Unknown id, another clinic's conversation or an archived lead's |
| 409 | `opted_out` | The patient opted out of e-mail |
| 409 | `chat_not_started` | The patient never wrote on the chat |
| 429 |  | Over 120 requests in this minute |
| 502 | `send_failed` | The WhatsApp provider did not take the message |

400 (Both text and templateId):

```json
{
  "error": "podaj text albo templateId (jedno z nich)",
  "code": "invalid_body"
}
```

401 (No Authorization header):

```json
{
  "error": "missing Authorization: Bearer <klucz z panelu Ustawienia>"
}
```

401 (Unknown or rotated key):

```json
{
  "error": "invalid key"
}
```

403 (The key can read but not send):

```json
{
  "error": "Klucz nie ma uprawnienia conversations:write. Placówka nadaje je w panelu: Ustawienia › Integracje › Klucz API.",
  "code": "missing_scope",
  "scope": "conversations:write"
}
```

404 (Unknown id, another clinic's conversation or an archived lead's):

```json
{
  "error": "Nie ma takiej rozmowy.",
  "code": "not_found"
}
```

409 (The patient opted out of e-mail):

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

409 (The patient never wrote on the chat):

```json
{
  "error": "Pacjent nie pisał na czacie.",
  "code": "chat_not_started"
}
```

429 (Over 120 requests in this minute):

```json
{
  "error": "przekroczono limit 120 zapytań/min"
}
```

502 (The WhatsApp provider did not take the message):

```json
{
  "error": "Dostawca kanału nie przyjął wiadomości. Jest w rozmowie jako niewysłana.",
  "code": "send_failed",
  "retryable": true
}
```

## Rate limit

- 120 requests per minute per key, shared by every endpoint.

## SDK method

`client.sendMessage(conversationId: string, input: SendMessageInput): Promise<MessageSent>`

Throws ApiError 409 with the reason in `code` (isSendRefused); 502 send_failed.
