{
  "openapi": "3.1.0",
  "info": {
    "title": "Mandroya Senses - publiczne API",
    "version": "1.0.0",
    "description": "Publiczne, nieautoryzowane API centrum terapeutycznego Mandroya Senses w Warszawie. Udostępnia katalog usług, pakiety i ceny, listę terapeutek, placówki partnerskie programu Mapa Rozwoju oraz wolne terminy. Tych samych danych używa strona mandroya.pl. Wszystkie ceny są w złotych (PLN) i dotyczą jednej wizyty, o ile opis nie mówi inaczej. API jest tylko do odczytu: rezerwacji nie da się złożyć programowo, terminy wybiera się w systemie pacjenta pod adresem https://wizyty.mandroya.pl.\n\n## Wersjonowanie\n\nWersja jest częścią ścieżki: bieżąca to `/api/v1`. Zmiany niełamiące kontraktu (nowe pole w odpowiedzi, nowy endpoint, nowa wartość w polu opisowym) wchodzą w obrębie tej samej wersji, więc klient powinien ignorować nieznane pola. Zmiana łamiąca kontrakt (usunięcie lub zmiana znaczenia pola, zmiana typu) dostanie nową ścieżkę `/api/v2`. Wycofanie sygnalizujemy nagłówkami. `Deprecation: true` oznacza ścieżkę, która działa, ale nie jest zalecana w nowych integracjach; towarzyszy jej nagłówek `Link` z `rel=\"deprecation\"` prowadzący do tej dokumentacji. `Sunset` z datą w formacie HTTP-date pojawia się dopiero wtedy, gdy wersja jest faktycznie wygaszana, i nie wcześniej niż 12 miesięcy przed wyłączeniem. Dopóki nie ma daty wyłączenia, nie wysyłamy tego nagłówka, bo data to zobowiązanie, a nie ozdoba.\n\nŚcieżki bez numeru wersji niosą już dziś `Deprecation: true`.\n\nŚcieżki bez numeru wersji (`/api/public-services` i pozostałe) działają nadal i wskazują na wersję 1. Zostają na stałe ze względu na klientów, którzy już z nich korzystają, ale w nowych integracjach używaj `/api/v1`.\n\n## Limity zapytań\n\nRuch do API jest ograniczany. Bieżący limit to 120 zapytań na minutę na adres IP. Każda odpowiedź niesie nagłówki `RateLimit-Limit`, `RateLimit-Remaining` i `RateLimit-Reset` (oraz ich odpowiedniki z przedrostkiem `X-`). Po przekroczeniu limitu API zwraca status 429 wraz z nagłówkiem `Retry-After`. Czytaj te nagłówki i po odpowiedzi 429 odczekaj wskazany czas, zamiast ponawiać zapytanie od razu. Limity i sposób ich stosowania możemy zmienić, jeżeli będzie tego wymagała stabilność usługi.\n\nOdpowiedzi niosą nagłówek `Cache-Control`: 60 sekund dla przeglądarki i 300 sekund dla buforów pośredniczących przy katalogu, pakietach, terapeutkach i placówkach, oraz 30 sekund przy terminach, które mają pozostać aktualne. Katalog i cennik zmieniają się rzadko, więc zalecamy buforowanie odpowiedzi po swojej stronie zgodnie z tym nagłówkiem.",
    "contact": {
      "name": "Mandroya Senses",
      "email": "senses@mandroya.pl",
      "url": "https://mandroya.pl/kontakt"
    },
    "license": {
      "name": "Dane dostępne do użytku informacyjnego",
      "url": "https://mandroya.pl/regulamin"
    }
  },
  "servers": [
    {
      "url": "https://mandroya.pl/api/v1",
      "description": "Produkcja, wersja 1"
    }
  ],
  "tags": [
    {
      "name": "Katalog",
      "description": "Usługi, pakiety i ceny"
    },
    {
      "name": "Zespół",
      "description": "Terapeutki i placówki"
    },
    {
      "name": "Terminy",
      "description": "Wolne terminy do rezerwacji"
    }
  ],
  "paths": {
    "/public-services": {
      "get": {
        "operationId": "listServices",
        "tags": [
          "Katalog"
        ],
        "summary": "Lista usług wraz z cenami",
        "description": "Zwraca pełny katalog usług: nazwę, kategorię, czas trwania, cenę pojedynczej wizyty oraz dwie flagi. is_online_only oznacza usługę prowadzoną wyłącznie zdalnie. is_self_bookable oznacza, że usługę można zarezerwować samodzielnie; usługi bez tej flagi nie mają wolnych terminów w /public-availability. Usługi z ceną 0 zł to pozycje techniczne i bezpłatne badania przesiewowe.",
        "responses": {
          "200": {
            "description": "Odpowiedź poprawna",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "services"
                  ],
                  "properties": {
                    "services": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "name",
                          "duration_minutes",
                          "price",
                          "is_online_only",
                          "is_self_bookable",
                          "category_name"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "name": {
                            "type": "string",
                            "description": "Pełna nazwa usługi, np. Terapia SI (30 min.)"
                          },
                          "short_name": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "Nazwa skrócona, zwykle pusta"
                          },
                          "description": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "Opis redakcyjny; ma go mniejszość usług"
                          },
                          "duration_minutes": {
                            "type": "integer",
                            "description": "Czas trwania wizyty w minutach"
                          },
                          "price": {
                            "type": "integer",
                            "description": "Cena jednej wizyty w PLN"
                          },
                          "is_online_only": {
                            "type": "boolean"
                          },
                          "is_self_bookable": {
                            "type": "boolean"
                          },
                          "category_name": {
                            "type": "string",
                            "description": "Kategoria z systemu",
                            "examples": [
                              "Logopedia",
                              "Integracja Sensoryczna",
                              "Diagnozy",
                              "Psychologia",
                              "Pozostałe Terapie"
                            ]
                          }
                        }
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "RateLimit-Limit": {
                "description": "Liczba zapytań dozwolona w oknie",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Remaining": {
                "description": "Ile zapytań zostało w bieżącym oknie",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Reset": {
                "description": "Za ile sekund okno się zeruje",
                "schema": {
                  "type": "integer"
                }
              },
              "Deprecation": {
                "description": "Obecny na ścieżkach bez numeru wersji. Oznacza, że ścieżka jest wspierana, ale w nowych integracjach należy używać /api/v1.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "true"
                  ]
                }
              },
              "Sunset": {
                "description": "Data wyłączenia w formacie HTTP-date. Pojawia się dopiero, gdy wersja jest wygaszana, nie wcześniej niż 12 miesięcy przed wyłączeniem.",
                "schema": {
                  "type": "string"
                }
              },
              "Link": {
                "description": "Odnośnik do dokumentacji, z rel=\"deprecation\".",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Przekroczono limit zapytań",
            "headers": {
              "RateLimit-Limit": {
                "description": "Liczba zapytań dozwolona w oknie",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Remaining": {
                "description": "Ile zapytań zostało w bieżącym oknie",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Reset": {
                "description": "Za ile sekund okno się zeruje",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Po ilu sekundach ponowić",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/public-packages": {
      "get": {
        "operationId": "listPackages",
        "tags": [
          "Katalog"
        ],
        "summary": "Pakiety wizyt",
        "description": "Pakiety obniżające cenę za wizytę. usage_count to liczba wizyt w pakiecie, current_price to cena całego pakietu w PLN, a validity_weeks to okres ważności w tygodniach. Cenę za jedną wizytę liczy się jako current_price podzielone przez usage_count. Pakiet wiąże się z usługą przez service_id.",
        "responses": {
          "200": {
            "description": "Odpowiedź poprawna",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "packages"
                  ],
                  "properties": {
                    "packages": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "name",
                          "usage_count",
                          "validity_weeks",
                          "current_price",
                          "service_id",
                          "service_name"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "name": {
                            "type": "string"
                          },
                          "short_name": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "description": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "usage_count": {
                            "type": "integer",
                            "description": "Liczba wizyt w pakiecie"
                          },
                          "validity_weeks": {
                            "type": "integer",
                            "description": "Ważność pakietu w tygodniach"
                          },
                          "current_price": {
                            "type": "integer",
                            "description": "Cena całego pakietu w PLN"
                          },
                          "service_id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "service_name": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "RateLimit-Limit": {
                "description": "Liczba zapytań dozwolona w oknie",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Remaining": {
                "description": "Ile zapytań zostało w bieżącym oknie",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Reset": {
                "description": "Za ile sekund okno się zeruje",
                "schema": {
                  "type": "integer"
                }
              },
              "Deprecation": {
                "description": "Obecny na ścieżkach bez numeru wersji. Oznacza, że ścieżka jest wspierana, ale w nowych integracjach należy używać /api/v1.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "true"
                  ]
                }
              },
              "Sunset": {
                "description": "Data wyłączenia w formacie HTTP-date. Pojawia się dopiero, gdy wersja jest wygaszana, nie wcześniej niż 12 miesięcy przed wyłączeniem.",
                "schema": {
                  "type": "string"
                }
              },
              "Link": {
                "description": "Odnośnik do dokumentacji, z rel=\"deprecation\".",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Przekroczono limit zapytań",
            "headers": {
              "RateLimit-Limit": {
                "description": "Liczba zapytań dozwolona w oknie",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Remaining": {
                "description": "Ile zapytań zostało w bieżącym oknie",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Reset": {
                "description": "Za ile sekund okno się zeruje",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Po ilu sekundach ponowić",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/public-therapists": {
      "get": {
        "operationId": "listTherapists",
        "tags": [
          "Zespół"
        ],
        "summary": "Terapeutki i ich usługi",
        "description": "Lista terapeutek przyjmujących w centrum wraz z usługami, które prowadzą. Pola avatar_url, bio i education bywają puste. Nie zawiera żadnych danych pacjentów.",
        "responses": {
          "200": {
            "description": "Odpowiedź poprawna",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "therapists"
                  ],
                  "properties": {
                    "therapists": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "first_name",
                          "last_name",
                          "services"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "first_name": {
                            "type": "string"
                          },
                          "last_name": {
                            "type": "string"
                          },
                          "avatar_url": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "uri"
                          },
                          "bio": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "education": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "services": {
                            "type": "array",
                            "description": "Usługi prowadzone przez tę osobę",
                            "items": {
                              "type": "object",
                              "required": [
                                "id",
                                "name"
                              ],
                              "properties": {
                                "id": {
                                  "type": "string",
                                  "format": "uuid"
                                },
                                "name": {
                                  "type": "string"
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "RateLimit-Limit": {
                "description": "Liczba zapytań dozwolona w oknie",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Remaining": {
                "description": "Ile zapytań zostało w bieżącym oknie",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Reset": {
                "description": "Za ile sekund okno się zeruje",
                "schema": {
                  "type": "integer"
                }
              },
              "Deprecation": {
                "description": "Obecny na ścieżkach bez numeru wersji. Oznacza, że ścieżka jest wspierana, ale w nowych integracjach należy używać /api/v1.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "true"
                  ]
                }
              },
              "Sunset": {
                "description": "Data wyłączenia w formacie HTTP-date. Pojawia się dopiero, gdy wersja jest wygaszana, nie wcześniej niż 12 miesięcy przed wyłączeniem.",
                "schema": {
                  "type": "string"
                }
              },
              "Link": {
                "description": "Odnośnik do dokumentacji, z rel=\"deprecation\".",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Przekroczono limit zapytań",
            "headers": {
              "RateLimit-Limit": {
                "description": "Liczba zapytań dozwolona w oknie",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Remaining": {
                "description": "Ile zapytań zostało w bieżącym oknie",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Reset": {
                "description": "Za ile sekund okno się zeruje",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Po ilu sekundach ponowić",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/public-institutions": {
      "get": {
        "operationId": "listInstitutions",
        "tags": [
          "Zespół"
        ],
        "summary": "Placówki partnerskie programu Mapa Rozwoju",
        "description": "Aktywne placówki (przedszkola, żłobki, szkoły) objęte programem Mapa Rozwoju. Dzieci z tych placówek mają preferencyjne warunki na diagnozę i terapię.",
        "responses": {
          "200": {
            "description": "Odpowiedź poprawna",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "institutions"
                  ],
                  "properties": {
                    "institutions": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "name",
                          "address"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "name": {
                            "type": "string"
                          },
                          "address": {
                            "type": "string"
                          },
                          "website_url": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "uri"
                          }
                        }
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "RateLimit-Limit": {
                "description": "Liczba zapytań dozwolona w oknie",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Remaining": {
                "description": "Ile zapytań zostało w bieżącym oknie",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Reset": {
                "description": "Za ile sekund okno się zeruje",
                "schema": {
                  "type": "integer"
                }
              },
              "Deprecation": {
                "description": "Obecny na ścieżkach bez numeru wersji. Oznacza, że ścieżka jest wspierana, ale w nowych integracjach należy używać /api/v1.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "true"
                  ]
                }
              },
              "Sunset": {
                "description": "Data wyłączenia w formacie HTTP-date. Pojawia się dopiero, gdy wersja jest wygaszana, nie wcześniej niż 12 miesięcy przed wyłączeniem.",
                "schema": {
                  "type": "string"
                }
              },
              "Link": {
                "description": "Odnośnik do dokumentacji, z rel=\"deprecation\".",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Przekroczono limit zapytań",
            "headers": {
              "RateLimit-Limit": {
                "description": "Liczba zapytań dozwolona w oknie",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Remaining": {
                "description": "Ile zapytań zostało w bieżącym oknie",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Reset": {
                "description": "Za ile sekund okno się zeruje",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Po ilu sekundach ponowić",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/public-availability": {
      "get": {
        "operationId": "listAvailability",
        "tags": [
          "Terminy"
        ],
        "summary": "Wolne terminy dla usługi",
        "description": "Wolne terminy dla jednej usługi w zadanym zakresie dat. Zakres nie może przekraczać 35 dni. API zwraca wyłącznie terminy faktycznie dostępne do samodzielnej rezerwacji, więc początek zwróconego zakresu bywa późniejszy niż date_from. Usługi bez flagi is_self_bookable nie mają terminów w ogóle.\n\nUwaga na interpretację wyniku: sloty przychodzą jako siatka co 10 minut, więc ich liczba nie odpowiada liczbie wizyt, które da się umówić. Okno 13:00-18:30 to 29 slotów, ale mieści 6 wizyt po 50 minut. Żeby policzyć realną pojemność dnia, trzeba upakować nienachodzące przedziały o długości duration_minutes danej usługi.",
        "parameters": [
          {
            "name": "service_id",
            "in": "query",
            "required": true,
            "description": "Identyfikator usługi z /public-services",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "date_from",
            "in": "query",
            "required": true,
            "description": "Początek zakresu, format YYYY-MM-DD",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "date_to",
            "in": "query",
            "required": true,
            "description": "Koniec zakresu, format YYYY-MM-DD. Maksymalnie 35 dni od date_from",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Odpowiedź poprawna",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "slots"
                  ],
                  "properties": {
                    "slots": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "slot_date",
                          "start_time",
                          "end_time",
                          "is_online",
                          "available_therapists"
                        ],
                        "properties": {
                          "slot_date": {
                            "type": "string",
                            "format": "date"
                          },
                          "start_time": {
                            "type": "string",
                            "description": "Godzina rozpoczęcia, format HH:MM:SS"
                          },
                          "end_time": {
                            "type": "string",
                            "description": "Godzina zakończenia slotu, format HH:MM:SS"
                          },
                          "is_online": {
                            "type": "boolean"
                          },
                          "available_therapists": {
                            "type": "integer",
                            "description": "Ile terapeutek jest wolnych w tym slocie"
                          }
                        }
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "RateLimit-Limit": {
                "description": "Liczba zapytań dozwolona w oknie",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Remaining": {
                "description": "Ile zapytań zostało w bieżącym oknie",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Reset": {
                "description": "Za ile sekund okno się zeruje",
                "schema": {
                  "type": "integer"
                }
              },
              "Deprecation": {
                "description": "Obecny na ścieżkach bez numeru wersji. Oznacza, że ścieżka jest wspierana, ale w nowych integracjach należy używać /api/v1.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "true"
                  ]
                }
              },
              "Sunset": {
                "description": "Data wyłączenia w formacie HTTP-date. Pojawia się dopiero, gdy wersja jest wygaszana, nie wcześniej niż 12 miesięcy przed wyłączeniem.",
                "schema": {
                  "type": "string"
                }
              },
              "Link": {
                "description": "Odnośnik do dokumentacji, z rel=\"deprecation\".",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Brak lub niepoprawny service_id, brak zakresu dat albo zakres dłuższy niż 35 dni",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Przekroczono limit zapytań",
            "headers": {
              "RateLimit-Limit": {
                "description": "Liczba zapytań dozwolona w oknie",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Remaining": {
                "description": "Ile zapytań zostało w bieżącym oknie",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Reset": {
                "description": "Za ile sekund okno się zeruje",
                "schema": {
                  "type": "integer"
                }
              },
              "Retry-After": {
                "description": "Po ilu sekundach ponowić",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "description": "Błąd zwracany przez API. Pole error zawsze niesie komunikat czytelny dla człowieka. Pola code i hint pojawiają się przy błędach routingu (nieznana ścieżka).",
        "properties": {
          "error": {
            "type": "string",
            "examples": [
              "Nieprawidlowy service_id",
              "Zakres dat za duzy (max 35 dni)"
            ]
          },
          "code": {
            "type": "string",
            "examples": [
              "endpoint_not_found",
              "rate_limit_exceeded"
            ]
          },
          "hint": {
            "type": "string",
            "description": "Podpowiedź, gdzie szukać dalej"
          }
        }
      }
    }
  },
  "externalDocs": {
    "description": "Dokumentacja API",
    "url": "https://mandroya.pl/api"
  }
}
