На главную
REST API · v1

API ElixTime

Читайте расписание, клиентов и услуги салона, создавайте и отменяйте записи из своей системы. Тот же движок, что за виджетом записи, — значит и те же правила: занятый слот не займётся дважды, уведомления уйдут, лимиты тарифа сработают.

Базовый адресhttps://api.elixtime.ru/v1/api

Начало работы

  1. Откройте Настройки → Интеграции → Ключи API в админке салона.
  2. Выпустите ключ и сразу скопируйте его — второй раз он не покажется.
  3. Проверьте связь запросом /ping.
curl
curl https://api.elixtime.ru/v1/api/ping \
  -H "X-Api-Key: elx_ваш_ключ"

Ключи выпускает только владелец салона. Управляющие и сотрудники этот раздел не видят.

Аутентификация

Каждый запрос несёт заголовок X-Api-Key. Ключ принадлежит салону, а не человеку: увольнение администратора не ломает интеграцию.

Заголовок
X-Api-Key: elx_cyAidJRFkZbRD840b1reIGsPZyiIzkKTiTwaYLj5

На сервере хранится только SHA-256 хеш ключа — восстановить его невозможно ни нам, ни вам. Потеряли — выпустите новый и отзовите старый.

Права

ОбластьЧто разрешено
readВсе методы GET
read_writeПлюс создание и отмена записей

Права «на всё» не бывает намеренно: интеграции с аналитикой незачем уметь отменять визиты.

Форматы и единицы

Списки

Каждый список отвечает одинаковым конвертом, чтобы не гадать про пагинацию:

JSON
{ "items": [ … ], "total": 128, "limit": 50, "offset": 0 }

Время

Везде ISO 8601 со смещением. Записи отдаются в часовом поясе салона — его отдаёт /ping. На вход принимается любое корректное ISO-время, включая Z.

Деньги

Здесь легко ошибиться, поэтому прямо: price и full_price — в рублях, а paid_amount_kopecks и refunded_amount_kopecks — в копейках. price — сколько платит клиент, с учётом промокода и баллов, — одинаково в API и во всех вебхуках о записи и оплате. full_price — полная цена визита. Они различаются у визита по абонементу и подарка по штампам: клиент за них не платит (price = 0), а полная цена остаётся.

Выгрузка в CSV

Любой список отдаётся файлом, если добавить format=csv. Те же поля, что в JSON, только плоские — вложенные значения вроде остатков по точкам в файл не попадают.

curl
curl "https://api.elixtime.ru/v1/api/clients?format=csv&limit=1000" \
  -H "X-Api-Key: elx_ваш_ключ" -o clients.csv

Файл приходит в UTF-8 с BOM — Excel в Windows откроет кириллицу без бубна. Поддерживают выгрузку: /clients, /bookings,/products, /stock-movements, /reviews,/gift-certificates, /abonements.

Для полной выгрузки идите страницами по offset: ответ всегда содержит total, а limit ограничен 200–500 в зависимости от метода.

Ошибки

Тело ошибки всегда одной формы. Ветвитесь по code — он не меняется, в отличие от текста.

JSON
{ "error": { "code": "revoked_api_key", "message": "Ключ отозван." } }
HTTPcodeКогда
401missing_api_keyЗаголовок X-Api-Key не передан
401malformed_api_keyКлюч не начинается с elx_
401invalid_api_keyТакого ключа не существует
401revoked_api_keyКлюч отозван в настройках салона
403read_only_keyКлючу с правом read запрещена запись
404booking_not_foundЗаписи нет — или она принадлежит другому салону
409booking_completedЗавершённый визит нельзя отменить
422client_contact_requiredНе передан ни client_phone, ни client_email

Методы

GET/pingread

Проверка ключа

Самый дешёвый запрос: подтверждает, что ключ рабочий, и сообщает, какому салону он принадлежит. Удобно вызывать сразу после того, как пользователь вставил ключ в вашу форму.

Ответ
{
  "ok": true,
  "business": {
    "id": "62312670-1ddf-46c1-89d8-98377d3e77cf",
    "name": "Барбершоп Сезон",
    "slug": "barbershop-sezon",
    "timezone": "Europe/Moscow"
  },
  "scopes": "read"
}
GET/servicesread

Услуги

Прайс салона: длительность в минутах, цена в рублях, категория.

ПараметрОписание
limitдо 500, по умолчанию 100
offsetсмещение для постраничного обхода
Ответ
{
  "items": [
    {
      "id": "1e2bd6a5-…",
      "name": "Стрижка",
      "category": "Услуги",
      "duration_min": 60,
      "price": 1200,
      "active": true
    }
  ],
  "total": 1, "limit": 100, "offset": 0
}
GET/employeesread

Мастера

Сотрудники салона. Неактивные не удаляются, а помечаются active: false — записи за ними сохраняются.

ПараметрОписание
limitдо 500
offset—
Ответ
{
  "items": [
    { "id": "66d53ed4-…", "name": "Алексей", "role": null, "phone": null, "active": true }
  ],
  "total": 1, "limit": 100, "offset": 0
}
GET/locationsread

