Перейти к содержимому

Документация API

REST API для касс, сайтов и мобильных приложений. Всё в JSON, аутентификация по ключу организации.

Базовый адрес

https://loyal.rc9.ru/api/v1

На своём сервере — адрес вашей установки.

Скачать openapi.json

Ключи и разрешения

Ключ создаётся в панели организации, в разделе «Интеграции → API-ключи», и показывается один раз. Передавайте его в заголовке Authorization:

GET /api/v1/clients/79001234567/balance
Authorization: Bearer <api-key>
Accept: application/json

Ключ привязан к организации: через него видны только её клиенты и заказы. Отключённый или просроченный ключ получает 401. У каждого ключа свой набор разрешений — ключу кассы не обязательно уметь проводить возвраты. Нет нужного разрешения — 403.

РазрешениеЧто открывает
write:clients POST /clients, PATCH /clients/{phone}
read:balance GET /clients/{phone}/balance, /loyalty, /pass
read:transactions GET /clients/{phone}/transactions
calculate:bonuses POST /orders/calculate
write:transactions POST /orders/process
write:returns POST /orders/return
write:events POST /events, GET /events/types

Повторы запросов

Сеть кассы рвётся, и запрос приходится повторять. Чтобы повтор не провёл заказ дважды, POST /orders/process и POST /orders/return требуют заголовок X-Idempotency-Key — уникальный UUID на каждую операцию.

  • Повтор, пока первый запрос ещё обрабатывается, — 409.
  • Повтор после успеха — тот же ответ с 200, бонусы заново не начисляются.
  • После внутренней ошибки 500 ключ освобождается, запрос можно повторить.
  • Без заголовка — 422.

Номер заказа и номер возврата тоже уникальны в организации: повтор номера — 409 с идентификатором уже проведённой операции.

POST /events заголовка не принимает: уникальность держится по смыслу — один бонус за регистрацию на клиента, один за отзыв на товар. Повтор и исчерпанный лимит — это 200 с awarded: 0 и причиной в reason, а не ошибка.

Ошибки

Ошибка приходит объектом с полем error. Некоторые ответы добавляют подробности — например, отказ в списании бонусов сообщает, сколько списать можно:

HTTP/1.1 422 Unprocessable Entity

{
  "error": "Списание бонусов невозможно по правилам организации",
  "redeem_requested": 3000,
  "max_redeemable": 2505
}

Сценарий кассы

До оплаты касса спрашивает, сколько бонусов можно списать и сколько начислится. После оплаты — проводит заказ с ключом идемпотентности.

POST /api/v1/orders/calculate
Authorization: Bearer <api-key>
Content-Type: application/json

{ "phone": "79001234567", "total": 10000, "redeem": 500,
  "items": [{ "id": 1024, "name": "Кофе", "price": 5000, "qty": 2 }] }


POST /api/v1/orders/process
Authorization: Bearer <api-key>
X-Idempotency-Key: 6f1e2c9a-6b3f-4a5b-9c1d-2e3f4a5b6c7d
Content-Type: application/json

{ "phone": "79001234567", "order_number": "POS-2026-000123",
  "total": 10000, "redeem": 500,
  "items": [{ "id": 1024, "name": "Кофе", "price": 5000, "qty": 2 }] }

На одном и том же теле запроса calculate и process считают одинаково: сколько обещали на экране кассы, столько и начислится.

Что важно знать

  • Телефон передавайте нормализованным — 11 цифр, начиная с 7. Разные записи одного номера создадут разных клиентов.
  • Позиции необязательны, но без items не сработают правила по товарам, категориям и свойствам. Правила по каталогу работают, когда в заказе передан catalog_id или catalog_code.
  • Отказ в списании отменяет весь заказ: ни заказа, ни начисления в базе не остаётся. Лимит заранее отдаёт POST /orders/calculate.
  • Начисленное может стать доступным не сразу, если организация задала задержку активации: такие бонусы приходят в pending, а в истории — со статусом pending.
  • Бонусы сгорают. Для блока «скоро сгорят» в личном кабинете баланс отдаёт expiring_next и expiring_within_30d.
  • К заказу применяется уровень, который был до него. Новый уровень действует со следующей покупки.
  • Возврат пересчитывается пропорционально, с округлением в пользу магазина. Если клиент уже потратил начисленное, недостача приходит в shortfall.

