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. A service whose bookingPolicy is confirm or pay answers status: pending: the slot is held until pendingUntil and the lead's stage changes only once the visit is confirmed; with a prepayment the answer adds paymentUrl.
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. - locationIdstringsince 1.12.0
The location picked; the slot must be there, otherwise 409.
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 (7)
- idstringrequired
Appointment id.
- startAtintegerrequired
Start, epoch seconds (UTC).
- endAtintegerrequired
End, epoch seconds (UTC).
- leadIdstringrequired
The patient's lead in the clinic's CRM.
- statusstringsince 1.13.0
confirmed: the visit is booked. pending: the slot is held while the clinic confirms it or, with
paymentUrl, until the patient pays. Tell the patient plainly that the clinic will confirm. Absent on older servers, which means confirmed.One of
confirmedpending - pendingUntilintegersince 1.13.0
With pending: until when the slot is held, epoch seconds (UTC). Without a decision or the payment by then the visit expires and the patient gets an e-mail. Since 1.23.1 never later than
startAt: the clinic decides at the latest an hour before the visit (a visit closer than that still leaves at least 15 minutes, but never past its start) and a payment window closes before the visit starts; a visit too close for an online payment (less than Stripe's minimum window) is booked without one. - paymentUrlstring (uri)since 1.13.0
The clinic's Stripe Checkout page for the prepayment or deposit. Send the patient there; after paying they come back to the clinic's site (/wizyta/platnosc/…). Absent when there is nothing to pay.
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.