BrasnarHub · за разработчици

BrasnarHub API v1

Базов адрес: https://brasnarhub.com/api/v1 · Формат: JSON (UTF-8) · Часова зона: Europe/Sofia · Суми: цели евроцентове (2550 = €25.50).

Машинно четима спецификация: OpenAPI 3 (openapi.yaml).

Автентикация

Клиенти получават токен от POST /auth/token и го пращат като Authorization: Bearer <token>.

Салони създават API ключове в панела → „API ключове“ (план ENTERPRISE). Всеки ключ има права:

Крайни точки

Публични (без токен)

МетодАдресОписание
GET/api/v1/salonsСписък с публикувани салони. Филтри: city, district, category, q, per_page, page.
GET/api/v1/salons/{slug}Салон с активните услуги и бръснари.
GET/api/v1/salons/{slug}/availabilityСвободни часове за ден: service_ids[] (задължително), barber (slug или id), date (ГГГГ-ММ-ДД).
GET/api/v1/barbers/free-now„Свободни СЕГА“ — бръснари със свободен час в следващите 2 часа. city, limit (1–20).

Клиенти (Bearer токен на потребител)

МетодАдресОписание
POST/api/v1/auth/tokenВход: email, password, device_name → token. Ограничено до 5 опита/мин.
DELETE/api/v1/auth/tokenАнулира текущия токен.
POST/api/v1/bookingsНова резервация: salon, service_ids[], barber, starts_at (ISO 8601), name, phone, email, notes. ?src=own за собствен линк на салона.
GET/api/v1/me/bookingsМоите резервации: status, upcoming=1, per_page, page.
POST/api/v1/bookings/{code}/cancelОтказ от резервация (reason). Спазва срока за отказ на салона.

Салони (API ключ на салон, план ENTERPRISE)

МетодАдресОписание
GET/api/v1/salon/bookingsРезервации за период: from, to (ГГГГ-ММ-ДД, до 92 дни), status, barber. Право: bookings:read.
POST/api/v1/salon/bookingsРезервация от салона (източник „по телефона“): service_ids[], barber, starts_at, client{name, phone, email, notes, salon_client_id}. Право: bookings:write.
PATCH/api/v1/salon/bookings/{code}action: complete | no_show | cancel | reschedule (+ starts_at, reason). Право: bookings:write.
GET/api/v1/salon/clientsКлиентска база: q, per_page, page. Право: clients:read.

Пример

curl -X POST https://brasnarhub.com/api/v1/bookings \
  -H "Authorization: Bearer $TOKEN" -H "Accept: application/json" -H "Content-Type: application/json" \
  -d '{"salon":"barbershop-sofia","service_ids":[12],"barber":"ivan-petrov","starts_at":"2026-09-18T10:30:00+03:00","phone":"0888123456"}'

Странициране

Списъците връщат data, links и meta (current_page, last_page, per_page, total). Параметри: page, per_page (до 100).

Грешки

Всички грешки имат един и същ формат. Съобщенията са на български и могат да се покажат на потребителя.

{
  "error": {
    "code": "booking_failed",
    "message": "Този час току-що беше зает. Избери друг свободен час.",
    "details": {}
  }
}
HTTPcodeКога
401unauthenticatedЛипсващ, невалиден или изтекъл токен / API ключ.
403plan_required, insufficient_abilityПланът на салона не включва API или ключът няма нужното право.
404not_foundСалон, бръснар, услуга или резервация не са намерени.
422validation_failed, booking_failed, invalid_credentials…Невалидни данни или нарушено правило (напр. зает час).
429too_many_requestsПревишен лимит. Виж хедъра Retry-After.

Лимити

Публични: 60 заявки/мин на IP · Вход: 5/мин · Клиенти: 120/мин · Салонни ключове: 300/мин.

Уебхукове

Салонът добавя URL адреси в „API ключове“ → „Уебхукове“. При събитие изпращаме POST с JSON тяло. Неуспешните доставки (не-2xx или таймаут) се повтарят след 1, 5, 15, 60 и 240 минути; всеки опит се записва в дневника.

Хедъри: X-BrasnarHub-Event, X-BrasnarHub-Delivery (uuid), X-BrasnarHub-Event-Id и Idempotency-Key (еднакви за всички адреси и повторни опити на едно събитие — използвай ги за дедупликация; също event_id в тялото), X-BrasnarHub-Timestamp, X-BrasnarHub-Signature: sha256=<hex>.

Подписът е HMAC-SHA256 на "{timestamp}.{сурово тяло}" с тайния ключ на адреса. Проверка (PHP):

$expected = 'sha256=' . hash_hmac('sha256', $_SERVER['HTTP_X_BRASNARHUB_TIMESTAMP'] . '.' . file_get_contents('php://input'), $secret);
if (! hash_equals($expected, $_SERVER['HTTP_X_BRASNARHUB_SIGNATURE']) || abs(time() - (int) $_SERVER['HTTP_X_BRASNARHUB_TIMESTAMP']) > 300) {
    http_response_code(400); exit;
}