Клиенты

Поиск клиентов, баланс бонусов и история транзакций

POST /clients

Создать или найти клиента

Создаёт нового клиента или возвращает существующего. Если клиент уже привязан к организации — возвращает его данные. Поле name устанавливается только если имя ещё не задано.

Тело запроса application/json

ПолеТипОписание
phone* string

Номер телефона — уникальный идентификатор клиента

name string | null

Имя (задаётся только при первом создании)

Пример

curl -X POST https://loyal.rc9.ru/api/v1/clients \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"phone":"+79001234567","name":"Иван"}'

Ответы

201 Клиент создан или найден
ПолеТипОписание
phone string

Номер телефона клиента

anket_id integer

ID анкеты клиента в организации

balance integer

Баланс бонусов (целое число)

{
    "phone": "+79001234567",
    "anket_id": 42,
    "balance": 150
}
GET /clients/{phone}/balance

Баланс бонусов клиента

Возвращает текущий баланс бонусных баллов клиента в данной организации. Требует разрешение read:balance.

Параметры

ИмяГдеТипОписание
phone* путь string

Номер телефона клиента (URL-encoded, напр. %2B79001234567)

Пример

curl -X GET https://loyal.rc9.ru/api/v1/clients/+79001234567/balance \
  -H "Authorization: Bearer $API_KEY"

Ответы

200 OK
ПолеТипОписание
phone string
anket_id integer
balance integer

Доступно к трате

available integer
pending integer

Начислено, но ещё не активировано

pending_next BonusBucket
pending_next.amount integer
pending_next.activates_at string (date-time) | null
pending_next.expires_at string (date-time) | null
expiring_next BonusBucket
expiring_next.amount integer
expiring_next.activates_at string (date-time) | null
expiring_next.expires_at string (date-time) | null
expiring_within_30d integer
{
    "phone": "string",
    "anket_id": 0,
    "balance": 0,
    "available": 0,
    "pending": 0,
    "pending_next": {
        "amount": 1200,
        "activates_at": "string",
        "expires_at": "string"
    },
    "expiring_next": {
        "amount": 1200,
        "activates_at": "string",
        "expires_at": "string"
    },
    "expiring_within_30d": 0
}
404 Клиент не найден или не привязан к организации

Объект ошибки: {"error": "…"}

GET /clients/{phone}/transactions

История транзакций

Пагинированный список бонусных транзакций клиента (20 на страницу, сортировка по убыванию даты). Требует разрешение read:transactions.

Параметры

ИмяГдеТипОписание
phone* путь string

Номер телефона клиента (URL-encoded)

page строка запроса integer

Номер страницы

Пример

curl -X GET https://loyal.rc9.ru/api/v1/clients/+79001234567/transactions \
  -H "Authorization: Bearer $API_KEY"

Ответы

200 OK
ПолеТипОписание
phone string
anket_id integer
data object[]
data[].id integer
data[].type string

deposit — начисление, withdraw — списание Значения: "deposit", "withdraw".

data[].amount integer

Сумма в бонусах

data[].description string | null
data[].created_at string (date-time)
data[].event string

Бизнес-тип операции Значения: "accrued", "redeemed", "redemption_refunded", "accrual_reverted", "expired", "event_awarded", "adjustment", "manual_deposit", "manual_withdraw".

data[].order_number string | null
data[].order_id integer | null
data[].status string

pending — начисление ещё не активировано и в баланс не входит Значения: "completed", "pending".

data[].expires_at string (date-time) | null
meta object
meta.current_page integer
meta.last_page integer
meta.total integer
{
    "phone": "+79001234567",
    "anket_id": 42,
    "data": [
        {
            "id": 0,
            "type": "deposit",
            "amount": 0,
            "description": "string",
            "created_at": "string",
            "event": "accrued",
            "order_number": "string",
            "order_id": 0,
            "status": "completed",
            "expires_at": "string"
        }
    ],
    "meta": {
        "current_page": 0,
        "last_page": 0,
        "total": 0
    }
}
404 Клиент не найден

Объект ошибки: {"error": "…"}

GET /clients/{phone}/pass

Скачать карту Apple Wallet

