Sayly для разработчиков

REST API и вебхуки

Подключите CRM, бота или внутренний сервис к расписанию Sayly. Здесь собран полный путь: от первого запроса до безопасной обработки событий.

Стабильная версия v1 JSON-запросы и ответы Только HTTPS
Начните за несколько минут

Быстрый старт

API использует обычный HTTPS и JSON. Подойдёт любой HTTP-клиент.

1
Создайте ключ

Откройте Интеграции → API в личном кабинете. Скопируйте ключ в защищённое хранилище секретов.

2
Добавьте заголовки

Передавайте Bearer-ключ и Accept: application/json в каждом запросе. Для POST и PATCH добавьте Content-Type: application/json.

3
Проверьте профиль

Выполните GET /me. Ответ 200 подтверждает, что ключ активен и принадлежит нужному аккаунту.

Пример запроса
curl --request GET "{{base_url}}/me" \
  --header "Authorization: Bearer $SAYLY_API_KEY" \
  --header "Accept: application/json"
Доступ

Авторизация

Ключ даёт доступ только к данным владельца, который выпустил его. После ротации старый ключ перестаёт работать немедленно.

Authorization: Bearer sayly_live_…
  • Отправляйте ключ только в заголовке Authorization.
  • Все запросы выполняйте по HTTPS.
  • Один аккаунт имеет один активный ключ; ротация отзывает предыдущий.
  • При компрометации сразу перегенерируйте ключ в кабинете.
Аккаунт

Профиль

Проверьте владельца ключа и базовые настройки локали.

GET /me

Получить профиль владельца

Возвращает идентификатор, имя, email, локаль, часовой пояс и публичную ссылку записи.

Пример ответа
{
  "data": {
    "id": 42,
    "name": "Анна Смирнова",
    "email": "anna@example.com",
    "locale": "ru",
    "time_zone": "Asia/Yekaterinburg",
    "public_booking_url": "https://sayly.ru/anna"
  }
}
Каталог и доступность

Услуги и свободные слоты

Услуга в API соответствует типу встречи в Sayly. Слоты уже учитывают расписание, занятые встречи, перерывы, правила записи и календарные конфликты.

GET /services

Получить список услуг

Возвращает активные услуги постранично. Архивные услуги можно включить отдельным параметром.

Query-параметры

ПолеТипОбязательноОписание
include_archivedboolean Нет

Добавить архивные услуги. По умолчанию false.

per_pageinteger Нет

Размер страницы от 1 до 100. По умолчанию 25.

Пример запроса
curl "{{base_url}}/services?per_page=25" \
  --header "Authorization: Bearer $SAYLY_API_KEY" \
  --header "Accept: application/json"
Пример ответа
{
  "data": [
    {
      "id": 18,
      "name": "Консультация",
      "slug": "consultation",
      "description": "Онлайн-встреча по вашему вопросу",
      "duration_minutes": 60,
      "time_zone": "Asia/Yekaterinburg",
      "is_archived": false,
      "event_type": "individual",
      "location": { "type": "zoom", "label": "Zoom" }
    }
  ],
  "meta": { "current_page": 1, "last_page": 1, "per_page": 25, "total": 1 }
}
GET /services/{service_id}

Получить одну услугу

Возвращает актуальные настройки услуги. Чужой или отсутствующий идентификатор даст 404.

Пример запроса
curl "{{base_url}}/services/18" \
  --header "Authorization: Bearer $SAYLY_API_KEY" \
  --header "Accept: application/json"
GET /services/{service_id}/slots

Получить свободные слоты

Возвращает доступные интервалы для выбранной календарной даты в часовом поясе клиента.

Query-параметры

ПолеТипОбязательноОписание
datedate Да

Дата клиента в формате YYYY-MM-DD.

time_zoneIANA timezone Да

Например Europe/Moscow или Asia/Yekaterinburg.

Пример запроса
curl "{{base_url}}/services/18/slots?date=2026-08-14&time_zone=Europe%2FMoscow" \
  --header "Authorization: Bearer $SAYLY_API_KEY" \
  --header "Accept: application/json"
Пример ответа
{
  "data": [
    {
      "starts_at": "2026-08-14T10:00:00.000+03:00",
      "ends_at": "2026-08-14T11:00:00.000+03:00"
    }
  ]
}
Встречи

