{
  "openapi": "3.1.0",
  "info": {
    "title": "MentAI — publiczny tor rezerwacji wizyty (dla agentów AI i integracji)",
    "version": "1.0.0",
    "summary": "Public booking API of a MentAI practice: read free slots, request an appointment. No API key.",
    "description": "Ten opis dotyczy PUBLICZNEGO toru rezerwacji jednego gabinetu prowadzonego na MentAI (mentai.pl). Nie wymaga klucza. Adresatem jest agent AI działający w imieniu klienta (osoby, która chce umówić wizytę) albo prosta integracja.\n\nJak to działa:\n1. Poznaj `slug` gabinetu — to końcówka jego publicznej strony rezerwacji `https://mentai.pl/rezerwacja.html?g=<slug>` (gabinet podaje ją na swojej stronie WWW, w wizytówce, w podpisie maila).\n2. `GET /booking/{slug}/availability` bez parametrów zwraca ofertę (usługi, terapeuci). Z `serviceId` (i `therapistId`, gdy gabinet ma terapeutów) zwraca wolne terminy `slots`.\n3. `POST /booking/{slug}/reserve` z wybranym `slotStart` i danymi klienta tworzy PROŚBĘ o wizytę. Standardowo gabinet ją zatwierdza (status `do_decyzji_gabinetu`); potwierdzenie klient dostaje kanałem kontaktu (e-mail/SMS/WhatsApp), nie przez to API.\n\nZasady dla agenta: przekaż klientowi `visitId` i status; nie obiecuj terminu przed potwierdzeniem gabinetu; wpisz w `reason`, że rezerwacja pochodzi od agenta AI (pomaga gabinetowi przy akceptacji); pytaj klienta o zgodę RODO (`rodoConsent`) świadomie — to zgoda osoby, nie agenta; nie wysyłaj treści klinicznych ani danych wrażliwych. Terminy są w czasie lokalnym gabinetu (`timezone`, zwykle Europe/Warsaw), bez oznaczenia strefy — nie przeliczaj na UTC. Kwoty w groszach brutto (`priceGrossGrosze`: 22000 = 220,00 zł).\n\nLimity: rezerwacja ≤ 2 żądania/s (burst 5) na cały serwis; agent w pętli może dostać 429 — odczekaj i ponów raz. Wizyty testowe oznaczaj w `name` prefiksem `TEST` i uprzedź gabinet.\n\nEN: Public booking API of a single MentAI practice. Step 1: find the practice `slug` (from its booking page URL). Step 2: `GET availability` → services, therapists, free `slots` (local time, no timezone suffix). Step 3: `POST reserve` → an appointment REQUEST (`do_decyzji_gabinetu` = awaiting the practice's decision); confirmation reaches the client by e-mail/SMS/WhatsApp. Never promise a slot before confirmation; put `AI agent` in `reason`; amounts are in grosze (PLN cents).",
    "contact": { "name": "MentAI (Understand Sp. z o.o.)", "url": "https://mentai.pl/", "email": "kontakt@mentai.pl" },
    "termsOfService": "https://mentai.pl/warunki-testu.html"
  },
  "externalDocs": { "description": "Instrukcja dla asystentów AI (llms.txt) i API gabinetu z kluczem", "url": "https://mentai.pl/llms.txt" },
  "servers": [
    { "url": "https://02lw3esaj9.execute-api.eu-central-1.amazonaws.com", "description": "Produkcja (AWS eu-central-1, Frankfurt)" }
  ],
  "tags": [ { "name": "booking", "description": "Publiczna rezerwacja wizyty w gabinecie (bez klucza)" } ],
  "paths": {
    "/booking/{slug}/availability": {
      "get": {
        "tags": ["booking"],
        "operationId": "getAvailability",
        "summary": "Offer and free slots of a practice (services, therapists, slots)",
        "description": "Bez `serviceId` zwraca tylko ofertę (`services`, `therapists`), `slots` jest puste. Z `serviceId` liczy wolne terminy. Gdy gabinet ma terapeutów (`therapists` niepuste), podaj `therapistId` (id albo `any` = dowolny) — bez tego `slots` jest puste. Okno: domyślnie 14 dni od `from`, maksymalnie 31 dni; gabinet może mieć własne ograniczenie wyprzedzenia. Terminy w czasie lokalnym gabinetu.",
        "parameters": [
          { "name": "slug", "in": "path", "required": true, "schema": { "type": "string", "pattern": "^[a-z0-9](?:[a-z0-9-]{1,38}[a-z0-9])$" }, "description": "Publiczny identyfikator gabinetu z jego strony rezerwacji (`rezerwacja.html?g=<slug>`)." },
          { "name": "serviceId", "in": "query", "schema": { "type": "string" }, "description": "Id usługi z `services[].serviceId` (np. `bs-…`). Wymagane, żeby dostać `slots`." },
          { "name": "therapistId", "in": "query", "schema": { "type": "string" }, "description": "Id terapeuty z `therapists[].therapistId` albo `any` (dowolny). Wymagane do `slots`, gdy gabinet ma terapeutów." },
          { "name": "from", "in": "query", "schema": { "type": "string", "format": "date" }, "description": "Początek okna (YYYY-MM-DD), nie wcześniej niż dziś. Domyślnie dziś." },
          { "name": "to", "in": "query", "schema": { "type": "string", "format": "date" }, "description": "Koniec okna (YYYY-MM-DD), maksymalnie 31 dni od `from`." }
        ],
        "responses": {
          "200": {
            "description": "Oferta i (gdy podano usługę) wolne terminy.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Availability" },
              "example": { "ok": true, "slug": "demo", "gabinet": { "name": "Gabinet Pokazowy MentAI", "address": "ul. Przykładowa 1, Poznań", "cancelDeadlineHours": 24 }, "timezone": "Europe/Warsaw",
                "services": [ { "serviceId": "bs-73baac70a970", "name": "Pierwsza konsultacja psychologiczna", "durationMinutes": 50, "priceGrossGrosze": 22000 } ],
                "therapists": [ { "therapistId": "eed7c6c4-0000-0000-0000-000000000000", "name": "Anna Przykładowa" } ],
                "service": { "serviceId": "bs-73baac70a970", "name": "Pierwsza konsultacja psychologiczna", "durationMinutes": 50, "priceGrossGrosze": 22000 },
                "slots": ["2026-09-15T10:00", "2026-09-15T11:00", "2026-09-16T09:00"], "videoModuleEnabled": false } } }
          },
          "404": { "description": "`booking_not_found` (zły slug albo rezerwacja online wyłączona), `service_not_found`, `therapist_not_found`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Za dużo żądań — odczekaj i ponów.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/booking/{slug}/reserve": {
      "post": {
        "tags": ["booking"],
        "operationId": "reserveSlot",
        "summary": "Request an appointment in a free slot (practice confirms)",
        "description": "Tworzy PROŚBĘ o wizytę w terminie z listy `slots`. Standardowa odpowiedź: `status: do_decyzji_gabinetu` — gabinet zatwierdza, klient dostaje potwierdzenie kanałem kontaktu. `potwierdzona` tylko w gabinetach z automatyczną akceptacją. Jeśli gabinet ma włączone płatności i podano e-mail, odpowiedź może zawierać `paymentUrl` (link do zapłaty; płatność nie jest warunkiem przyjęcia prośby). Wymagany telefon klienta (9-15 cyfr), e-mail opcjonalny. `rodoConsent` musi być `true` — to świadoma zgoda klienta na przetwarzanie danych w celu umówienia wizyty.",
        "parameters": [ { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ReserveRequest" },
          "example": { "serviceId": "bs-73baac70a970", "therapistId": "any", "slotStart": "2026-09-15T10:00", "name": "Jan Kowalski", "contactPhone": "+48 600 000 000", "contactEmail": "jan@example.com", "rodoConsent": true, "reminderChannel": "sms", "reason": "Umówione przez agenta AI klienta (asystent osobisty). Klient prosi o pierwszą konsultację." } } } },
        "responses": {
          "200": { "description": "Prośba przyjęta.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ReserveResponse" }, "example": { "ok": true, "visitId": "3a1f8c2e-5d6b-4e7f-9a0b-1c2d3e4f5a6b", "status": "do_decyzji_gabinetu" } } } },
          "400": { "description": "`invalid_json`, `name_required`, `phone_required`, `invalid_contact`, `missing_consent`, `invalid_slot`, `therapist_required`, `service_not_offered`, `child_age_required`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "`booking_not_found`, `service_not_found`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "`slot_unavailable` (termin zajęty — pobierz `availability` ponownie), `qualification_required` (usługa wymaga wcześniejszej kwalifikacji w gabinecie), `online_not_available`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "413": { "description": "`payload_too_large`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Limit tempa (2/s na serwis) — odczekaj i ponów raz.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Error": { "type": "object", "required": ["ok", "error"], "properties": { "ok": { "type": "boolean", "const": false }, "error": { "type": "string", "description": "Kod błędu (snake_case) z listy przy odpowiedzi." } } },
      "Service": { "type": "object", "properties": { "serviceId": { "type": "string" }, "name": { "type": "string" }, "durationMinutes": { "type": "integer" }, "priceGrossGrosze": { "type": "integer", "description": "Cena brutto w groszach (22000 = 220,00 zł). 0 lub brak = cena ustalana w gabinecie." }, "requiresQualification": { "type": "boolean" } }, "additionalProperties": true },
      "Therapist": { "type": "object", "properties": { "therapistId": { "type": "string" }, "name": { "type": "string" }, "serviceIds": { "type": "array", "items": { "type": "string" } } }, "additionalProperties": true },
      "Availability": { "type": "object", "required": ["ok", "slug", "gabinet", "timezone", "services", "therapists", "slots"], "properties": {
        "ok": { "type": "boolean" }, "slug": { "type": "string" },
        "gabinet": { "type": "object", "properties": { "name": { "type": "string" }, "address": { "type": "string" }, "headline": { "type": "string" }, "infoNotes": { "type": "string", "description": "Informacje gabinetu dla klientów (dojazd, zasady) — przekaż klientowi." }, "cancelDeadlineHours": { "type": "integer", "description": "Ile godzin przed wizytą można ją bezpłatnie odwołać." } }, "additionalProperties": true },
        "timezone": { "type": "string", "description": "Strefa czasowa terminów w `slots` i `slotStart` (IANA, np. Europe/Warsaw)." },
        "services": { "type": "array", "items": { "$ref": "#/components/schemas/Service" } },
        "therapists": { "type": "array", "items": { "$ref": "#/components/schemas/Therapist" }, "description": "Pusta lista = gabinet jednoosobowy, `therapistId` niepotrzebne." },
        "service": { "$ref": "#/components/schemas/Service" },
        "slots": { "type": "array", "items": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}$" }, "description": "Wolne początki wizyt w czasie lokalnym gabinetu, bez strefy (np. 2026-09-15T10:00)." },
        "videoModuleEnabled": { "type": "boolean", "description": "Czy gabinet prowadzi wizyty online (wtedy `visitMode: online` jest możliwy dla usług online)." }
      }, "additionalProperties": true },
      "ReserveRequest": { "type": "object", "required": ["serviceId", "slotStart", "name", "contactPhone", "rodoConsent"], "properties": {
        "serviceId": { "type": "string" },
        "therapistId": { "type": "string", "description": "Id terapeuty albo `any`. Wymagane, gdy gabinet ma terapeutów." },
        "slotStart": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}$", "description": "Dokładnie jedna wartość z `slots`." },
        "name": { "type": "string", "maxLength": 120, "description": "Imię i nazwisko klienta (osoby umawianej). Wizyty testowe: prefiks `TEST`." },
        "contactPhone": { "type": "string", "description": "Telefon klienta, 9-15 cyfr, może być z +48 i spacjami. Wymagany." },
        "contactEmail": { "type": "string", "format": "email", "description": "Opcjonalny; potrzebny do przypomnień e-mail i linku do płatności." },
        "rodoConsent": { "type": "boolean", "const": true, "description": "Świadoma zgoda klienta na przetwarzanie danych w celu umówienia wizyty (polityka: https://mentai.pl/#polityka-prywatnosci)." },
        "patientName": { "type": "string", "maxLength": 120, "description": "Gdy wizyta dotyczy dziecka: imię dziecka (klient = opiekun)." },
        "patientAge": { "type": "integer", "minimum": 0, "maximum": 17, "description": "Wiek dziecka; wymagany razem z `patientName`." },
        "visitMode": { "type": "string", "enum": ["gabinet", "online"], "description": "`online` tylko gdy `videoModuleEnabled` i usługa online; inaczej 409 `online_not_available`." },
        "reminderChannel": { "type": "string", "enum": ["whatsapp", "sms", "email"], "description": "Preferowany kanał przypomnień; `email` tylko z podanym e-mailem." },
        "reason": { "type": "string", "maxLength": 300, "description": "Krótki powód/kontekst dla gabinetu. Agent AI: wpisz, że rezerwacja pochodzi od agenta i w czyim imieniu." },
        "policyVersion": { "type": "string" }
      } },
      "ReserveResponse": { "type": "object", "required": ["ok", "visitId", "status"], "properties": {
        "ok": { "type": "boolean" },
        "visitId": { "type": "string", "description": "Identyfikator prośby o wizytę — przekaż klientowi." },
        "status": { "type": "string", "enum": ["do_decyzji_gabinetu", "potwierdzona"], "description": "`do_decyzji_gabinetu` = czeka na zatwierdzenie przez gabinet (standard). Potwierdzenie przyjdzie do klienta kanałem kontaktu." },
        "paymentUrl": { "type": "string", "format": "uri", "description": "Opcjonalnie: link do płatności (gabinet z włączonymi płatnościami, podany e-mail)." }
      }, "additionalProperties": true }
    }
  }
}