Точки

Адреса салона. Отдаются целиком, без пагинации — их всегда немного.

Ответ
{
  "items": [
    { "id": "72b0c9c4-…", "name": "Центр", "address": "Краснодар, ул. Красная, 1" }
  ],
  "total": 1, "limit": 1, "offset": 0
}
GET/clientsread

Клиенты

База клиентов салона. Поиск идёт одновременно по имени, телефону и почте.

ПараметрОписание
searchподстрока в имени, телефоне или email
formatjson (по умолчанию) или csv
limitдо 200, по умолчанию 50
offset—
Ответ
{
  "items": [
    {
      "id": "f041b05b-…",
      "name": "Мария Реброва",
      "phone": "+79182001406",
      "email": null,
      "created_at": "2026-07-09T15:19:10.361026+00:00"
    }
  ],
  "total": 1, "limit": 50, "offset": 0
}
GET/bookingsread

Записи

Записи за период. Без date_from и date_to отдаются ближайшие 30 дней — чтобы случайный запрос не выгреб всю историю салона.

ПараметрОписание
date_fromISO 8601, включительно
date_toISO 8601, исключительно
statuspending · confirmed · completed · cancelled · no_show
employee_idзаписи одного мастера
formatjson (по умолчанию) или csv
limitдо 200, по умолчанию 50
offset—
Ответ
{
  "items": [
    {
      "id": "9f1c…",
      "status": "confirmed",
      "start": "2026-08-20T10:00:00+03:00",
      "end": "2026-08-20T11:00:00+03:00",
      "employee_id": "66d53ed4-…",
      "service_id": "1e2bd6a5-…",
      "location_id": "72b0c9c4-…",
      "client_id": "f041b05b-…",
      "client_name": "Мария Реброва",
      "price": 1200,
      "full_price": 1200,
      "paid_amount_kopecks": 60000,
      "refunded_amount_kopecks": 0,
      "payment_status": "succeeded",
      "comment": null,
      "created_at": "2026-08-18T09:12:44.100Z"
    }
  ],
  "total": 1, "limit": 50, "offset": 0
}
GET/bookings/{id}read

Одна запись

Та же структура, что и в списке. Чужая запись отдаёт 404, а не 403 — существование чужих данных не подтверждается.

Ответ
{ "id": "9f1c…", "status": "confirmed", "start": "…", "…": "…" }
GET/productsread

Товары и остатки

Расходники со складскими остатками. Остаток лежит отдельной строкой на каждую пару товар-точка, поэтому здесь он уже собран в stock — второй запрос на каждый товар не нужен.

ПараметрОписание
location_idостатки одной точки
low_stocktrue — только то, что на минимуме или ниже
formatjson (по умолчанию) или csv
limitдо 500
offset—
Ответ
{
  "items": [
    {
      "id": "b41f…",
      "name": "Краска 6.0",
      "sku": "KR-60",
      "unit": "мл",
      "cost_per_unit": 4,
      "min_stock": 500,
      "active": true,
      "qty_total": 1250,
      "stock": [
        { "location_id": "72b0c9c4-…", "qty_left": 1250, "updated_at": "2026-08-19T18:04:11Z" }
      ]
    }
  ],
  "total": 1, "limit": 100, "offset": 0
}
GET/stock-movementsread

Движения склада

Приход, списание и корректировки. Без дат — последние 30 дней. qty_delta со знаком: минус — списание (в том числе автоматическое после визита, тогда заполнен booking_id), плюс — приход.

ПараметрОписание
date_from / date_toISO 8601
product_idпо одному товару
formatjson или csv
limitдо 500
offset—
Ответ
{
  "items": [
    {
      "id": "7c2a…",
      "product_id": "b41f…",
      "location_id": "72b0c9c4-…",
      "qty_delta": -60,
      "kind": "consumption",
      "booking_id": "9f1c…",
      "note": null,
      "created_at": "2026-08-19T11:32:07Z"
    }
  ],
  "total": 1, "limit": 100, "offset": 0
}
GET/reviewsread

Отзывы

Оценки и тексты отзывов вместе со ссылками на визит, мастера и услугу.

ПараметрОписание
statuspending · published · hidden
formatjson или csv
limitдо 200
offset—
Ответ
{
  "items": [
    {
      "id": "3ad9…",
      "rating": 5,
      "comment": "Всё понравилось",
      "author_name": "Мария",
      "status": "published",
      "booking_id": "9f1c…",
      "employee_id": "66d53ed4-…",
      "service_id": "1e2bd6a5-…",
      "created_at": "2026-08-18T14:20:01Z"
    }
  ],
  "total": 1, "limit": 50, "offset": 0
}
GET/gift-certificatesread

Сертификаты

Номиналы и остатки в рублях. Код отдаётся целиком — ключ уже подтверждает, что запрос от самого салона.