Возвращает файл .pkpass с картой лояльности клиента.

Требует настроенного шаблона карты и сертификатов в разделе Бонусы → Карта Wallet. Разрешение: read:balance.

Параметры

ИмяГдеТипОписание
phone* путь string

Номер телефона клиента (URL-encoded, напр. %2B79001234567)

Пример

curl -X GET https://loyal.rc9.ru/api/v1/clients/+79001234567/pass \
  -H "Authorization: Bearer $API_KEY"

Ответы

200 Файл карты

Файл application/vnd.apple.pkpass

401 Недействительный или истёкший API-ключ
403 У ключа нет разрешения read:balance
404 Клиент не найден или не привязан к организации
500 Не удалось собрать карту (нет сертификатов или шаблона)
PATCH /clients/{phone}

Дописать анкету клиента

Заполняет имя, дату рождения и контакты. Правка даты рождения перезапускает отсчёт права на бонус ко дню рождения — иначе дату вписывали бы за три дня до праздника. Контакты наружу не отдаются, только признаки их наличия. Требует разрешение write:clients.

Параметры

ИмяГдеТипОписание
phone* путь string

Номер телефона клиента (URL-encoded)

Тело запроса application/json

ПолеТипОписание
name string
last_name string
middle_name string
birth_date string (date)

Правка перезапускает отсчёт права на бонус ко дню рождения

email string (email)
telegram_chat_id string

Пример

curl -X PATCH https://loyal.rc9.ru/api/v1/clients/+79001234567 \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"string","last_name":"string","middle_name":"string","birth_date":"2026-01-01","email":"string","telegram_chat_id":"string"}'

Ответы

200 OK
ПолеТипОписание
phone string
anket_id integer
name string | null
last_name string | null
birth_date string (date) | null
has_email boolean
has_telegram boolean
{
    "phone": "string",
    "anket_id": 0,
    "name": "string",
    "last_name": "string",
    "birth_date": "2026-01-01",
    "has_email": true,
    "has_telegram": true
}
404 Клиент не найден

Объект ошибки: {"error": "…"}

422 Ошибка валидации

Объект ошибки: {"error": "…"}

GET /clients/{phone}/loyalty

Уровень клиента и прогресс

Текущий уровень, следующий и сколько осталось купить до него. tier равен null, если уровни у организации не настроены; next_tier — если клиент на вершине. Требует разрешение read:balance.

Параметры

ИмяГдеТипОписание
phone* путь string

Номер телефона клиента (URL-encoded)

Пример

curl -X GET https://loyal.rc9.ru/api/v1/clients/+79001234567/loyalty \
  -H "Authorization: Bearer $API_KEY"

Ответы

200 OK
ПолеТипОписание
phone string
anket_id integer
tier TierRef
tier.code string
tier.name string
tier.threshold number
next_tier TierRef
next_tier.code string
next_tier.name string
next_tier.threshold number
lifetime_total number
amount_to_next number
{
    "phone": "string",
    "anket_id": 0,
    "tier": {
        "code": "gold",
        "name": "MILLER'S 10%",
        "threshold": 150000
    },
    "next_tier": {
        "code": "gold",
        "name": "MILLER'S 10%",
        "threshold": 150000
    },
    "lifetime_total": 162350,
    "amount_to_next": 12350
}
404 Клиент не найден

Объект ошибки: {"error": "…"}

Заказы

Расчёт и обработка заказов с начислением / списанием бонусов

POST /orders/calculate

Расчёт бонусов без записи

Рассчитывает возможное начисление и максимальное списание бонусов без создания заказа в базе данных. Передайте любое значение в redeem, чтобы получить can_redeem_max. Требует разрешение calculate:bonuses.

Позиции. Переданные items участвуют в расчёте, поэтому правила «за каждую позицию» и «от суммы товаров» здесь дают то же число, что и POST /orders/process на том же теле запроса.

Тело запроса application/json

ПолеТипОписание
phone* string
total* number

Сумма заказа

items OrderItem[] | null
items[].id integer | null

ID товара (опционально, для /calculate)

items[].name string | null

Название товара (опционально, для /process)

items[].price number | null
items[].qty integer | null
redeem integer | null

Передайте любое значение, чтобы рассчитать can_redeem_max

