Открытый API
Чеки, начисление и списание баллов, карты гостей — из вашей кассы, 1С или мобильного приложения.
API повторяет то, что делает экран кассира: проводит чеки, списывает и начисляет баллы, находит карты гостей. Через него подключают 1С, самописные кассы, товароучётные программы и мобильные приложения.
Базовый адрес и версия
https://api.snovacard.ru/api/v1Версия в адресе меняется только при несовместимых правках. Новые поля в ответах добавляются без смены версии — разбирайте JSON так, чтобы незнакомое поле не ломало клиента.
Ключ доступа
Ключ выдаётся в кабинете: «Интеграции» → «Ключи API». Он передаётся заголовком X-API-Key и заменяет собой вход по паролю — отдельной процедуры получения токена нет.

У ключа есть области доступа — выдавайте минимальные. Касса, которая только проводит чеки, не должна уметь читать базу гостей.
| Область | Что открывает |
|---|---|
transactions:write | Проводить чеки и возвраты |
transactions:read | Читать операции |
cards:read | Искать карты и читать историю |
cards:write | Изменять карты |
customers:read | Читать гостей |
customers:write | Заводить гостей и выдавать карты |
Провести чек
Главный метод. Гость указывается штрихкодом карты (cardBarcode) или телефоном (phone) — что-то одно обязательно.
POST /transactions
curl -X POST https://api.snovacard.ru/api/v1/transactions \
-H "X-API-Key: wlk_live_..." \
-H "Idempotency-Key: 9f1c0f2e-4a7b-4f3a-9c1e-2f1b7d5a8e10" \
-H "Content-Type: application/json" \
-d '{
"cardBarcode": "2244530202581",
"amount": 850,
"redeemPoints": 100,
"locationId": "0f2a...",
"externalId": "чек-2026-08-27-0042",
"items": [
{ "name": "Капучино", "category": "напитки", "qty": 2, "price": 250 },
{ "name": "Круассан", "category": "выпечка", "qty": 1, "price": 350 }
]
}'amount— сумма чека до списания баллов, в рублях.redeemPoints— сколько баллов списать. Больше потолка нельзя: вернётсяREDEEM_LIMIT_EXCEEDED.items— позиции чека. Нужны правилам, которые смотрят на категории товаров; без них правило по категории просто не сработает.externalId— номер чека в вашей системе. Пригодится при разборе расхождений.applyDiscount: true— ваша касса показала гостю скидку правилом программы (discountиз поиска карты) и вычла её из суммы к оплате. Без флага сервер скидку не считает и не записывает: касса, которая о правиле не знает, гостю ничего не скинула.amountпри этом — сумма чека до скидки, как её пробила касса; скидка считается первой, потом купон от остатка, потом баллы.locationId— точка продаж изGET /program. От неё зависят три вещи: часовой пояс, по которому считаются правила вроде «до 11 утра»; выручка по точкам в отчётах; и сами правила — их можно привязать к точке. Точка, которой у компании нет, отклоняется: чек с чужой ссылкой считался бы не по тем правилам и молчал бы об этом.
Ответ 201
{
"transactionId": "5a1d...",
"card": { "id": "b7c2...", "barcode": "2000000012345", "balance": 1849, "balanceBefore": 1864 },
"points": { "earned": 85, "redeemed": 100, "redeemableWas": 425 },
"tier": { "name": "Легенда", "level": 2, "changed": false },
"appliedRules": [{ "id": "…", "name": "Базовое начисление 10%", "points": 85 }],
"currencyName": "баллы"
}redeemableWas — сколько можно было списать в этом чеке: по нему касса видит, что гость списал не всё, что мог. У повторного запроса с тем же ключом это число нулевое — оно живёт только в первом ответе.
Правила и точки продаж
Правило начисления или лимита списания заведение может привязать к точке: «после 18:00 двойные баллы, но только в зале», «в интернет-магазине списывать не больше 20% чека». На чеке работают правила без привязки плюс привязанные к его точке; правила чужих точек пропускаются.
Найти карту и узнать потолок списания
До чека гостю называют, сколько он может списать. Считает это сервер — правила лимита живут у него, — а касса складывает готовые числа.
POST /cards/lookup
{ "phone": "+79995551234", "locationId": "0f2a..." }Ответ 200
{
"id": "b7c2...", "barcode": "2000000012345", "balance": 1849, "status": "ACTIVE",
"currencyName": "зёрна",
"currency": { "name": "зёрна", "one": "зерно", "few": "зерна", "many": "зёрен" },
"customer": { "name": "Анна Соколова", "phone": "+79995551234" },
"tier": { "name": "Легенда", "level": 2 },
"maxRedeemPercent": 20,
"maxRedeemPoints": null,
"pointValue": 1,
"discount": { "percent": 10, "cap": null, "ruleName": "Постоянным −10 %" }
}maxRedeemPercent— какую долю чека можно закрыть баллами на названной точке: настройка программы, срезанная правилами лимита. БезlocationIdприходит доля для чека без точки.maxRedeemPoints— потолок в баллах, если правила его задали;null— потолка нет. Отдельным числом, потому что доля зависит от суммы чека, а потолок нет.discount— прямая скидка правилом программы, которую гостю называют до чека: «ваша скидка 10 %».null— скидки нет. Рубли считаются от полной суммы чека:round(amount × percent) / 100, не большеcap, если он задан. Чтобы сервер записал скидку, чек проводится сapplyDiscount: true.
Сколько списать с конкретного чека: min(баланс, amount × maxRedeemPercent / 100 / pointValue, maxRedeemPoints), округляя вниз. Тот же расчёт делает наш экран кассы.
То же самое по штрихкоду: GET /cards/by-barcode/:barcode?locationId=0f2a….
Остальные методы
| Метод и адрес | Что делает | Область |
|---|---|---|
POST /customers | Завести гостя и выдать ему карту | customers:write |
POST /transactions | Провести чек: начисление и списание | transactions:write |
POST /transactions/:id/refund | Возврат: полный или частичный | transactions:write |
GET /cards/by-barcode/:barcode | Найти карту по штрихкоду | cards:read |
POST /cards/lookup | Найти карту по телефону: номер идёт телом запроса, а не в адресе | cards:read |
GET /cards/:id/history | История движения баллов по карте | cards:read |
GET /program | Проверить ключ: программа, цена балла, компания и тариф | любая |
Завести гостя
POST /customers
{
"phone": "+79995551234",
"firstName": "Анна",
"email": "anna@example.com",
"birthDate": "1994-05-14",
"consent": true,
"consentVersion": "1.0",
"source": "касса"
}Возврат
POST /transactions/:id/refund
{ "amount": 350, "comment": "возврат круассана" }Ответ 201
{ "balance": 1799, "cardId": "b7c2...", "refundedAmount": 350, "complete": false }Без amount возвращается весь остаток чека. С amount — часть, и таких возвратов может быть несколько: интернет-магазин возвращает заказ по позициям. Доля считается от суммы чека накопительно: снимается столько же процентов начисленного, возвращается столько же процентов списанного, а когда возвращена вся сумма, чек помечается возвращённым и всё сходится до балла — сколько бы кусков ни было. Округление в пользу заведения. Сумма больше остатка — VALIDATION_ERROR с details.remaining; возврат уже возвращённого чека — TRANSACTION_ALREADY_REFUNDED.
Частичный возврат защищайте заголовком Idempotency-Key, как и чек: повтор с тем же ключом не снимет долю дважды. Полному возврату ключ не нужен — его повтор отвергается статусом.
Кто я
GET /program
{
"company": { "name": "Кофейня «Заря»", "slug": "demo-coffee" },
"program": {
"id": "…", "name": "Зёрна", "slug": "zarya", "isActive": true,
"currencyName": "зёрна",
"currency": { "name": "зёрна", "one": "зерно", "few": "зерна", "many": "зёрен" },
"pointValue": 1, "maxRedeemPercent": 30, "minRedeemAmount": 1,
"welcomeBonus": 100, "pointsExpireDays": 180
},
"operator": { "named": true, "title": "ООО «Заря»" },
"plan": { "code": "PRO", "name": "Про", "api": true, "webhooks": true },
"cards": { "active": 43, "limit": 2000 },
"locations": [
{ "id": "0f2a...", "name": "Заря на Покровке", "maxRedeemPercent": 30, "maxRedeemPoints": null },
{ "id": "7b31...", "name": "Интернет-магазин", "maxRedeemPercent": 20, "maxRedeemPoints": null }
]
}Первый вызов любой интеграции: проверяет ключ и отдаёт то, что нужно показать до первого гостя, — как называются бонусы и сколько стоит балл. Областей доступа не требует. operator.named — заведение назвало себя оператором персональных данных; пока это не так, выдача карт отвечает OPERATOR_NOT_NAMED. При нескольких программах укажите ?programId=….
locations — точки продаж заведения, только действующие. Их идентификаторы и уходят в locationId; список стоит обновлять по расписанию, а не запоминать навсегда: заведение заводит новые точки и закрывает старые. cards — сколько карт выдано и сколько разрешает тариф: предел упирается молча, и гость узнаёт о нём первым.
У каждой точки — свой maxRedeemPercent и maxRedeemPoints: потолок списания с учётом привязанных к ней правил. Нужны, чтобы назвать «до 20%» до того, как покупатель назвался, — в чекауте, на экране кассы, в товароучёте: cards/lookup требует телефон или карту, а число нужно и без них. Берите поле точки как есть — склеивать с процентом программы не надо, у точки без своих правил он уже подставлен.
Проверить на стенде
Проще всего — в песочнице: она выдаёт компанию с ключом, программой и тремя гостями за пару секунд, без регистрации, и живёт неделю. Тот же адрес API и те же коды ошибок, что у боевых клиентов.
Второй путь — своя компания на бесплатном тарифе: заведите программу, выдайте карту себе и проводите по ней чеки. Данные такой компании ничем не отличаются от боевых, поэтому не забудьте обнулить её перед запуском.
Полное описание методов с телами запросов доступно в Swagger при локальном запуске API — /docs на порту 3001.
Частые вопросы
Как передать, что чек пробит в конкретной точке?
Поле locationId в теле запроса, идентификатор берётся из GET /program. Оно определяет часовой пояс для правил вроде «двойные баллы до 11 утра», выручку по точкам в отчётах и сами правила: их можно привязать к точке. Ту же точку называйте и при поиске карты, иначе потолок списания разойдётся с чеком.
Можно ли начислить баллы без чека?
Да, ручной корректировкой из кабинета — она требует комментария и попадает в журнал действий. В API такого метода нет намеренно: начисление без основания должно оставаться решением человека, а не строкой в чужом коде.
Что будет, если гость не найден?
Вернётся CARD_NOT_FOUND со статусом 404. Заводить гостя автоматически при первом чеке мы не станем: карта — это согласие на обработку данных, а его даёт человек.
Первые 50 карт — бесплатно. Программа настраивается за вечер.
Создать программу
Снова