Restoza API и вебхуки

API позволяет вашим системам (CRM, касса, сайт) читать брони и столы, создавать и менять брони. Вебхуки — обратное направление: Restoza сама присылает событие на ваш адрес, когда бронь создана или изменена. Ключи и адреса вебхуков создаются администратором заведения в «Настройках» → «Интеграции».

Авторизация

Передавайте ключ в заголовке Authorization: Bearer <ключ>. Ключ привязан к одному заведению и показывается один раз при создании. Ключ «только чтение» не может создавать и менять брони.

curl https://restoza.ru/api/v1/tables \
  -H "Authorization: Bearer rz_xxxxxxxxxxxxxxxx"

Лимит — 60 запросов в минуту на ключ; при превышении — ответ 429 с заголовком Retry-After. Если доступ заведения к платформе ограничен — 403 access_blocked.

Ошибки

{ "error": { "code": "conflict", "message": "Один из выбранных столов уже занят на это время" } }

Объект брони

{
  "id": 123,
  "status": "CONFIRMED",          // PENDING | CONFIRMED | REJECTED | WAITLIST
  "guestName": "Иван",
  "phone": "+7 999 000-00-00",
  "email": null,                  // необязательный; если указан — гость получает письма о статусе
  "guestsCount": 4,
  "comment": "У окна",           // пожелание гостя
  "adminNote": null,              // внутренняя заметка персонала
  "startAt": "2026-10-10T15:00:00.000Z",
  "endAt": "2026-10-10T17:00:00.000Z",
  "arrivedAt": null,              // время отметки «гость пришёл»
  "noShowAt": null,               // время отметки «гость не пришёл» (неявка); не бывает одновременно с arrivedAt
  "depositPaid": false,
  "source": "WEB",                // откуда заявка: WEB | TELEGRAM | MAX | ADMIN (создана в кабинете) | API
  "cancelledByGuestAt": null,     // время отмены гостем в мини-приложении (статус при этом REJECTED)
  "preferredHall": null,          // зал, выбранный гостем в форме: { "id": 2, "name": "Терраса" } или null
  "createdAt": "2026-10-02T10:00:00.000Z",
  "updatedAt": "2026-10-02T10:00:00.000Z",
  "tables": [{ "id": 1, "label": "1", "number": 1, "zone": "BAR", "capacityMin": 1, "capacityMax": 2 }],
  "services": [                   // услуги брони (подтягиваются по столу или добавлены вручную); суммы — в рублях
    { "id": 1, "name": "Депозит", "kind": "DEPOSIT", "priceType": "FIXED", "unitPrice": 5000, "auto": true, "paidAt": null }
  ],
  "guestTags": ["аллергия на орехи"],
  "guestNotes": null
}

Время — в UTC (ISO 8601). Статусы: PENDING — новая заявка, CONFIRMED — подтверждена, со столом, WAITLIST — лист ожидания (без стола), REJECTED — отклонена/отменена.

Методы

GET/bookings

Список броней. Параметры: from, to (по времени начала, ISO 8601), status (через запятую), updatedSince (для синхронизации — только изменённые после момента), limit (1–500, по умолчанию 100), cursor. Ответ: { "data": [...], "nextCursor": 456 } — пока nextCursor не null, передавайте его в cursor для следующей страницы.
curl "https://restoza.ru/api/v1/bookings?from=2026-10-10T00:00:00Z&to=2026-10-11T00:00:00Z&status=CONFIRMED" \
  -H "Authorization: Bearer rz_xxx"

GET/bookings/{id}

Одна бронь.

POST/bookings