catalog_id integer | null

ID каталога товаров организации. Без него заказ не привязан ни к одному каталогу, и правила бонусов по каталогу, категории и свойствам товара не сработают. Если передан, items[].id сверяются с товарами этого каталога.

catalog_code string | null

Альтернатива catalog_id — код каталога. Указывать оба можно, но они должны ссылаться на один каталог.

Пример

curl -X POST https://loyal.rc9.ru/api/v1/orders/calculate \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"phone":"+79001234567","total":1000,"items":[{"id":0,"name":"string","price":500,"qty":2}],"redeem":1,"catalog_id":7,"catalog_code":"tpl_65f0a1b2c3d4e"}'

Ответы

200 Результат расчёта
ПолеТипОписание
can_accrue integer

Бонусов будет начислено

can_redeem_max integer

Максимум бонусов для списания (0 если redeem не передан)

client_balance integer

Текущий баланс клиента

{
    "can_accrue": 50,
    "can_redeem_max": 100,
    "client_balance": 150
}
404 Клиент не найден в организации

Объект ошибки: {"error": "…"}

422 Каталог не найден в организации, catalog_id и catalog_code противоречат друг другу, либо items[].id отсутствуют в каталоге (тогда в ответе есть unknown_items)

Объект ошибки: {"error": "…"}

POST /orders/process

Обработка заказа + бонусы

Создаёт заказ и выполняет начисление / списание бонусов. Операция идемпотентна: при повторном запросе с тем же X-Idempotency-Key возвращается кешированный ответ без повторной обработки. Требует разрешение write:transactions.

Отказ в списании. Если запрошенная сумма не проходит по правилам организации, заказ не проводится: возвращается 422 с полями redeem_requested и max_redeemable, в базе не остаётся ни заказа, ни начисления. Повтор с тем же X-Idempotency-Key отдаёт тот же отказ. Чтобы заранее узнать доступный лимит, используйте POST /orders/calculate.

Параметры

ИмяГдеТипОписание
X-Idempotency-Key* заголовок string (uuid)

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

Тело запроса application/json

ПолеТипОписание
phone* string
order_number* string

Номер заказа из вашей системы

total* number

Итоговая сумма заказа

items OrderItem[] | null
items[].id integer | null

ID товара (опционально, для /calculate)

items[].name string | null

Название товара (опционально, для /process)

items[].price number | null
items[].qty integer | null
redeem integer | null

Количество бонусов для списания (0 — не списывать)

catalog_id integer | null

ID каталога товаров организации. Без него заказ не привязан ни к одному каталогу, и правила бонусов по каталогу, категории и свойствам товара не сработают. Если передан, items[].id сверяются с товарами этого каталога.

catalog_code string | null

Альтернатива catalog_id — код каталога. Указывать оба можно, но они должны ссылаться на один каталог.

Пример

curl -X POST https://loyal.rc9.ru/api/v1/orders/process \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -H "Content-Type: application/json" \
  -d '{"phone":"+79001234567","order_number":"ORD-2026-001","total":1000,"items":[{"id":0,"name":"string","price":500,"qty":2}],"redeem":50,"catalog_id":7,"catalog_code":"tpl_65f0a1b2c3d4e"}'

Ответы

200 Заказ успешно обработан
ПолеТипОписание
order_id integer
order_number string
accrued integer

Начислено бонусов

redeemed integer

Списано бонусов

balance_after integer

Баланс клиента после операции

{
    "order_id": 15,
    "order_number": "ORD-2026-001",
    "accrued": 50,
    "redeemed": 50,
    "balance_after": 150
}
404 Клиент не найден в организации

Объект ошибки: {"error": "…"}

422 Списание бонусов невозможно по правилам организации — заказ не проведён. Тот же код возвращается, если каталог не найден в организации или items[].id отсутствуют в каталоге — в последнем случае ответ содержит unknown_items
ПолеТипОписание
error string
redeem_requested integer

Запрошенная сумма списания

max_redeemable integer

Сколько можно списать по правилам организации

{
    "error": "Списание бонусов невозможно по правилам организации",
    "redeem_requested": 5000,
    "max_redeemable": 1000
}
500 Ошибка обработки заказа

Объект ошибки: {"error": "…"}

POST /orders/return

