Concepts
Booking modes
Online booking, a request for a time, an external widget or the phone: what each clinic uses and what your frontend does.
A clinic takes bookings where its calendar already is, so your frontend has to follow the clinic's mode. This page lists the modes, how to recognise each in the API, and which endpoints to call.
Which mode a clinic uses
The clinic's site shows exactly one booking call to action per page. Read the blocks of its pages (GET /content) and follow the same order of priority:
| Mode | The clinic's calendar | On the clinic's site | Your frontend |
|---|---|---|---|
| Online booking | In the panel (the clinic's schedule) | block booking.v1 | catalog, slots, book |
| ZnanyLekarz widget | At ZnanyLekarz | block znanylekarz-embed.v1 | embed the same widget with the block's props |
| Booksy widget | At Booksy | block booksy-embed.v1 | embed the same widget with the block's props |
| Phone | Anywhere, reception answers | the page's cta is a tel: link | show the phone from site.profile.phone |
| Request a time | In the clinic's own practice software | block lead-form.v1 with mode: "time-request" | POST /leads with timeRequest |
A plain enquiry form (lead-form.v1 without a mode, or mode: "contact") is the fallback: send it to POST /leads. Bookings through ZnanyLekarz or Booksy go to those services, not to this API.
Online booking
The clinic keeps its schedule in the panel; the API books into the same calendar the clinic's site uses, so there is no second schedule to sync.
- Catalog. GET /booking/catalog lists the active services and specialists. A service with an empty
resourceIdscannot be booked today. A treatment points at its service withbookingServiceId, a team member at their specialist withbookingResourceId, so a "book" button on a treatment page knows its service. - Slots. GET /booking/slots returns free slots of a service for up to 31 days, counted live (never cached). Omit
resourcefor any specialist. Times are UTC epoch seconds;localDateandlocalStartMingive the clinic's local date and minute (Europe/Warsaw) for display. - Book. POST /booking/appointments with the slot's
startAtandresourceIdand the patient's name and phone. Reception gets a "Nowa wizyta" e-mail with an .ics file; when you send the patient's e-mail, the patient gets the confirmation and reminder e-mails the clinic's site sends.
When someone takes the slot between your two calls you get 409 with error: "slot_taken" and slots, that day's free slots right now: offer one of them, nothing was stored. Sending the same data again within 5 minutes returns the existing appointment instead of booking twice, so a retry after a timeout is safe.
import { isSlotTaken } from "@pozyskajpacjenta/sdk";
try {
await client.createAppointment({ serviceId, resourceId, startAt, patient });
} catch (error) {
if (isSlotTaken(error)) showSlots(error.slots);
else throw error;
}Request a time
Since API 1.9.0, clinics that keep their calendar in their own practice software ask for a time instead of showing free slots: the patient picks a treatment, optionally a doctor, and 1 to 3 preferred windows. Reception gets a "Prośba o termin" e-mail, confirms one time or proposes another in the panel, and enters the visit in its own system; the patient hears back by e-mail when they left an address. Nothing is synced with the clinic's calendar.
Send it as timeRequest on POST /leads:
{
"name": "Jan Nowak",
"phone": "+48 600 100 200",
"timeRequest": {
"windows": [
{
"date": "2026-10-05",
"from": "08:00",
"to": "12:00"
},
{
"date": "2026-10-06",
"from": "15:00",
"to": "18:00"
}
],
"serviceSlug": "higienizacja",
"doctorSlug": "maria-zielinska"
}
}- Windows are in Polish time. The clinic's site offers parts of the day: morning 08:00–12:00, midday 12:00–15:00, afternoon 15:00–18:00 and evening 18:00–20:00, and only on days and parts of the day the clinic is open (
site.profile.openingHours). The SDK exportsTIME_OF_DAYandwindowFromPart(date, part)to build them. serviceSluganddoctorSlugcome from GET /services and GET /doctors; without slugs sendserviceNameordoctorNameas text.- Windows in the past or more than 180 days ahead are dropped, and
datehas to be a real calendar day. - An invalid request never loses the lead: it is stored without the request and the response carries
warnings: ["invalid_time_request"]. With?strict=1you get 400invalid_time_requestinstead.
The response echoes the stored request with status: "pending"; reception then confirms a time or proposes another in the panel.
Phone and NFZ
Services under the public health fund are phone-only. A catalog service with nfz: true is for information: show it with a note that booking is by phone and the clinic's phone number. GET /booking/slots and POST /booking/appointments answer 400 with code: "nfz_phone_only" for it, and nothing is stored.