Skip to content
Documentation
pozyskajpacjentaDocs

API referenceBooking

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 resourceId of the slot.

  • startAtintegerrequired

    Start, epoch seconds (UTC): the startAt of 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 version of 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 marketingText from 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 valid privacyNoticeVersion: 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

StatuscodeWhen
400The body is not JSON
400serviceId, resourceId or startAt missing
400patient.name or patient.phone missing
400nfz_phone_onlyAn NFZ service (phone booking only)
401No Authorization header
401Unknown or rotated key
409slot_takenThe slot was taken in the meantime (slots trimmed to three)
429Over 120 requests in this minute

Branch on the status and code, never on the Polish error text. All statuses in Errors.

400 · The body is not JSON
{
  "error": "invalid JSON"
}
400 · serviceId, resourceId or startAt missing
{
  "error": "wymagane: serviceId, resourceId, startAt (epoch s, UTC)"
}
400 · patient.name or patient.phone missing
{
  "error": "wymagane: patient.name, patient.phone"
}
400 · An NFZ service (phone booking only)
{
  "error": "usługa NFZ — rejestracja telefoniczna",
  "code": "nfz_phone_only"
}
401 · No Authorization header
{
  "error": "missing Authorization: Bearer <klucz z panelu Ustawienia>"
}
401 · Unknown or rotated key
{
  "error": "invalid key"
}
409 · The slot was taken in the meantime (slots trimmed to three)
{
  "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
    }
  ]
}
429 · Over 120 requests in this minute
{
  "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-Remaining and RateLimit-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.