ПараметрОписание
statuspending (не оплачен) · active · used_up · expired · cancelled
formatjson или csv
limitдо 200
offset—
Ответ
{
  "items": [
    {
      "id": "cd10…",
      "code": "GIFT-4K7M2P",
      "status": "active",
      "initial_amount": 5000,
      "balance": 2600,
      "buyer_name": "Ольга",
      "recipient_name": "Мария",
      "expires_at": "2027-02-19T00:00:00Z",
      "created_at": "2026-08-19T09:00:00Z"
    }
  ],
  "total": 1, "limit": 50, "offset": 0
}
GET/abonementsread

Абонементы

Проданные абонементы клиентов: сколько посещений осталось и до какой даты.

ПараметрОписание
client_idпо одному клиенту
statusactive · exhausted (посещения кончились) · expired
formatjson или csv
limitдо 200
offset—
Ответ
{
  "items": [
    {
      "id": "aa31…",
      "client_id": "f041b05b-…",
      "template_id": "77c2…",
      "template_name": "5 маникюров",
      "uses_left": 3,
      "price_paid": 9000,
      "status": "active",
      "sold_at": "2026-07-30T12:00:00Z",
      "expires_at": "2026-10-28T00:00:00Z"
    }
  ],
  "total": 1, "limit": 50, "offset": 0
}
POST/bookingsread_write

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

Создаёт визит теми же правилами, что и виджет: занятый слот получает отказ на уровне ограничения в базе, действуют лимиты тарифа, уходят уведомления клиенту и мастеру. Клиент находится по телефону или почте, а если такого ещё нет — заводится.

ПараметрОписание
employee_idобязательно
service_idобязательно
startISO 8601, обязательно
client_nameобязательно
client_phone / client_emailнужен хотя бы один
location_idесли у салона несколько точек
commentдо 1000 символов
Ответ
// 201 Created — тело как у GET /bookings/{id}
{ "id": "…", "status": "pending", "start": "2026-08-21T12:00:00+03:00", "…": "…" }
POST/bookings/{id}/cancelread_write

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

Переводит визит в cancelled теми же правилами, что и отмена в админке: визит возвращается на абонемент, номинал — на сертификат, списанные баллы — клиенту, оплата возвращается через платёжный шлюз (по Робокассе визит помечается «вернуть вручную»), клиент получает уведомление, очередь ожидания — предложение занять слот. Повторный вызов безопасен — уже отменённая запись просто возвращается как есть. Завершённый визит отменить нельзя.

ПараметрОписание
reasonнеобязательно, дописывается в комментарий
Ответ
{ "id": "9f1c…", "status": "cancelled", "…": "…" }

Пример: занять слот из своей формы

curl
curl -X POST https://api.elixtime.ru/v1/api/bookings \
  -H "X-Api-Key: elx_ваш_ключ" \
  -H "Content-Type: application/json" \
  -d '{
    "employee_id": "66d53ed4-…",
    "service_id":  "1e2bd6a5-…",
    "start":       "2026-08-21T12:00:00+03:00",
    "client_name": "Мария Реброва",
    "client_phone": "+79182001406"
  }'

Если мастер в это время занят, запрос вернёт ошибку — проверка стоит ограничением в базе данных, а не в коде, поэтому две одновременные попытки занять один слот не пройдут обе.

Вебхуки

Обратное направление: ElixTime сам постучится к вам, когда что-то произошло. Настраиваются там же, в Настройки → Интеграции, отдельно от ключей — у каждого вебхука свой секрет.

СобытиеКогда приходит
booking.createdКлиент или администратор создал запись
booking.confirmedЗапись подтверждена
booking.cancelledЗапись отменена
booking.completedВизит завершён
booking.rescheduledИзменилось время или мастер
payment.succeededЮKassa подтвердила оплату
tip.receivedПолучены чаевые

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

Headers
X-ElixTime-Event:     booking.created
X-ElixTime-Signature: 9f2c…  (HMAC-SHA256 от тела, hex)
X-ElixTime-Timestamp: 2026-08-20T09:14:03.918Z
X-ElixTime-Delivery:  4be1…  (id доставки, для дедупликации)
X-ElixTime-Attempt:   1

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

Считайте HMAC-SHA256 от сырого тела запроса секретом вебхука и сравните с заголовком. Сравнивайте константным по времени сравнением, а не обычным ==.

Python
import hmac, hashlib

def verify(raw_body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)
Node.js
const crypto = require('crypto');

function verify(rawBody, signature, secret) {
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

Доставка идёт с повторами и бэк-оффом, поэтому одно событие может прийти дважды. Дедуплицируйте по X-ElixTime-Delivery и отвечайте 2xx как можно быстрее — обработку выносите в очередь.

Ограничения

  • До 10 активных ключей на салон. Отозванные не считаются.
  • limit: до 500 для справочников, до 200 для клиентов и записей.
  • Публичный API входит в тариф Pro и выше.
  • Версия в адресе: /v1/. Ломающие изменения выйдут как /v2/, старая версия продолжит работать.

Не хватает метода — напишите, что вы строите: support@elxcorp.com.