Создать бронь (нужен ключ с записью). Поля: startAt, durationHours (1–6), guestName, phone, guestsCount — обязательные; tableIds, email, comment, depositPaid, arrived — по желанию. Со столами бронь сразу CONFIRMED, без столов — WAITLIST. Время должно укладываться в часы работы заведения. Если указан email, гость получит письмо (подтверждение или «заявка получена»), пока письма не отключены в настройках заведения.
curl -X POST https://restoza.ru/api/v1/bookings \
  -H "Authorization: Bearer rz_xxx" -H "Content-Type: application/json" \
  -d '{"startAt":"2026-10-10T15:00:00Z","durationHours":2,"guestName":"Иван",
       "phone":"+79990000000","guestsCount":4,"tableIds":[1]}'

PATCH/bookings/{id}

Изменить бронь (нужен ключ с записью). Любые из полей: status, tableIds (полная замена списка столов), startAt, endAt, guestName, phone, email (пустая строка или null — очистить), guestsCount, comment, adminNote, arrived (true/false), noShow (true/false; только для подтверждённой брони после её начала), depositPaid. Отмена брони — {"status":"REJECTED"}.

GET/tables

Столы заведения с залом и вместимостью.

GET/services

Услуги заведения с ценами (kind: DEPOSIT, ENTRY или EXTRA; priceType: FIXED, PER_GUEST или PER_HOUR; цена в рублях) и столами, к которым они привязаны.

GET/availability

Свободные и занятые столы на интервал: start (ISO 8601) и durationHours. Ответ: { "data": { "tables": [{ "id": 1, "status": "free" }] } }.

Вебхуки

События:

Если при изменении поменялся статус, вы получите оба события: booking.updated и booking.status_changed. В «Настройках» → «Интеграции» можно подписаться только на нужные.

POST /ваш-адрес
Content-Type: application/json
X-Restoza-Event: booking.created
X-Restoza-Delivery: 981
X-Restoza-Timestamp: 1790000000
X-Restoza-Signature: sha256=3b1f…

{
  "id": "evt_9f2c1d7a5b3e8c4d6a1f0e2b",
  "event": "booking.created",
  "createdAt": "2026-10-02T10:00:00.000Z",
  "data": { "booking": { …объект брони… } }
}

Отвечайте любым 2xx в течение 10 секунд. Поле id события уникально — используйте его, чтобы не обработать одно событие дважды (при повторах оно то же самое).

Проверка подписи

Подпись — HMAC-SHA256 от строки <timestamp>.<тело запроса> с вашим секретом (он виден в «Настройках» → «Интеграции»). Сравнивайте в постоянное время и отбрасывайте запросы с устаревшим timestamp (например, старше 5 минут) — так старый перехваченный запрос нельзя отправить повторно. Подписывайте и проверяйте именно сырое тело, до разбора JSON.

Node.js:

const crypto = require("crypto");

function verify(secret, rawBody, headers) {
  const ts = headers["x-restoza-timestamp"];
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const expected = "sha256=" +
    crypto.createHmac("sha256", secret).update(ts + "." + rawBody).digest("hex");
  const given = headers["x-restoza-signature"] || "";
  return given.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}

Python:

import hmac, hashlib, time

def verify(secret: str, raw_body: bytes, headers) -> bool:
    ts = headers.get("X-Restoza-Timestamp", "")
    if abs(time.time() - int(ts)) > 300:
        return False
    digest = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest("sha256=" + digest, headers.get("X-Restoza-Signature", ""))

PHP:

function verify(string $secret, string $rawBody, array $headers): bool {
    $ts = $headers['X-Restoza-Timestamp'] ?? '';
    if (abs(time() - (int)$ts) > 300) return false;
    $expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $rawBody, $secret);
    return hash_equals($expected, $headers['X-Restoza-Signature'] ?? '');
}

Повторы и отключение

Если ваш сервер не ответил 2xx, отправка повторяется: через 1 минуту, 5 минут, 30 минут, 2 часа и 12 часов (всего до 6 попыток). После этого доставка помечается неудачной — её можно отправить вручную из журнала. Если подряд накопится много неудач, адрес отключается автоматически (исправьте приёмник и включите его снова). Адреса во внутренней сети (localhost, 10.x, 192.168.x и т. п.) не допускаются; редиректы не выполняются.