Возврат по заказу

Полный или частичный. Начисленное снимается с округлением вверх, списанное возвращается с округлением вниз — в обе стороны в пользу магазина, иначе серией частичных возвратов можно доить баланс. Списанное возвращается в исходные партии с их сроками. Требует заголовок X-Idempotency-Key и разрешение write:returns.

Параметры

ИмяГдеТипОписание
X-Idempotency-Key* заголовок string

Тело запроса application/json

ПолеТипОписание
order_number* string
return_number* string

Уникален внутри организации

total number

Сумма возврата; либо передайте full

full boolean

Вернуть весь остаток заказа

items object[]

Пример

curl -X POST https://loyal.rc9.ru/api/v1/orders/return \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Idempotency-Key: value" \
  -H "Content-Type: application/json" \
  -d '{"order_number":"string","return_number":"string","total":0,"full":true,"items":[{}]}'

Ответы

200 OK
ПолеТипОписание
return_id integer
order_id integer
order_number string
return_number string
full boolean
ratio number
accrual_revoked integer
redemption_refunded integer
shortfall integer

Не удалось снять: клиент уже потратил начисленное

balance_after integer
{
    "return_id": 0,
    "order_id": 0,
    "order_number": "string",
    "return_number": "string",
    "full": true,
    "ratio": 0,
    "accrual_revoked": 0,
    "redemption_refunded": 0,
    "shortfall": 0,
    "balance_after": 0
}
404 Заказ или клиент не найдены

Объект ошибки: {"error": "…"}

409 Возврат с таким номером уже проведён

Объект ошибки: {"error": "…"}

422 Сумма возврата больше остатка по заказу

Объект ошибки: {"error": "…"}

События

Начисления вне заказа: регистрация, отзывы

POST /events

Начислить бонус за событие

Сумма берётся из настроек организации, в теле запроса она не принимается. Повтор и исчерпанный лимит возвращают 200 с awarded: 0 и причиной: эндпоинт зовут с ретраями, и 4xx попал бы в мониторинг интегратора как авария, хотя система отработала по настройкам. Уникальность держит бизнес-ключ (организация, клиент, событие, объект), а не заголовок идемпотентности — сайт, переустановивший интеграцию, сгенерирует новый ключ, а бонус за регистрацию всё равно должен остаться один. Требует разрешение write:events.

Тело запроса application/json

ПолеТипОписание
phone* string
event_type* string
subject_type string
subject_id string
external_key string
meta object

Пример

curl -X POST https://loyal.rc9.ru/api/v1/events \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"phone":"string","event_type":"review_photo","subject_type":"product","subject_id":"1024","external_key":"string","meta":{}}'

Ответы

200 Начислено, повтор или отказ по правилам
ПолеТипОписание
event_type string
awarded integer
already_awarded boolean
award_id integer | null
expires_at string (date-time) | null
balance integer
reason string | null

Значения: "already_awarded", "rule_inactive", "outside_period", "limit_reached", null.

{
    "event_type": "string",
    "awarded": 0,
    "already_awarded": true,
    "award_id": 0,
    "expires_at": "string",
    "balance": 0,
    "reason": "already_awarded"
}
404 Клиент не найден

Объект ошибки: {"error": "…"}

422 Событие не описано в настройках или не передан обязательный объект

Объект ошибки: {"error": "…"}

GET /events/types

Какие события организация оплачивает

Чтобы витрина писала «получите 500 баллов за отзыв с фото» без хардкода. Требует разрешение write:events.

Пример

curl -X GET https://loyal.rc9.ru/api/v1/events/types \
  -H "Authorization: Bearer $API_KEY"

Ответы

200 OK
ПолеТипОписание
data object[]
data[].code string
data[].name string
data[].amount integer
data[].requires_subject boolean
data[].limit_scope string

Значения: "once_per_client", "once_per_subject", "unlimited".

data[].min_order_amount number | null
{
    "data": [
        {
            "code": "string",
            "name": "string",
            "amount": 0,
            "requires_subject": true,
            "limit_scope": "once_per_client",
            "min_order_amount": 0
        }
    ]
}

Нужна помощь с интеграцией?

Подключим вашу кассу или сайт и проверим сценарии на тестовом стенде.

Оставить заявку