Записи

Читайте, создавайте, обновляйте, переносите и отменяйте записи. При создании и переносе Sayly повторно проверяет доступность выбранного слота.

GET /bookings

Получить список записей

Возвращает записи владельца ключа от новых к старым.

Query-параметры

ПолеТипОбязательноОписание
statusstring Нет

pending, confirmed, cancelled, completed или spam.

service_idinteger Нет

Фильтр по услуге.

date_fromISO 8601 Нет

Начало диапазона.

date_toISO 8601 Нет

Конец диапазона.

per_pageinteger Нет

От 1 до 100.

Пример запроса
curl "{{base_url}}/bookings?status=confirmed&per_page=50" \
  --header "Authorization: Bearer $SAYLY_API_KEY" \
  --header "Accept: application/json"
POST /bookings

Создать запись

Создаёт подтверждённую запись только в свободном слоте. При гонке за один слот один запрос получит 201, остальные — 409.

Тело запроса

ПолеТипОбязательноОписание
service_idinteger Да

Идентификатор услуги.

starts_atISO 8601 Да

Начало слота из ответа /slots.

time_zoneIANA timezone Да

Часовой пояс клиента.

namestring Да

Имя клиента, до 120 символов.

emailemail Нет

Email. Обязателен email или phone.

phonestring Нет

Телефон. Обязателен email или phone.

commentstring Нет

Комментарий до 2000 символов.

client_locationstring Нет

Место встречи для услуги со свободным адресом.

Пример запроса
curl --request POST "{{base_url}}/bookings" \
  --header "Authorization: Bearer $SAYLY_API_KEY" \
  --header "Accept: application/json" \
  --header "Content-Type: application/json" \
  --data '{
    "service_id": 18,
    "starts_at": "2026-08-14T10:00:00+03:00",
    "time_zone": "Europe/Moscow",
    "name": "Иван Петров",
    "email": "ivan@example.com",
    "comment": "Первичная консультация"
  }'
Пример ответа
{
  "data": {
    "id": "8e33531a-7c95-45b9-9cdf-3c82a74c3f53",
    "status": "confirmed",
    "service": { "id": 18, "name": "Консультация", "slug": "consultation" },
    "client": { "name": "Иван Петров", "email": "ivan@example.com", "phone": null },
    "starts_at": "2026-08-14T07:00:00.000000Z",
    "ends_at": "2026-08-14T08:00:00.000000Z",
    "time_zone": "Europe/Moscow",
    "source": { "key": "rest_api", "label": "REST API" }
  }
}
GET /bookings/{booking_id}

Получить запись

Возвращает запись по UUID. Идентификаторы другого аккаунта не раскрываются и дают 404.

PATCH /bookings/{booking_id}

Обновить данные клиента

Частично обновляет имя, email, телефон или комментарий. Для времени используйте отдельный метод переноса.

Тело запроса

ПолеТипОбязательноОписание
namestring Нет

Новое имя клиента.

emailemail|null Нет

Новый email.

phonestring|null Нет

Новый телефон.

commentstring|null Нет

Новый комментарий.

Пример запроса
curl --request PATCH "{{base_url}}/bookings/8e33531a-7c95-45b9-9cdf-3c82a74c3f53" \
  --header "Authorization: Bearer $SAYLY_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{"comment":"Клиент просит позвонить за 5 минут"}'
POST /bookings/{booking_id}/reschedule

Перенести запись

Переносит будущую индивидуальную запись в свободный слот и синхронизирует подключённый календарь.

Тело запроса

ПолеТипОбязательноОписание
starts_atISO 8601 Да

Новое начало из ответа /slots.

time_zoneIANA timezone Да

Часовой пояс, в котором выбран слот.

Пример запроса
curl --request POST "{{base_url}}/bookings/8e33531a-7c95-45b9-9cdf-3c82a74c3f53/reschedule" \
  --header "Authorization: Bearer $SAYLY_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{"starts_at":"2026-08-15T14:00:00+03:00","time_zone":"Europe/Moscow"}'
DELETE /bookings/{booking_id}

Отменить запись

Отменяет будущую подтверждённую запись. Успешный ответ не содержит тела и имеет статус 204.

Пример запроса
curl --request DELETE "{{base_url}}/bookings/8e33531a-7c95-45b9-9cdf-3c82a74c3f53" \
  --header "Authorization: Bearer $SAYLY_API_KEY" \
  --header "Accept: application/json"
