Открытый API

Чеки, начисление и списание баллов, карты гостей — из вашей кассы, 1С или мобильного приложения.

API повторяет то, что делает экран кассира: проводит чеки, списывает и начисляет баллы, находит карты гостей. Через него подключают 1С, самописные кассы, товароучётные программы и мобильные приложения.

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

https://api.snovacard.ru/api/v1

Версия в адресе меняется только при несовместимых правках. Новые поля в ответах добавляются без смены версии — разбирайте JSON так, чтобы незнакомое поле не ломало клиента.

Ключ доступа

Ключ выдаётся в кабинете: «Интеграции» → «Ключи API». Он передаётся заголовком X-API-Key и заменяет собой вход по паролю — отдельной процедуры получения токена нет.

Вкладка «Ключи API»: создание ключа с названием и список выданных ключей
Ключ показывается один раз: в базе хранится только его отпечаток.
Вкладка «Ключи API»: создание ключа с названием и список выданных ключей

У ключа есть области доступа — выдавайте минимальные. Касса, которая только проводит чеки, не должна уметь читать базу гостей.

ОбластьЧто открывает
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 карт — бесплатно. Программа настраивается за вечер.

Создать программу