Откройте Интеграции → API в личном кабинете. Скопируйте ключ в защищённое хранилище секретов.
REST API и вебхуки
Подключите CRM, бота или внутренний сервис к расписанию Sayly. Здесь собран полный путь: от первого запроса до безопасной обработки событий.
Быстрый старт
API использует обычный HTTPS и JSON. Подойдёт любой HTTP-клиент.
Передавайте Bearer-ключ и Accept: application/json в каждом запросе. Для POST и PATCH добавьте Content-Type: application/json.
Выполните 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.
- Один аккаунт имеет один активный ключ; ротация отзывает предыдущий.
- При компрометации сразу перегенерируйте ключ в кабинете.
Профиль
Проверьте владельца ключа и базовые настройки локали.
/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. Слоты уже учитывают расписание, занятые встречи, перерывы, правила записи и календарные конфликты.
/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 }
}
/services/{service_id}
Получить одну услугу
Возвращает актуальные настройки услуги. Чужой или отсутствующий идентификатор даст 404.
curl "{{base_url}}/services/18" \
--header "Authorization: Bearer $SAYLY_API_KEY" \
--header "Accept: application/json"
/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 повторно проверяет доступность выбранного слота.
/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"
/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" }
}
}
/bookings/{booking_id}
Получить запись
Возвращает запись по UUID. Идентификаторы другого аккаунта не раскрываются и дают 404.
/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 минут"}'
/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"}'
/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-DeliveryUUID конкретной доставки.
X-Sayly-TimestampUnix timestamp формирования подписи.
X-Sayly-SignatureHMAC-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
$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-статус.
Ключ отсутствует, имеет неверный формат, отозван или перегенерирован.
Для доступа к REST API требуется активный платный тариф. Код ошибки: subscription_required.
Ресурс не существует или принадлежит другому аккаунту.
Слот занят, запись уже нельзя отменить или перенести. Получите слоты повторно.
Поля запроса не прошли проверку. Детали находятся в объекте errors.
Превышен лимит запросов. Учитывайте Retry-After и применяйте exponential backoff.
{
"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.