События в реальном времени

Вебхуки

Sayly отправляет POST с JSON сразу после события. Endpoint должен ответить любым статусом 2xx. Каждое событие имеет стабильный id для идемпотентной обработки.

Доступные события

booking.created

Создана новая подтверждённая запись.

booking.updated

Изменился статус записи.

booking.cancelled

Запись отменена.

booking.rescheduled

Изменились дата или время.

booking.reminder.sent

Отправлено плановое напоминание.

Заголовки доставки

X-Sayly-Event

Тип события, например booking.created.

X-Sayly-Delivery

UUID конкретной доставки.

X-Sayly-Timestamp

Unix timestamp формирования подписи.

X-Sayly-Signature

HMAC-SHA256 в формате v1=<hex>.

Idempotency-Key

Идентификатор события; одинаков на всех повторах.

Пример события
{
  "id": "71194329-c9aa-4df6-a197-41de565b98d3",
  "type": "booking.created",
  "created_at": "2026-08-10T08:15:42.000000Z",
  "data": {
    "booking": {
      "id": "8e33531a-7c95-45b9-9cdf-3c82a74c3f53",
      "status": "confirmed",
      "service": { "id": 18, "name": "Консультация", "slug": "consultation" },
      "client": { "name": "Иван Петров", "email": "ivan@example.com", "phone": null },
      "starts_at": "2026-08-14T07:00:00.000000Z",
      "ends_at": "2026-08-14T08:00:00.000000Z",
      "time_zone": "Europe/Moscow"
    }
  }
}

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

Подписывается точная строка timestamp + "." + raw_body. Проверяйте подпись до JSON-декодирования и используйте constant-time comparison.

PHP
<?php
$timestamp = $_SERVER['HTTP_X_SAYLY_TIMESTAMP'] ?? '';
$received = $_SERVER['HTTP_X_SAYLY_SIGNATURE'] ?? '';
$rawBody = file_get_contents('php://input');

// Дополнительно отклоняйте timestamp старше 5 минут.
if (abs(time() - (int) $timestamp) > 300) {
    http_response_code(401);
    exit;
}

$expected = 'v1='.hash_hmac(
    'sha256',
    $timestamp.'.'.$rawBody,
    $_ENV['SAYLY_WEBHOOK_SECRET']
);

if (! hash_equals($expected, $received)) {
    http_response_code(401);
    exit;
}

$event = json_decode($rawBody, true, flags: JSON_THROW_ON_ERROR);
// Сохраните $event['id'] и не обрабатывайте его повторно.
http_response_code(204);
Предсказуемые ответы

Ошибки и лимиты

Ошибки валидации возвращаются как JSON. Никогда не определяйте результат только по тексту — проверяйте HTTP-статус.

401
Unauthorized

Ключ отсутствует, имеет неверный формат, отозван или перегенерирован.

402
Payment Required

Для доступа к REST API требуется активный платный тариф. Код ошибки: subscription_required.

404
Not Found

Ресурс не существует или принадлежит другому аккаунту.

409
Booking Conflict

Слот занят, запись уже нельзя отменить или перенести. Получите слоты повторно.

422
Validation Error

Поля запроса не прошли проверку. Детали находятся в объекте errors.

429
Too Many Requests

Превышен лимит запросов. Учитывайте Retry-After и применяйте exponential backoff.

HTTP 422
{
  "message": "The starts at field must be a date after now.",
  "errors": {
    "starts_at": ["The starts at field must be a date after now."]
  }
}

Частота запросов ограничивается отдельно для каждого API-ключа. При превышении лимита API возвращает статус 429 и заголовок Retry-After.

Перед запуском

Checklist безопасности

Ключ хранится в защищённом хранилище секретов и никогда не попадает в логи.
Все запросы используют HTTPS, а интеграция проверяет TLS-сертификат.
Вебхук проверяет HMAC по исходному body до JSON-декодирования.
Timestamp вебхука не старше 5 минут, а event id защищает от повторной обработки.
Ответ 2xx возвращается только после надёжного сохранения события.
При утечке ключ и секрет вебхука ротируются немедленно.
Интеграция сохраняет и журналирует только минимально необходимые персональные данные клиентов.