Book an appointment
POST/api/v1/booking/appointments
- Since
- 1.2.0
- Auth
- Bearer key
- operationId
- createAppointment
Books a visit at a free slot from GET /booking/slots, in the same calendar as the clinic's site. The patient's lead is found by phone number (however it is written) or created; the booking moves the lead to the booked stage unless it is already at a visited, not interested or unreachable stage (the stage stays, the visit is stored). Reception gets a "Nowa wizyta" e-mail with an .ics file. Sending the same data again within 5 minutes returns the existing appointment instead of a second one. If the slot was taken in the meantime you get 409 with that day's free slots.
Request
Send the clinic's key in Authorization: Bearer (see API keys).
Body application/json
The appointment as JSON.
- serviceIdstringrequired
The booking service, an id from the catalog's
services. - resourceIdstringrequired
The specialist, the
resourceIdof the slot. - startAtintegerrequired
Start, epoch seconds (UTC): the
startAtof a free slot. - patientobjectrequired
The patient.
Fields (3)
- namestringrequired
Full name.
- phonestringrequired
Phone number; the lead is matched by it, however it is written.
- emailstring
E-mail address, optional.
- notestring
Note for reception, e.g. the reason for the visit.
- privacyNoticeVersionstringsince 1.7.0
The
versionof the information clause your form showed (from GET /privacy). Stored with the submission and its time; without it the submission is stored without a clause version. - marketingConsentbooleansince 1.7.0
true when the patient ticked the optional marketing box (label
marketingTextfrom GET /privacy). Recorded for SMS (when there is a phone) and e-mail (when there is an address) together with the clause version. It counts only with a validprivacyNoticeVersion: without it no consent is recorded and an earlier opt-out stays. For a returning patient booking an appointment, the e-mail consent covers only the address stored with their lead (or the given address when the lead had none). Omit it, or send false, when the box was not ticked.
curl -X POST "https://app.pozyskajpacjenta.pl/api/v1/booking/appointments" \
-H "Authorization: Bearer $PP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"serviceId": "bsvc_konsultacja",
"resourceId": "bres_piotr",
"startAt": 1791181800,
"patient": {
"name": "Jan Nowak",
"phone": "+48 600 100 200",
"email": "jan.nowak@example.com"
},
"note": "Pierwsza wizyta, ząb boli przy zimnym."
}'import { PozyskajPacjentaClient, isSlotTaken } from "@pozyskajpacjenta/sdk";
const client = new PozyskajPacjentaClient({
apiKey: process.env.PP_API_KEY ?? "",
baseUrl: "https://app.pozyskajpacjenta.pl",
});
try {
const { appointment } = await client.createAppointment({
serviceId: "bsvc_konsultacja",
resourceId: "bres_piotr",
startAt: 1791181800,
patient: {
name: "Jan Nowak",
phone: "+48 600 100 200",
email: "jan.nowak@example.com",
},
note: "Pierwsza wizyta, ząb boli przy zimnym.",
});
console.log(appointment.id);
} catch (error) {
// 409: the slot was taken in the meantime; offer one of that day's free slots
if (isSlotTaken(error)) console.log(error.slots);
else throw error;
}<?php
$body = array(
'serviceId' => 'bsvc_konsultacja',
'resourceId' => 'bres_piotr',
'startAt' => 1791181800,
'patient' => array(
'name' => 'Jan Nowak',
'phone' => '+48 600 100 200',
'email' => 'jan.nowak@example.com',
),
'note' => 'Pierwsza wizyta, ząb boli przy zimnym.',
);
$response = wp_remote_post(
'https://app.pozyskajpacjenta.pl/api/v1/booking/appointments',
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 );Response
201The appointment was booked.
- appointmentobjectrequired
The appointment.
Fields (4)
- idstringrequired
Appointment id.
- startAtintegerrequired
Start, epoch seconds (UTC).
- endAtintegerrequired
End, epoch seconds (UTC).
- leadIdstringrequired
The patient's lead in the clinic's CRM.
Appointment booked (a resend within 5 minutes returns the same)
{
"appointment": {
"id": "Onqp5yPLKb3IFff2WKfnV",
"startAt": 1791181800,
"endAt": 1791183600,
"leadId": "lead_1"
}
}Errors
| Status | code | When |
|---|---|---|
| 400 | The body is not JSON | |
| 400 | serviceId, resourceId or startAt missing | |
| 400 | patient.name or patient.phone missing | |
| 400 | nfz_phone_only | An NFZ service (phone booking only) |
| 401 | No Authorization header | |
| 401 | Unknown or rotated key | |
| 409 | slot_taken | The slot was taken in the meantime (slots trimmed to three) |
| 429 | Over 120 requests in this minute |
Branch on the status and code, never on the Polish error text. All statuses in Errors.
{
"error": "invalid JSON"
}{
"error": "wymagane: serviceId, resourceId, startAt (epoch s, UTC)"
}{
"error": "wymagane: patient.name, patient.phone"
}{
"error": "usługa NFZ — rejestracja telefoniczna",
"code": "nfz_phone_only"
}{
"error": "missing Authorization: Bearer <klucz z panelu Ustawienia>"
}{
"error": "invalid key"
}{
"error": "slot_taken",
"slots": [
{
"resourceId": "bres_piotr",
"startAt": 1791183600,
"endAt": 1791185400,
"localDate": "2026-10-05",
"localStartMin": 540
},
{
"resourceId": "bres_piotr",
"startAt": 1791184500,
"endAt": 1791186300,
"localDate": "2026-10-05",
"localStartMin": 555
},
{
"resourceId": "bres_piotr",
"startAt": 1791185400,
"endAt": 1791187200,
"localDate": "2026-10-05",
"localStartMin": 570
}
]
}{
"error": "przekroczono limit 120 zapytań/min"
}Rate limit
- 120 requests per minute per key, shared by every endpoint.
- Every response to a valid key carries
RateLimit-RemainingandRateLimit-Reset; see Rate limits.
SDK method
client.createAppointment(input: AppointmentInput): Promise<AppointmentCreated>
Throws SlotTakenError (409, with fresh slots); ApiError 400.
The TypeScript tab above uses it. Install and errors: TypeScript SDK.