Обзор
Base URL: https://loyella.ru. Все методы — под префиксом /v1.
- Запросы и ответы — JSON (
Content-Type: application/json), кодировка UTF-8. - Успешный ответ, как правило, обёрнут в
{ "data": … }. Единственное исключение — выпуск карты (POST /v1/passes): плоский объект безdata(совместимость с 1С Fitness). - Неизвестный путь под
/v1возвращает JSON404(а не HTML). - Версионирование: новые поля и методы выходят без смены версии; ломающие изменения — только в новой версии (
/v2).
Эта страница — человекочитаемая версия того же API. Есть два машиночитаемых формата: OpenAPI 3.0 (.yaml) — для импорта в Postman/Insomnia и генераторов клиентов; и API Blueprint (.apib) — для Apiary/Stoplight.
Аутентификация
Каждый запрос подписывается парой ключей организации в заголовках:
X-Client-Id: client-<slug>-xxxxxxxx X-Client-Secret: <секрет>
- Ключи выпускаются в кабинете: раздел «Интеграции» (тариф Pro). Секрет показывается один раз — сохраните его; в базе хранится только его bcrypt-хэш.
- Любой отказ авторизации (нет заголовков / неизвестный
client_id/ неверный секрет / организация приостановлена) — единый ответ403. - Данные жёстко изолированы по организации: чужой серийный номер отдаёт
404(существование чужой карты не раскрывается).
{ "error": { "code": 403, "message": "Forbidden" } }
Идемпотентность
Повтор мутации (POST/PATCH/PUT/DELETE) с тем же ключом не выполняет операцию второй раз, а возвращает сохранённый первый ответ дословно (с заголовком Idempotent-Replay: true). Это защищает от двойных списаний при ретраях по таймауту или 5xx.
Как задаётся ключ
- Общий случай — заголовок
Idempotency-Key(принимается иIdempotence-Key). - Денежные пути —
/points/credit,/points/debit,/redeem,/stamps/set— ключом служит обязательное поле телаoperation_id, и оно приоритетнее заголовка (стабильный бизнес-ключ операции; заголовок некоторые клиенты регенерируют на каждый ретрай — это задвоило бы деньги).
Поведение повтора
- Тот же ключ + то же тело → сохранённый ответ, без побочных эффектов.
- Тот же ключ + другое тело →
409(коллизия «ключ ↔ тело»). - Запрос с этим ключом ещё обрабатывается →
409(«повторите позже»). - Ключ хранится ~24 часа.
404 Pass not found или 422 Недостаточно средств). Если первая попытка вернула исправимую 4xx (пополнили баланс, исправили серийный номер) — повторяйте её с новым operation_id / Idempotency-Key, иначе получите закэшированную ошибку, пока не истечёт срок хранения ключа. Ответы 5xx не кэшируются и корректно переисполняются.
Лимиты запросов
По умолчанию — 600 запросов в минуту на организацию (щедрый порог, защита прода от лавины, а не тарифный лимит). На каждом ответе — заголовки чернового стандарта IETF:
| Заголовок | Значение |
|---|---|
RateLimit-Limit | потолок запросов за окно |
RateLimit-Remaining | сколько запросов осталось |
RateLimit-Reset | секунд до сброса окна |
При превышении — 429 + заголовок Retry-After:
{ "error": { "code": 429, "message": "Слишком много запросов — попробуйте позже." } }
Формат ошибок
Единый конверт ошибки: { "error": { "code", "message" } }. Поле code, как правило, дублирует HTTP-статус; у части унаследованных методов 1С может присутствовать только message. Отдельные валидации несут дополнительные поля (field, balance, target).
| Статус | Когда |
|---|---|
400 | некорректное тело (нет url у вебхука; ключ идемпотентности > 255 символов) |
402 | недостаточно средств на балансе организации для выпуска карты |
403 | ошибка аутентификации / организация приостановлена |
404 | карта / вид / вебхук не найдены (или принадлежат другой организации) |
409 | конфликт идемпотентности (тот же ключ с другим телом / запрос ещё в обработке) |
422 | бизнес-ошибка (недостаточно средств на карте, штампы не настроены, некорректный параметр) |
429 | превышен лимит запросов |
500 | внутренняя ошибка (безопасно ретраить — не кэшируется идемпотентностью) |
Карты
Выпуск, чтение, обновление и удаление карт. project_id — внешний номер вида карты в вашей организации (тот, что вы задаёте в 1С), а не внутренний id.
Полное состояние карты. Резолвится сначала по serial_number, затем по pass_number. Чужой/несуществующий → 404.
curl https://loyella.ru/v1/passes/a1b2c3d4-...-001 \ -H "X-Client-Id: $ID" -H "X-Client-Secret: $SECRET"
{
"data": {
"serial_number": "a1b2c3d4-...-001",
"pass_number": "CARD-000123",
"project_id": 1,
"status": "active",
"voided": false,
"created_via": "api",
"fields": { "first_name": "Иван", "bonuses": "150", "phone": "+79990001122" },
"point_id": 4,
"registrations": 1,
"created_at": "2026-07-01T10:00:00.000Z",
"updated_at": "2026-07-10T12:30:00.000Z",
"link": "https://loyella.ru/pass/a1b2c3d4-...-001"
}
}
Список и поиск карт. Два режима:
- Точечный поиск по одному идентификатору:
?serial_number=|?pass_number=|?phone=(телефон нормализуется). Вернёт 0..1 карт. - Фильтруемый список:
?query=(ФИО/номер),?status=active|voided(или?voided=true|false),?project_id=,?updated_since=<iso>,?limit=(по умолчанию 50, максимум 100),?offset=.
curl "https://loyella.ru/v1/passes?query=Петров&status=active&limit=20" \ -H "X-Client-Id: $ID" -H "X-Client-Secret: $SECRET"
{
"data": {
"items": [
{ "serial_number": "a1b2c3d4-...-001", "pass_number": "CARD-000123",
"project_id": 1, "status": "active", "voided": false,
"first_name": "Иван", "last_name": "Петров",
"fields": { "first_name": "Иван", "bonuses": "150" },
"created_at": "...", "updated_at": "...", "link": "https://loyella.ru/pass/..." }
],
"total": 1, "limit": 20, "offset": 0
}
}
Выпустить карту. project_id — внешний номер вида (по умолчанию 1). Выпуск списывает годовую ставку с баланса; при нехватке средств → 402, карта не создаётся. created_via: "test" — бесплатный тестовый выпуск. Ответ плоский, без data (совместимость с 1С).
curl -X POST https://loyella.ru/v1/passes \
-H "X-Client-Id: $ID" -H "X-Client-Secret: $SECRET" \
-H "Content-Type: application/json" \
-d '{"project_id":1,"fields":{"first_name":"Иван","phone":"+79990001122"}}'
{
"serial_number": "a1b2c3d4-...-001",
"pass_number": "CARD-000123",
"link": "https://loyella.ru/pass/a1b2c3d4-...-001"
}
Обновить одну карту (поверхностный мерж полей). {"voided": true} аннулирует карту. После обновления карта пушится на устройства и отправляется вебхук.
curl -X PATCH https://loyella.ru/v1/passes/a1b2c3d4-...-001 \
-H "X-Client-Id: $ID" -H "X-Client-Secret: $SECRET" \
-H "Content-Type: application/json" \
-d '{"phone":"+79995556677","bonuses":"200"}'
{ "data": { "serial_number": "a1b2c3d4-...-001", "pass_number": "CARD-000123",
"status": "updated", "success": true, "fields": { "phone": "+79995556677", "bonuses": "200" } } }
Пакетное обновление. Принимает массив [{serial_number, fields}] (или один объект). Ответ — карта результатов по серийным номерам. Основной метод 1С Fitness.
curl -X PATCH https://loyella.ru/v1/passes \
-H "X-Client-Id: $ID" -H "X-Client-Secret: $SECRET" \
-H "Content-Type: application/json" \
-d '[{"serial_number":"a1b2...001","fields":{"bonuses":"300"}}]'
{ "data": { "a1b2...001": { "serial_number": "a1b2...001", "pass_number": "CARD-000123",
"status": "updated", "success": true } } }
Записать одни и те же поля списку карт: {serial_numbers:[…], fields:{…}}. Ответ — карта результатов по серийным.
curl -X PATCH https://loyella.ru/v1/passes/by-fields \
-H "X-Client-Id: $ID" -H "X-Client-Secret: $SECRET" \
-H "Content-Type: application/json" \
-d '{"serial_numbers":["a1b2...001","a1b2...002"],"fields":{"tier":"gold"}}'
Удалить карту. Чужой/несуществующий серийный → 404. Для «мягкого» аннулирования используйте PATCH с {"voided": true}.
curl -X DELETE https://loyella.ru/v1/passes/a1b2c3d4-...-001 \ -H "X-Client-Id: $ID" -H "X-Client-Secret: $SECRET"
{ "data": { "serial_number": "a1b2c3d4-...-001", "status": "deleted", "success": true } }
Журнал событий карты (выпуск, установка, штампы, баллы, рассылки и т.д.). Пагинация limit (до 100, по умолчанию 50) / offset, фильтр type. Внутренние поля meta (id кассира, id устройства и пр.) наружу не отдаются — только безопасное человекочитаемое подмножество в details.
curl "https://loyella.ru/v1/passes/a1b2...001/events?type=points_earn&limit=20" \ -H "X-Client-Id: $ID" -H "X-Client-Secret: $SECRET"
{ "data": {
"items": [
{ "id": 812, "type": "points_earn", "source": "api", "created_at": "2026-07-13T14:05:00Z",
"details": { "field": "bonuses", "delta": 150, "before": 150, "after": 300, "operation_id": "sale-4417" } },
{ "id": 640, "type": "issue", "source": "api", "created_at": "2026-07-10T09:00:00Z",
"details": { "channel": "api" } }
],
"total": 2, "limit": 20, "offset": 0 } }
Штампы
Штамп-карты (собери N штампов — получи награду). Штампы настраиваются на виде карты; если выключены — методы отвечают 422.
Поставить один штамп (+1) с логикой награды: при достижении цели выдаётся награда, счётчик сбрасывается в 0. Кулдауна нет — для защиты от двойного штампа при ретрае передавайте Idempotency-Key.
curl -X POST https://loyella.ru/v1/passes/a1b2...001/stamp \ -H "X-Client-Id: $ID" -H "X-Client-Secret: $SECRET" \ -H "Idempotency-Key: stamp-2026-07-13-17-42"
{ "data": { "serial_number": "a1b2...001", "pass_number": "CARD-000123",
"stamps": { "current": 4, "target": 6 }, "rewarded": false, "success": true } }
Установить абсолютное значение счётчика count (0..target) — для импорта/коррекции. Награда не триггерится. operation_id обязателен.
curl -X POST https://loyella.ru/v1/passes/a1b2...001/stamps/set \
-H "X-Client-Id: $ID" -H "X-Client-Secret: $SECRET" \
-H "Content-Type: application/json" \
-d '{"count":3,"operation_id":"import-2026-07-13-001"}'
{ "data": { "serial_number": "a1b2...001", "pass_number": "CARD-000123",
"stamps": { "current": 3, "target": 6 }, "operation_id": "import-2026-07-13-001", "success": true } }
Баллы и бонусы
Начисление и списание баллов с журналом операций и идемпотентностью. Поле по умолчанию — bonuses; можно указать другое (латиница/цифры/_, 1..64 символа). operation_id обязателен — ключ идемпотентности проводки.
Начислить баллы. Дополнительно принимает comment и source_doc (попадают в журнал).
curl -X POST https://loyella.ru/v1/passes/a1b2...001/points/credit \
-H "X-Client-Id: $ID" -H "X-Client-Secret: $SECRET" \
-H "Content-Type: application/json" \
-d '{"amount":150,"field":"bonuses","operation_id":"sale-4417","source_doc":"Чек №4417"}'
{ "data": { "serial_number": "a1b2...001", "pass_number": "CARD-000123", "field": "bonuses",
"before": 150, "after": 300, "delta": 150, "operation_id": "sale-4417", "success": true } }
Списать баллы (та же механика, знак минус). Уход остатка ниже нуля запрещён — 422 с текущим остатком в поле balance.
curl -X POST https://loyella.ru/v1/passes/a1b2...001/points/debit \
-H "X-Client-Id: $ID" -H "X-Client-Secret: $SECRET" \
-H "Content-Type: application/json" \
-d '{"amount":50,"operation_id":"redeem-4418"}'
{ "error": { "code": 422, "message": "Недостаточно средств на балансе", "field": "bonuses", "balance": 40 } }
Погашение подарочных
Частичное списание с номинала подарочной карты/депозита (поле по умолчанию — balance). Уход в минус запрещён (422). void_at_zero: true + остаток 0 → карта аннулируется в той же транзакции. operation_id обязателен.
curl -X POST https://loyella.ru/v1/passes/GIFT-000042/redeem \
-H "X-Client-Id: $ID" -H "X-Client-Secret: $SECRET" \
-H "Content-Type: application/json" \
-d '{"amount":300,"field":"balance","operation_id":"sale-9001","void_at_zero":true}'
{ "data": { "serial_number": "GIFT-000042", "pass_number": "GIFT-000042", "field": "balance",
"before": 300, "after": 0, "redeemed": 300, "operation_id": "sale-9001", "voided": true, "success": true } }
Рассылки
Push-рассылка держателям карт (текст появляется на экране блокировки). Для точечных сообщений одному клиенту используйте обновление полей карты (PATCH /v1/passes/{serial}) — рассылки предназначены для массовых отправок и ограничены 5 в сутки на организацию.
Запустить рассылку. operation_id обязателен (ключ идемпотентности — повтор с тем же ключом не отправит второй раз). target: "all" (все активные карты), {"project_id": N} (карты вида), {"serial": "…"} или {"serials": ["…"]}. Ответ 202 — рассылка поставлена в очередь; 409 — предыдущая ещё идёт; 429 — исчерпан дневной лимит.
curl -X POST https://loyella.ru/v1/broadcasts \
-H "X-Client-Id: $ID" -H "X-Client-Secret: $SECRET" \
-H "Content-Type: application/json" \
-d '{"message":"Акция выходного дня!","target":{"project_id":1},"operation_id":"promo-2026-07-13"}'
{ "data": { "id": 5501, "queued": 128, "status": "sending" } }
Статус рассылки по её id (из ответа выше). Пока это последняя рассылка и она идёт — status: "sending" с прогрессом sent; иначе completed.
curl https://loyella.ru/v1/broadcasts/5501 -H "X-Client-Id: $ID" -H "X-Client-Secret: $SECRET"
{ "data": { "id": 5501, "message": "Акция выходного дня!", "target": "project:1",
"queued": 128, "sent": 128, "status": "completed", "created_at": "2026-07-13T14:05:00Z" } }
Аналитика
Компактная сводка организации (только чтение) — для BI и учётных систем.
Карты (всего/активные/аннулированные), установки в Apple Wallet, выпуск за сегодня и 30 дней, распределение активных карт по каналам (api — 1С, admin — кабинет, form — анкета).
curl https://loyella.ru/v1/stats -H "X-Client-Id: $ID" -H "X-Client-Secret: $SECRET"
{ "data": {
"cards": { "total": 1240, "active": 1180, "voided": 60 },
"installs": { "apple": 902 },
"issued": { "today": 8, "last_30_days": 210,
"by_channel": [ { "channel": "api", "count": 800 }, { "channel": "admin", "count": 200 },
{ "channel": "form", "count": 180 } ] } } }
Реестр полей
Известные поля организации (латинский ключ → русская подпись), чтобы CRM/1С и Loyella одинаково понимали смысл каждого ключа.
Объявленный реестр плюс фактические ключи из карт, ещё не заведённые в реестр (in_registry: false). Плоские срезы keys[] / labels{} — для простой интеграции.
curl https://loyella.ru/v1/fields -H "X-Client-Id: $ID" -H "X-Client-Secret: $SECRET"
{ "data": {
"fields": [ { "key": "first_name", "label": "Имя", "in_registry": true },
{ "key": "tier", "label": "Tier", "in_registry": false } ],
"keys": ["first_name", "tier"],
"labels": { "first_name": "Имя", "tier": "Tier" } } }
Аддитивный upsert подписей: новый ключ — создаётся, существующий — переименовывается. Ничего не удаляется. Принимает {fields:[{key,label}]} или {labels:{ключ:подпись}}. Любая невалидная запись → 422, реестр не меняется.
curl -X PUT https://loyella.ru/v1/fields \
-H "X-Client-Id: $ID" -H "X-Client-Secret: $SECRET" \
-H "Content-Type: application/json" \
-d '{"labels":{"tier":"Уровень","visits":"Визитов"}}'
{ "data": { "created": 1, "updated": 1,
"fields": [ { "key": "tier", "label": "Уровень" }, { "key": "visits", "label": "Визитов" } ] } }
Справочники
Метаданные вида карты, нумераторы и данные сертификата — для совместимости с 1С Fitness.
Список видов карт организации: внешний номер, название, продукт, настройка штампов и счётчик активных карт. Только чтение (создание/переименование видов через API пока не поддерживается).
curl https://loyella.ru/v1/projects -H "X-Client-Id: $ID" -H "X-Client-Secret: $SECRET"
{ "data": { "items": [
{ "external_id": 1, "name": "Карта лояльности", "product": "loyalty",
"stamps": { "enabled": true, "target": 6 }, "active_cards": 1180 } ], "total": 1 } }
{id} — внешний номер вида. Возвращает данные вида + описания полей (attribute/label/required/type).
curl https://loyella.ru/v1/projects/1 -H "X-Client-Id: $ID" -H "X-Client-Secret: $SECRET"
Список нумераторов организации.
curl https://loyella.ru/v1/numerators -H "X-Client-Id: $ID" -H "X-Client-Secret: $SECRET"
Создать нумератор: {name, prefix, start, format}.
curl -X POST https://loyella.ru/v1/numerators \
-H "X-Client-Id: $ID" -H "X-Client-Secret: $SECRET" \
-H "Content-Type: application/json" \
-d '{"name":"VIP","prefix":"VIP-","start":1000}'
{ "data": { "created": true, "id": 2 } }
Идентификаторы сертификата (без секретов) — для совместимости с протоколом 1С Fitness.
curl https://loyella.ru/v1/certificates -H "X-Client-Id: $ID" -H "X-Client-Secret: $SECRET"
{ "data": [ { "pass_type_identifier": "pass.ru.loyella", "team_identifier": "XXXXXXXXXX",
"organization_name": "Моя организация" } ] }
Вебхуки
Подписка на события Loyella. Тело события POST-ится на ваш URL с HMAC-подписью; доставка надёжная (ретраи с backoff, журнал доставок, авто-отключение при постоянных провалах).
Конверт события
{
"event": "points.earned",
"id": "evt_2b7c9f1e-...",
"created_at": "2026-07-13T14:05:00.000Z",
"tenant": "my-org-slug",
"data": { "serial_number": "a1b2...001", "field": "bonuses", "delta": 150, "before": 150, "after": 300 }
}
Заголовки доставки и подпись HMAC
| Заголовок | Значение |
|---|---|
X-Loyella-Event | имя события, напр. points.earned |
X-Loyella-Delivery | уникальный id доставки — дедуплицируйте ретраи на своей стороне |
X-Loyella-Timestamp | unix-время (секунды) формирования подписи |
X-Loyella-Attempt | номер попытки (1..6) |
X-Loyella-Signature | sha256= + HMAC-SHA256 по строке timestamp + "." + rawBody |
Подпись считается по сырому телу запроса (байт-в-байт), склеенному с временной меткой через точку. Секрет (whsec_…) выдаётся один раз при создании подписки. Проверка на Node.js:
const crypto = require('crypto');
// rawBody — СЫРАЯ строка тела запроса (до JSON.parse):
// app.use('/webhooks/loyella', express.raw({ type: 'application/json' }), handler)
function verifyLoyellaSignature(req, rawBody, secret) {
const ts = req.headers['x-loyella-timestamp'];
const got = req.headers['x-loyella-signature'] || '';
const expected =
'sha256=' + crypto.createHmac('sha256', secret).update(ts + '.' + rawBody).digest('hex');
const a = Buffer.from(got), b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Доставка, ретраи, авто-отключение
- Успех — ответ вашего сервера с кодом
2xx. Таймаут запроса — 10 секунд. - До 6 попыток. После неудачной попытки следующая через:
30s → 2m → 10m → 1h → 6h. - Если подписка накопила 15 подряд «мёртвых» доставок — она автоматически отключается (
active: false). Проверьте приёмник и включите её снова (PATCH /v1/webhooks/{id}с{"active": true}). - Отвечайте
2xxбыстро и обрабатывайте асинхронно — медленный приёмник упирается в таймаут.
Каталог событий (18)
Подписаться можно на конкретное событие, на группу (card.*, points.*) или на всё (*).
| Событие | Группа | Когда |
|---|---|---|
card.issued | Карты | карта выпущена (любой канал) |
card.installed | Карты | карта установлена в кошелёк |
card.uninstalled | Карты | карта удалена из кошелька |
card.fields_updated | Карты | поля карты изменены |
card.voided | Карты | карта аннулирована |
stamp.added | Лояльность | поставлен штамп |
stamp.rewarded | Лояльность | штампы собраны, выдана награда |
points.earned | Лояльность | баллы начислены |
points.redeemed | Лояльность | баллы списаны |
scan.operation | Лояльность | операция сканера (визит/сторно) |
review.created | Клиенты | оставлен отзыв (с оценкой) |
form.submitted | Клиенты | заполнена анкета самозаписи |
broadcast.sent | Кампании | отправлена push-рассылка |
publish.applied | Кампании | дизайн применён |
automation.triggered | Кампании | сработала автоматизация |
push.delivered | Служебные | push доставлен |
push.failed | Служебные | push не доставлен |
telegram.linked | Служебные | привязан Telegram |
push.* — шумные: они стреляют на каждое пуш-уведомление (в том числе при каждом обновлении карты и на каждую рассылку). Подписывайтесь на них только если реально ведёте контроль доставки push.
Управление подписками
Создать подписку. Новый контур (рекомендуется): передайте events[] — вернётся secret (HMAC, показывается один раз). global: false + projects: [external_id…] ограничивает подписку конкретными видами карт.
curl -X POST https://loyella.ru/v1/webhooks \
-H "X-Client-Id: $ID" -H "X-Client-Secret: $SECRET" \
-H "Content-Type: application/json" \
-d '{"name":"CRM sync","url":"https://example.com/hooks/loyella","events":["card.*","points.earned"]}'
{ "data": { "id": 7, "secret": "whsec_0123abcd...", "events": ["card.*", "points.earned"] } }
Список подписок (без секретов).
Обновить подписку: {url?, events?, active?, name?}. Обновление events не меняет секрет.
Удалить подписку → { "data": { "deleted": true } }.
Немедленно отправить test.ping на URL подписки и вернуть ответ приёмника.
{ "data": { "ok": true, "status_code": 200, "error": null } }
Журнал последних доставок подписки (?limit=, по умолчанию 20, максимум 100).
Машиночитаемый справочник подписываемых событий (name/label/group).
Совместимость с 1С Fitness
Loyella — прямая замена сервиса карт для 1С Fitness. Прежние эндпоинты работают без изменений — байт-в-байт: тот же формат тела, те же ответы, тот же JSON-404 на неизвестный путь.
Подключение существующей интеграции
- Смените АдресAPI на
https://loyella.ru. - Пропишите ключи
X-Client-Id/X-Client-Secret, выданные в кабинете Loyella (раздел «Интеграции»). - Больше ничего менять не нужно.
Совместимы: выпуск карты (плоский ответ), пакетное обновление, обновление и аннулирование одной карты, удаление, метаданные вида, нумераторы, сертификат. «Старые» вебхуки (одиночный event: PassCreated / PassUpdated / PassRegistered / PassUnregistered) приходят прежним телом {event, data} без подписи. Новые dot-события — opt-in, они не ломают старую интеграцию.
Заголовки = Новый Соответствие;
Заголовки.Вставить("X-Client-Id", КлиентID);
Заголовки.Вставить("X-Client-Secret", Секрет);
Заголовки.Вставить("Content-Type", "application/json");
Запрос = Новый HTTPЗапрос("/v1/passes/" + Серийный + "/points/credit", Заголовки);
Запрос.УстановитьТелоИзСтроки("{""amount"":150,""operation_id"":""sale-4417""}");
Соединение = Новый HTTPСоединение("loyella.ru", 443, , , , , Новый ЗащищенноеСоединениеOpenSSL);
Ответ = Соединение.ОтправитьДляОбработки(Запрос);