Ключ создаётся в панели организации, в разделе «Интеграции → 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
}
Сценарий кассы
До оплаты касса спрашивает, сколько бонусов можно списать и сколько начислится. После оплаты — проводит заказ с ключом идемпотентности.
На одном и том же теле запроса calculate и process считают одинаково: сколько обещали на экране кассы, столько и начислится.
Что важно знать
Телефон передавайте нормализованным — 11 цифр, начиная с 7. Разные записи одного номера создадут разных клиентов.
Позиции необязательны, но без items не сработают правила по товарам, категориям и свойствам. Правила по каталогу работают, когда в заказе передан catalog_id или catalog_code.
Отказ в списании отменяет весь заказ: ни заказа, ни начисления в базе не остаётся. Лимит заранее отдаёт POST /orders/calculate.
Начисленное может стать доступным не сразу, если организация задала задержку активации: такие бонусы приходят в pending, а в истории — со статусом pending.
Бонусы сгорают. Для блока «скоро сгорят» в личном кабинете баланс отдаёт expiring_next и expiring_within_30d.
К заказу применяется уровень, который был до него. Новый уровень действует со следующей покупки.
Возврат пересчитывается пропорционально, с округлением в пользу магазина. Если клиент уже потратил начисленное, недостача приходит в shortfall.
Клиенты
Поиск клиентов, баланс бонусов и история транзакций
POST/clients
Создать или найти клиента
Создаёт нового клиента или возвращает существующего. Если клиент уже привязан к организации — возвращает его данные. Поле name устанавливается только если имя ещё не задано.
Возвращает файл .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)
Правка перезапускает отсчёт права на бонус ко дню рождения
Текущий уровень, следующий и сколько осталось купить до него. 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"
Расчёт и обработка заказов с начислением / списанием бонусов
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 — код каталога. Указывать оба можно, но они должны ссылаться на один каталог.
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 — код каталога. Указывать оба можно, но они должны ссылаться на один каталог.
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.
Сумма берётся из настроек организации, в теле запроса она не принимается. Повтор и исчерпанный лимит возвращают 200 с awarded: 0 и причиной: эндпоинт зовут с ретраями, и 4xx попал бы в мониторинг интегратора как авария, хотя система отработала по настройкам. Уникальность держит бизнес-ключ (организация, клиент, событие, объект), а не заголовок идемпотентности — сайт, переустановивший интеграцию, сгенерирует новый ключ, а бонус за регистрацию всё равно должен остаться один. Требует разрешение write:events.