# Create a lead

`POST https://app.pozyskajpacjenta.pl/api/v1/leads` · operationId `createLead` · since 1.0.0 · tag Leads

Stores a patient enquiry from your own form in the clinic's CRM. It follows the same path as an enquiry from the clinic's site: reception gets an e-mail, and a configured webhook receives `lead.created`. Call it from your server: the key is a secret. Field names are matched loosely (Polish and English aliases, any letter case), unknown fields are kept, and a lead without a phone or e-mail is stored with a warning unless you ask for `?strict=1`.

## Request

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

### Query parameters

- `strict` (`string`, since 1.3.0): Reject a lead without a phone and e-mail (400 missing_contact), and one with an invalid `timeRequest` (400 invalid_time_request), instead of storing it with `warnings`. One of `1`, `true`.

### Body (application/json)

The enquiry as JSON.

- `name` (`string`): Patient's name. Aliases: full_name, fullname, imie, imię, imie_nazwisko.
- `phone` (`string`): Phone number. Aliases: tel, telefon, phone_number, numer.
- `email` (`string`): E-mail address. Aliases: e-mail, mail.
- `message` (`string`): The patient's message. Aliases: wiadomosc, wiadomość, msg, comment, opis.
- `utm_source` (`string`): Campaign source, stored with the lead's tracking data.
- `utm_medium` (`string`, since 1.1.0): Campaign medium.
- `utm_campaign` (`string`, since 1.1.0): Campaign name.
- `gclid` (`string`): Google Ads click id.
- `fbclid` (`string`): Meta click id.
- `privacyNoticeVersion` (`string`, since 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.
- `marketingConsent` (`boolean`, since 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.
- `timeRequest` (`TimeRequestInput`, since 1.9.0): A request for a time: the patient's preferred windows, for clinics that keep their calendar in their own system.
  - `windows` (`TimeWindow[]`, required): 1 to 3 preferred windows.
    - `date` (`string (date)`, required): Day, YYYY-MM-DD.
    - `from` (`string`, required): Start, HH:MM.
    - `to` (`string`, required): End, HH:MM.
  - `serviceSlug` (`string`): Slug of a published treatment (GET /services).
  - `doctorSlug` (`string`): Slug of a published team member (GET /doctors); omit for any doctor.
  - `serviceName` (`string`): Treatment name as text, when your frontend has no slug.
  - `doctorName` (`string`): Doctor's name as text, when your frontend has no slug.

### cURL

```bash
curl -X POST "https://app.pozyskajpacjenta.pl/api/v1/leads" \
  -H "Authorization: Bearer $PP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Anna Kowalska",
    "phone": "+48 600 100 200",
    "email": "anna.kowalska@example.com",
    "message": "Proszę o kontakt w sprawie higienizacji.",
    "utm_source": "google",
    "utm_medium": "cpc",
    "utm_campaign": "higienizacja-jesien",
    "formularz": "landing-higienizacja"
  }'
```

### TypeScript

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

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

const { leadId, warnings } = await client.createLead({
  name: "Anna Kowalska",
  phone: "+48 600 100 200",
  email: "anna.kowalska@example.com",
  message: "Proszę o kontakt w sprawie higienizacji.",
  utm_source: "google",
  utm_medium: "cpc",
  utm_campaign: "higienizacja-jesien",
  formularz: "landing-higienizacja",
});
```

### PHP

```php
<?php
$body = array(
    'name'         => 'Anna Kowalska',
    'phone'        => '+48 600 100 200',
    'email'        => 'anna.kowalska@example.com',
    'message'      => 'Proszę o kontakt w sprawie higienizacji.',
    'utm_source'   => 'google',
    'utm_medium'   => 'cpc',
    'utm_campaign' => 'higienizacja-jesien',
    'formularz'    => 'landing-higienizacja',
);

$response = wp_remote_post(
    'https://app.pozyskajpacjenta.pl/api/v1/leads',
    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: Polish field names are accepted too

```json
{
  "imie": "Anna Kowalska",
  "telefon": "+48 600 100 200",
  "wiadomosc": "Proszę o kontakt."
}
```

### Request body: A lead with a request for a time (API 1.9.0)

```json
{
  "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"
  }
}
```

## Response

### 200: The lead was stored.

- `ok` (`boolean`, required): Always true.
- `leadId` (`string`, required): Id of the lead in the clinic's CRM.
- `warnings` (`string[]`, since 1.3.0): Only when there is a warning. missing_contact: stored, but without a phone or e-mail, so nobody can call back. invalid_time_request: stored without the request for a time, because `timeRequest` failed validation. One of `missing_contact`, `invalid_time_request`.
- `timeRequest` (`LeadTimeRequest`, since 1.9.0): The stored request for a time, when you sent a valid one.
  - `id` (`string`, required): Id of the request.
  - `status` (`string`, required): pending until reception acts; confirmed (a time was confirmed), proposed (reception proposed another time) or cancelled. One of `pending`, `confirmed`, `proposed`, `cancelled`.
  - `windows` (`TimeWindow[]`, required): The windows that were stored.
    - fields as in `TimeWindow` above
  - `service` (`string | null`, required): Name of the matched treatment, or the text you sent.
  - `doctor` (`string | null`, required): The doctor (matched or as sent); null for any doctor.

Example (Lead stored):

```json
{
  "ok": true,
  "leadId": "rS2bIhedVSStSQLc7DL_T"
}
```

Example (Stored without a phone or e-mail):

```json
{
  "ok": true,
  "leadId": "Sj2mY5tuT1UZP0QfgdvLH",
  "warnings": [
    "missing_contact"
  ]
}
```

Example (Stored with the request for a time):

```json
{
  "ok": true,
  "leadId": "PydHg_es-Kqw5bs1Y0-wu",
  "timeRequest": {
    "id": "tqf-L61QFuCLO2ZXCM1OL",
    "status": "pending",
    "windows": [
      {
        "date": "2026-10-05",
        "from": "08:00",
        "to": "12:00"
      },
      {
        "date": "2026-10-06",
        "from": "15:00",
        "to": "18:00"
      }
    ],
    "service": "Higienizacja",
    "doctor": "lek. dent. Maria Zielińska"
  }
}
```

Example (Stored without an invalid request for a time):

```json
{
  "ok": true,
  "leadId": "J_ashz8nW11Am5rfS90rB",
  "warnings": [
    "invalid_time_request"
  ]
}
```

## Errors

| Status | code | When |
| --- | --- | --- |
| 400 | `invalid_json` | The body is not JSON |
| 400 | `invalid_body` | The JSON is not an object |
| 400 | `missing_contact` | ?strict=1 and no phone or e-mail |
| 400 | `invalid_time_request` | ?strict=1 and an invalid request for a time |
| 401 |  | No Authorization header |
| 401 |  | Unknown or rotated key |
| 429 |  | Over 120 requests in this minute |

400 (The body is not JSON):

```json
{
  "error": "invalid JSON",
  "code": "invalid_json"
}
```

400 (The JSON is not an object):

```json
{
  "error": "treść musi być obiektem JSON z danymi leada",
  "code": "invalid_body"
}
```

400 (?strict=1 and no phone or e-mail):

```json
{
  "error": "wymagany kontakt: telefon (pole phone/tel/telefon/phone_number/numer, min. 6 cyfr) albo e-mail (pole email/e-mail/mail)",
  "code": "missing_contact"
}
```

400 (?strict=1 and an invalid request for a time):

```json
{
  "error": "timeRequest: windows.0.date: nie ma takiego dnia w kalendarzu",
  "code": "invalid_time_request"
}
```

401 (No Authorization header):

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

401 (Unknown or rotated key):

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

429 (Over 120 requests in this minute):

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

## Rate limit

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

## SDK method

`client.createLead(lead: LeadInput, options?: { strict?: boolean }): Promise<LeadCreated>`

Throws ApiError (400) with `code`; with `{ strict: true }` also missing_contact and invalid_time_request.
