{"openapi":"3.1.0","info":{"title":"Booking Public API","version":"1.0.0","summary":"Публичный API бронирования (no-auth, по tenant slug).","description":"Публичная поверхность сервиса бронирования Booking (booking.tehprof.kz).\n\nЭти endpoint'ы предназначены для встраиваемого виджета и сторонних интеграторов: показать ресурсы тенанта, свободные слоты и создать бронь от имени конечного клиента. Авторизация не требуется — тенант идентифицируется публичным `slug`. Управляющие операции (CRUD ресурсов, настройки, аналитика) живут в приватном `/api/v2/*` и требуют контекста портала Bitrix24 (а с F85.W2 — API-ключа).\n\nКанон-референс: Calendly public scheduling API.","contact":{"name":"TehProf Booking","url":"https://booking.tehprof.kz"}},"servers":[{"url":"https://booking.tehprof.kz/api/public/v1","description":"Production"}],"tags":[{"name":"Tenant","description":"Публичные данные тенанта и брендинг"},{"name":"Resources","description":"Ресурсы, группы, слоты, сценарий бронирования"},{"name":"Reservations","description":"Создание брони конечным клиентом"},{"name":"Media","description":"Фотографии ресурсов"}],"paths":{"/branding/{slug}":{"get":{"tags":["Tenant"],"summary":"Брендинг тенанта по slug","description":"Логотип, цвета и название бизнеса для публичной страницы / виджета.","parameters":[{"$ref":"#/components/parameters/Slug"}],"responses":{"200":{"description":"Брендинг тенанта","content":{"application/json":{"schema":{"type":"object","required":["ok","tenant"],"properties":{"ok":{"type":"boolean","const":true},"tenant":{"type":"object","properties":{"publicSlug":{"type":"string"},"name":{"type":"string"}}},"branding":{"$ref":"#/components/schemas/Branding"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"},"503":{"$ref":"#/components/responses/Unavailable"}}}},"/tenant/{slug}":{"get":{"tags":["Tenant"],"summary":"Данные тенанта","description":"Идентификатор, slug, название и брендинг. В отличие от `/branding/{slug}`, возвращает `tenant.id`.","parameters":[{"$ref":"#/components/parameters/Slug"}],"responses":{"200":{"description":"Данные тенанта","content":{"application/json":{"schema":{"type":"object","required":["ok","tenant"],"properties":{"ok":{"type":"boolean","const":true},"tenant":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"publicSlug":{"type":"string"},"name":{"type":"string"}}},"branding":{"$ref":"#/components/schemas/Branding"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/tenant/{slug}/resources":{"get":{"tags":["Resources"],"summary":"Список ресурсов тенанта","description":"Только публично-видимые активные ресурсы (`listing_visible = true AND is_active = true`). Кэшируется на 120 секунд.","parameters":[{"$ref":"#/components/parameters/Slug"},{"name":"group_id","in":"query","required":false,"description":"Фильтр по группе. UUID группы ИЛИ литерал `none` (ресурсы без группы). Принимается также как `groupId`.","schema":{"type":"string"}},{"name":"search","in":"query","required":false,"description":"Поиск по названию ресурса (1–100 символов; вне диапазона игнорируется).","schema":{"type":"string","minLength":1,"maxLength":100}}],"responses":{"200":{"description":"Список ресурсов","headers":{"Cache-Control":{"schema":{"type":"string","example":"public, max-age=120"}}},"content":{"application/json":{"schema":{"type":"object","required":["ok","resources"],"properties":{"ok":{"type":"boolean","const":true},"resources":{"type":"array","items":{"$ref":"#/components/schemas/PublicResource"}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/tenant/{slug}/groups":{"get":{"tags":["Resources"],"summary":"Группы ресурсов тенанта","description":"Возвращаются только группы, содержащие хотя бы один публично-видимый ресурс. Кэшируется на 300 секунд.","parameters":[{"$ref":"#/components/parameters/Slug"}],"responses":{"200":{"description":"Список групп","headers":{"Cache-Control":{"schema":{"type":"string","example":"public, max-age=300"}}},"content":{"application/json":{"schema":{"type":"object","required":["ok","groups"],"properties":{"ok":{"type":"boolean","const":true},"groups":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"color":{"type":["string","null"]},"icon":{"type":["string","null"]},"resourceCount":{"type":"integer"}}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/resources/{resourceId}/slots":{"get":{"tags":["Resources"],"summary":"Свободные слоты ресурса на дату","description":"Возвращает только доступные слоты на указанную дату (окно `[date 00:00Z, date+1d 00:00Z)`).","parameters":[{"$ref":"#/components/parameters/ResourceId"},{"name":"date","in":"query","required":true,"description":"Дата в формате `YYYY-MM-DD`.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},{"name":"duration_minutes","in":"query","required":false,"description":"Длительность слота в минутах (15–480).","schema":{"type":"integer","minimum":15,"maximum":480,"default":60}}],"responses":{"200":{"description":"Свободные слоты","content":{"application/json":{"schema":{"type":"object","required":["ok","slots"],"properties":{"ok":{"type":"boolean","const":true},"slots":{"type":"array","items":{"$ref":"#/components/schemas/Slot"}}}}}}},"400":{"description":"invalid_resource_id | invalid_date | invalid_duration","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/resources/{resourceId}/scenario":{"get":{"tags":["Resources"],"summary":"Сценарий бронирования ресурса","description":"Конфигурация формы бронирования: тип сценария, услуги, закрытые даты, таймзона. Кэшируется на 60 секунд.","parameters":[{"$ref":"#/components/parameters/ResourceId"}],"responses":{"200":{"description":"Сценарий ресурса","headers":{"Cache-Control":{"schema":{"type":"string","example":"public, max-age=60"}}},"content":{"application/json":{"schema":{"type":"object","required":["ok","scenario","resource"],"properties":{"ok":{"type":"boolean","const":true},"scenario":{"type":"string","description":"Тип сценария (`auto` нормализуется в `service_time`).","examples":["service_time","rental","hybrid"]},"config":{"type":"object","description":"Конфигурация сценария (услуги, длительности, времена заезда/выезда)."},"closedDates":{"type":"array","items":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},"resource":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"timezone":{"type":"string"},"publicSlug":{"type":["string","null"]},"adapterType":{"type":["string","null"]}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/resources/{resourceId}/services":{"get":{"tags":["Resources"],"summary":"Услуги ресурса с ценами","description":"Публичный список услуг ресурса с ценой каждой услуги (резолв override→день-недели→базовая по текущей дате). Гард `listing_visible = true AND is_active = true`. Кэшируется на 60 секунд. Авторизация не требуется.","parameters":[{"$ref":"#/components/parameters/ResourceId"}],"responses":{"200":{"description":"Список услуг с ценами","headers":{"Cache-Control":{"schema":{"type":"string","example":"public, max-age=60"}}},"content":{"application/json":{"schema":{"type":"object","required":["ok","resource","services"],"properties":{"ok":{"type":"boolean","const":true},"resource":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"publicSlug":{"type":["string","null"]}}},"services":{"type":"array","items":{"$ref":"#/components/schemas/PublicService"}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/reservations":{"post":{"tags":["Reservations"],"summary":"Создать бронь (конечный клиент)","description":"Создаёт бронь от имени гостя. Два режима, определяются автоматически:\n\n- **Слот** (`startAt`/`endAt`): запись на услугу в конкретное время.\n- **Аренда** (`checkInDate`/`checkOutDate`): посуточная аренда; время заезда/выезда сервер берёт из сценария ресурса.\n\nБронь создаётся в статусе `pending`. Сумма вычисляется на сервере (клиент сумму не передаёт). Лимит: 5 запросов / 10 минут на пару (IP, ресурс).","requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/SlotReservationRequest"},{"$ref":"#/components/schemas/RentalReservationRequest"}]}}}},"responses":{"200":{"description":"Бронь создана (статус pending). Внимание: возвращается код 200, не 201.","content":{"application/json":{"schema":{"type":"object","required":["ok","reservation"],"properties":{"ok":{"type":"boolean","const":true},"reservation":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"startAt":{"type":"string","format":"date-time"},"endAt":{"type":"string","format":"date-time"},"status":{"type":"string","const":"pending"}}}}}}}},"400":{"description":"validation_failed — ошибки полей тела","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false},"error":{"type":"object","properties":{"code":{"type":"string","const":"validation_failed"},"message":{"type":"string"},"fields":{"type":"object","additionalProperties":{"type":"string"}}}}}}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"slot_taken — слот занят (возвращает свободные слоты на тот же день)","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false},"error":{"$ref":"#/components/schemas/ErrorBody"},"availableSlots":{"type":"array","items":{"$ref":"#/components/schemas/Slot"}}}}}}},"422":{"description":"invalid_rental_range | resource_not_active | closed_date","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"rate_limited — превышен лимит (5 / 10 мин на пару IP+ресурс)","headers":{"Retry-After":{"description":"Секунд до сброса лимита","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"$ref":"#/components/responses/ServerError"}},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Safe retries for reservation creation (Stripe-style). Send a unique key (e.g. a UUID) and retry the same request after a network timeout: within 24 hours the original successful response is replayed and no duplicate reservation is created. Reusing the same key with a different request body returns 409 idempotency_key_reuse. Error responses are never cached. Applies only to requests authenticated with a Bearer API key (bk_live_); the key is scoped to that API key."}]},"get":{"summary":"Список броней (интегратор)","description":"Возвращает брони тенанта, владеющего API-ключом. Требует Bearer + scope reservations:read.","security":[{"bearerAuth":[]}],"parameters":[{"name":"resource_id","in":"query","schema":{"type":"string","format":"uuid"},"description":"Фильтр по ресурсу"},{"name":"from","in":"query","schema":{"type":"string","format":"date-time"},"description":"Брони, пересекающие интервал с этой даты"},{"name":"to","in":"query","schema":{"type":"string","format":"date-time"},"description":"… по эту дату"},{"name":"status","in":"query","schema":{"type":"string"},"description":"Фильтр по статусу (pending/confirmed/cancelled)"},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}}],"responses":{"200":{"description":"Список броней","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"items":{"type":"array","items":{"$ref":"#/components/schemas/PublicReservation"}}}}}}},"401":{"description":"Нужен API-ключ","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Ключу не хватает scope reservations:read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/reservations/{id}":{"get":{"tags":["Reservations"],"summary":"Статус брони по ID","description":"Статус брони по её UUID. Анонимно (без ключа) — статус-поля без гостевого PII. Со связкой `Authorization: Bearer bk_live_...` (scope `reservations:read`) И при совпадении тенанта ключа с тенантом брони — расширенный вид с гостевыми полями. Поле `metadata` не отдаётся никогда. Резолв ограничен публично-видимыми ресурсами (`listing_visible`).","security":[{},{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"UUID брони.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Статус брони (анонимный или расширенный вид)","content":{"application/json":{"schema":{"type":"object","required":["ok","reservation"],"properties":{"ok":{"type":"boolean","const":true},"reservation":{"$ref":"#/components/schemas/PublicReservation"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"description":"invalid_api_key — передан ключ `bk_live_...`, но он невалиден/отозван/истёк","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"scope_forbidden — у ключа нет scope `reservations:read`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"summary":"Отмена брони (интегратор)","description":"Переводит бронь в статус cancelled. Требует Bearer + scope reservations:write; тенант ключа должен владеть бронью. Идемпотентно. Триггерит исходящий вебхук reservation.cancelled.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Бронь отменена","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"reservation":{"$ref":"#/components/schemas/PublicReservation"}}}}}},"400":{"description":"Некорректный UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Нужен API-ключ","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Ключу не хватает scope reservations:write","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Бронь не найдена (или чужой тенант)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/resource-photos/{photoId}/file":{"get":{"tags":["Media"],"summary":"Файл фотографии ресурса","description":"Бинарный поток фото (через MinIO). Доступно только если фото принадлежит публично-видимому активному ресурсу. Кэшируется на 3600 секунд.","parameters":[{"name":"photoId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Бинарный файл изображения","headers":{"Cache-Control":{"schema":{"type":"string","example":"public, max-age=3600"}}},"content":{"image/*":{"schema":{"type":"string","format":"binary"}}}},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"},"502":{"description":"Ошибка чтения потока хранилища"},"503":{"$ref":"#/components/responses/Unavailable"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"API-ключ интегратора `bk_live_<48hex>` (F85.W2a). Server-side только — НЕ использовать из браузера (ключ виден в исходнике). Скоупы проверяются per-endpoint."}},"parameters":{"Slug":{"name":"slug","in":"path","required":true,"description":"Публичный slug тенанта.","schema":{"type":"string"}},"ResourceId":{"name":"resourceId","in":"path","required":true,"description":"UUID ресурса.","schema":{"type":"string","format":"uuid"}}},"schemas":{"Branding":{"type":["object","null"],"description":"Объект брендинга тенанта (логотип, цвета, тексты) либо null если не настроен.","additionalProperties":true},"PublicResource":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"publicSlug":{"type":["string","null"]},"name":{"type":"string"},"type":{"type":"string"},"adapterType":{"type":"string"},"groupId":{"type":["string","null"],"format":"uuid"},"groupName":{"type":["string","null"]},"groupColor":{"type":["string","null"]},"groupIcon":{"type":["string","null"]},"firstPhotoUrl":{"type":["string","null"],"description":"Относительный путь `/api/public/v1/resource-photos/{photoId}/file`."},"extensions":{"type":"object","additionalProperties":true},"metadata":{"type":"object","additionalProperties":true}}},"Slot":{"type":"object","properties":{"startAt":{"type":"string","format":"date-time"},"endAt":{"type":"string","format":"date-time"}}},"GuestFields":{"type":"object","properties":{"guestName":{"type":"string","minLength":2,"maxLength":200},"guestPhone":{"type":"string","pattern":"^\\+?\\d{6,15}$"},"guestEmail":{"type":"string","format":"email"},"notes":{"type":"string","maxLength":1000},"locale":{"type":"string","enum":["ru","en","kk"],"default":"ru"}}},"SlotReservationRequest":{"allOf":[{"$ref":"#/components/schemas/GuestFields"},{"type":"object","required":["resourceId","startAt","endAt","guestName","guestPhone"],"properties":{"resourceId":{"type":"string","format":"uuid"},"startAt":{"type":"string","format":"date-time"},"endAt":{"type":"string","format":"date-time","description":"Должен быть позже startAt."}}}],"description":"Бронь на услугу в конкретное время (режим определяется отсутствием checkInDate)."},"RentalReservationRequest":{"allOf":[{"$ref":"#/components/schemas/GuestFields"},{"type":"object","required":["resourceId","checkInDate","checkOutDate","guestName","guestPhone"],"properties":{"resourceId":{"type":"string","format":"uuid"},"checkInDate":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"checkOutDate":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Должен быть позже checkInDate. Время заезда/выезда сервер берёт из сценария ресурса."}}}],"description":"Посуточная аренда (режим определяется наличием checkInDate)."},"PublicService":{"type":"object","description":"Услуга ресурса с резолвленной ценой (F85.W7).","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":["string","null"]},"durationMin":{"type":["integer","null"]},"role":{"type":"string","examples":["main","addon"]},"isDefault":{"type":"boolean"},"price":{"type":["object","null"],"description":"Цена услуги. null — цена не настроена. onRequest=true — «по запросу» (amountCents=null).","properties":{"amountCents":{"type":["integer","null"],"description":"Цена в копейках/тиынах."},"currency":{"type":["string","null"]},"label":{"type":["string","null"],"description":"Готовая денежная строка, напр. «50 000 ₸»."},"onRequest":{"type":"boolean"}}}}},"PublicReservation":{"type":"object","description":"Статус брони (F85.W7). Гостевые поля (guestName/guestPhone/guestEmail/notes/source) присутствуют ТОЛЬКО при аутентификации ключом своего тенанта со scope reservations:read.","properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string","examples":["pending","confirmed","cancelled"]},"resourceId":{"type":"string","format":"uuid"},"startAt":{"type":["string","null"],"format":"date-time"},"endAt":{"type":["string","null"],"format":"date-time"},"periodType":{"type":["string","null"]},"amount":{"type":["string","null"],"description":"Сумма брони (NUMERIC как строка) либо null."},"currency":{"type":["string","null"]},"locale":{"type":["string","null"]},"createdAt":{"type":["string","null"],"format":"date-time"},"guestName":{"type":["string","null"],"description":"Только расширенный вид (ключ своего тенанта)."},"guestPhone":{"type":["string","null"],"description":"Только расширенный вид."},"guestEmail":{"type":["string","null"],"description":"Только расширенный вид."},"notes":{"type":["string","null"],"description":"Только расширенный вид."},"source":{"type":["string","null"],"description":"Только расширенный вид."}}},"ErrorBody":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}}},"Error":{"type":"object","required":["ok","error"],"properties":{"ok":{"type":"boolean","const":false},"error":{"$ref":"#/components/schemas/ErrorBody"}}}},"responses":{"BadRequest":{"description":"Некорректный запрос","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"Не найдено (tenant_not_found / resource_not_found / photo_not_found)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unavailable":{"description":"Сервис временно недоступен (store / storage не готов)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"ServerError":{"description":"Внутренняя ошибка сервера","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}