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

Ключ передаётся заголовком X-API-Key и заменяет собой вход по паролю — отдельной процедуры получения токена нет. Выглядит он как wlk_live_…; у ключей песочницы — wlk_test_…. Полное значение показывается один раз, сразу после создания, — скопируйте его и нажмите «Я скопировал». Потеряли — создайте новый, а старый отзовите.
Области доступа
У ключа есть области доступа — каждый метод требует свою. Ключ, созданный в кабинете, получает постоянный набор: transactions:write, cards:read и customers:write, — выбирать области форма не даёт. Этого хватает на все методы ниже.
| Область | Что открывает |
|---|---|
transactions:write | Проводить чеки и возвраты: POST /transactions, POST /transactions/:id/refund |
cards:read | Искать карты, читать историю, проверять купон: GET /cards/by-barcode/:barcode, POST /cards/lookup, GET /cards/:id/history, POST /cards/:id/coupon-preview |
customers:write | Заводить гостей и подтверждать их номер: POST /customers, POST /customers/phone-code, /phone-call-state, /phone-verify |
| не нужна | GET /program и GET /pos/locations — их может вызвать любой живой ключ |
Ещё три области — transactions:read, cards:write и customers:read — есть в схеме и у ключа песочницы, но ни один метод сегодня их не требует. Ключу без нужной области приходит FORBIDDEN с перечнем недостающих в тексте сообщения.
Выпущенные ключи
Все действующие ключи компании — на соседней вкладке «Интеграции» → «Выпущенные ключи». У каждого видно название, начало ключа (wlk_live_xxxxxxx…) — по нему ключ узнаётся в вашем конфиге, — и когда им пользовались последний раз: «использован 27 сент.» или «ещё не использовался». Области доступа в списке не показываются.
Кнопка «Отозвать» спрашивает подтверждение и гасит ключ сразу: программа, которая им ходила, перестанет работать. Отозванный ключ из списка исчезает и не восстанавливается.
Мы запоминаем заголовок User-Agent последнего запроса. Если в нём есть адрес в скобках с плюсом — МояКасса/2.1 (+https://shop.example.ru), — он выводится рядом с ключом, и владелец видит, чья программа им пользуется. Присылайте такой заголовок: доступ по нему не решается, но чужой адрес в списке заметен сразу.
Провести чек
Главный метод. Гость указывается штрихкодом карты (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": "2000000012345",
"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— сумма чека до скидок и списания баллов, в рублях. Больше нуля, не больше 10 000 000.redeemPoints— сколько баллов списать, целое, по умолчанию 0. Больше потолка нельзя: вернётсяREDEEM_LIMIT_EXCEEDED, а потолок никогда не выше баланса карты. Меньше минимального списания программы (minRedeemAmountизGET /program) — тоже отказ,VALIDATION_ERROR.items— позиции чека, до 500:name,category,qty,price, все необязательные. Нужны правилам, которые смотрят на категории товаров; без них правило по категории просто не сработает.externalId— номер чека в вашей системе, до 120 символов. Пригодится при разборе расхождений.couponCode— код купона или промокода, один на чек. Скидка купона считается раньше баллов, и потолок списания берётся уже от остатка. Проверить код до чека —POST /cards/:id/coupon-preview, ниже.applyDiscount: true— ваша касса показала гостю скидку правилом программы (discountиз поиска карты) и вычла её из суммы к оплате. Без флага сервер скидку не считает и не записывает: касса, которая о правиле не знает, гостю ничего не скинула.amountпри этом — сумма чека до скидки, как её пробила касса; скидка считается первой, потом купон от остатка, потом баллы.locationId— точка продаж изGET /program. От неё зависят три вещи: часовой пояс, по которому считаются правила вроде «до 11 утра»; выручка по точкам в отчётах; и сами правила — их можно привязать к точке. Точка, которой у компании нет, отклоняется (NOT_FOUND): чек с чужой ссылкой считался бы не по тем правилам и молчал бы об этом.staffId— кто провёл чек, идентификатор сотрудника компании. По нему считаются оценки визитов и «кто сколько провёл». Необязательный; чужой идентификатор —NOT_FOUND, а не тихий ноль.
Ответ 201
{
"transactionId": "5a1d...",
"card": { "id": "b7c2...", "barcode": "2000000012345", "balance": 1839, "balanceBefore": 1864 },
"points": { "earned": 75, "redeemed": 100, "redeemableWas": 425 },
"tier": { "name": "Легенда", "level": 2, "changed": false },
"appliedRules": [{ "id": "…", "name": "Базовое начисление 10%", "points": 75 }],
"stamps": null,
"coupon": null,
"discount": null,
"currencyName": "баллы"
}points.earnedсчитается от суммы, оплаченной деньгами, — после скидки, купона и списания баллов.redeemableWas— сколько можно было списать в этом чеке: по нему касса видит, что гость списал не всё, что мог.stamps— штампы по чеку:name,delta(сколько дал чек),countиtarget(счётчик и цель),rewardsиrewardPoints— подарки этим чеком.null— штампов в программе нет.coupon— погашенный купон:title,kind(POINTS,PERCENT,GIFT),discount— рубли, которые касса вычитает из чека сама,points— баллы на карту,gift— позиция, которую касса кладёт в чек бесплатно.discount— скидка правилом, если чек пришёл сapplyDiscount: true:percent,amountв рублях иruleName.
Повтор с тем же ключом в первые сутки отдаёт первый ответ целиком. Позже — или если два одинаковых запроса пришли одновременно — приходит ответ, собранный по уже проведённому чеку: баланс и баллы те же, но redeemableWas в нём 0, а appliedRules пуст.
Правила и точки продаж
Правило начисления или лимита списания заведение может привязать к точке: «после 18:00 двойные баллы, но только в зале», «в интернет-магазине списывать не больше 20% чека». На чеке работают правила без привязки плюс привязанные к его точке; правила чужих точек пропускаются.
Найти карту и узнать потолок списания
До чека гостю называют, сколько он может списать. Считает это сервер — правила лимита живут у него, — а касса складывает готовые числа.
POST /cards/lookup
{ "phone": "+79995551234", "locationId": "0f2a..." }Ответ 201
{
"id": "b7c2...", "barcode": "2000000012345", "balance": 1864, "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 %" },
"stamps": { "name": "Шестая чашка в подарок", "count": 3, "target": 6, "rewardPoints": 250 },
"coupons": [
{ "id": "…", "code": "K7QM2X", "title": "−15 % на завтрак", "kind": "PERCENT",
"value": 15, "giftSku": null, "validTo": "2026-10-31T20:59:59.000Z" }
]
}maxRedeemPercent— какую долю чека можно закрыть баллами на названной точке: настройка программы, срезанная правилами лимита. БезlocationIdприходит доля для чека без точки.maxRedeemPoints— потолок в баллах, если правила его задали;null— потолка нет. Отдельным числом, потому что доля зависит от суммы чека, а потолок нет.discount— прямая скидка правилом программы, которую гостю называют до чека: «ваша скидка 10 %».null— скидки нет. Рубли считаются от полной суммы чека:round(amount × percent) / 100, не большеcap, если он задан. Чтобы сервер записал скидку, чек проводится сapplyDiscount: true.stamps— прогресс по штампам: «третья чашка из шести». Без оглядки на точку — счётчик на карте один.null— штампов в программе нет.coupons— купоны гостя, которые можно погасить сейчас. Код уходит вcouponCodeчека.status—ACTIVEу действующей карты. По карте в другом статусе чек не проводится:CARD_BLOCKED.
Сколько списать с конкретного чека: min(баланс, сумма × maxRedeemPercent / 100 / pointValue, maxRedeemPoints), округляя вниз, где сумма — чек после скидки и купона. Тот же расчёт делает наш экран кассы.
То же самое по штрихкоду: GET /cards/by-barcode/:barcode?locationId=0f2a…. Номер телефона в адрес не кладётся нигде — он остался бы в журналах прокси, — поэтому поиск по телефону идёт телом POST. По телефону находится только действующая карта: гостя нет или вместо номера пришёл мусор — CARD_NOT_FOUND. По штрихкоду карта приходит в любом статусе — смотрите status.
Проверить купон до чека
POST /cards/:id/coupon-preview
{ "code": "K7QM2X", "amount": 850 }Ответ 201
{ "title": "−15 % на завтрак", "kind": "PERCENT", "discount": 127.5, "points": 0, "gift": null }Кассир вводит код с листовки и до «Провести» видит, что он даёт; опечатка иначе стоила бы возврата и второго чека. :id — идентификатор карты из поиска, amount — сумма чека, от неё считается скидка. Ответ тот же, что даст гашение: кода нет или он чужой — COUPON_NOT_FOUND, купон погашен, отозван или его срок вышел — COUPON_UNUSABLE с причиной в details.reason.
Все методы
| Метод и адрес | Что делает | Область |
|---|---|---|
GET /program | Проверить ключ: программа, цена балла, компания, тариф, точки | не нужна |
GET /pos/locations | Действующие точки продаж | не нужна |
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 |
POST /cards/:id/coupon-preview | Проверить код купона до чека | cards:read |
POST /customers | Завести гостя и выдать ему карту | customers:write |
POST /customers/phone-code | Отправить покупателю код подтверждения номера | customers:write |
POST /customers/phone-call-state | Узнать, подтвердил ли покупатель номер звонком | customers:write |
POST /customers/phone-verify | Проверить код подтверждения номера | customers:write |
Завести гостя
POST /customers
{
"phone": "+79995551234",
"firstName": "Анна",
"lastName": "Соколова",
"email": "anna@example.com",
"birthDate": "1994-05-14",
"consent": true,
"consentVersion": "1.0",
"source": "касса"
}Ответ 201
{
"customerId": "1c9e...",
"cardId": "b7c2...",
"barcode": "2000000012345",
"balance": 100,
"created": true,
"installUrl": "https://pass.snovacard.ru/card/…?token=…"
}Обязателен только phone. programId нужен, если у компании несколько программ, иначе — VALIDATION_ERROR. source — метка, откуда пришёл гость, до 40 символов, по умолчанию pos; она же приходит в вебхуке card.issued. installUrl — ссылка на карту гостя: с неё карта ставится в Apple Wallet или Google Кошелёк. Отдайте её гостю — QR-кодом на экране, сообщением.
Гость с таким номером уже есть и карта у него есть — вторая не заводится: приходит та же карта с created: false, баллы и приветственный бонус не трогаются. Ссылка installUrl при этом выпускается заново, а прежняя гаснет. Отказы при выдаче: OPERATOR_NOT_NAMED — заведение ещё не назвало себя оператором персональных данных, CARDS_LIMIT_REACHED — исчерпан лимит карт тарифа, PROGRAM_INACTIVE — программа выключена.
Подтвердить номер покупателя
Интернет-магазин знает покупателя по учётной записи, но телефон в профиле вписан им самим и владение номером не доказывает: по чужому номеру нашлась бы чужая карта. Прежде чем показать карту и списать баллы, номер подтверждают кодом. Код уходит покупателю каналами заведения и из его квоты, а сверяем его мы — магазин узнаёт только «подтверждён».
POST /customers/phone-code
{ "phone": "+79995551234" }Ответ 201
{
"sent": true,
"channel": "telegram",
"retryAfterSec": 60,
"reason": null,
"sentTo": null,
"call": null
}channel— чем ушёл код:telegram,sms,callилиemail. Письмо уходит только на почту, которую гость сам оставлял раньше, — адрес из запроса не принимается;sentTo— этот адрес, замаскированный.sent: false— код не отправлен. Сreason: null— прошло меньше минуты с прошлого кода на этот номер: он уже ушёл каналом изchannel, подождитеretryAfterSecсекунд. С заполненнымreason— доставить нечем:no_channels(у заведения не настроено ни одного канала) илиdelivery_failed(каналы есть, но ни один не довёз).call— если канал звонок:phonePretty— номер 8-800, на который покупатель звонит сам, вызов сбрасывается без ответа;checkId— что опрашивать;windowSec— сколько ждём звонка, 300 секунд.- В песочнице кода не отправляем вовсе, а возвращаем его полем
shownCode.
Пока покупатель звонит, опрашивайте POST /customers/phone-call-state с { "checkId": "…" }. Ответ 200 — { "state": "pending" }, expired или { "state": "confirmed", "code": "4821" }: после звонка с самого номера приходит код, и дальше подтверждение идёт общим путём.
POST /customers/phone-verify
{ "phone": "+79995551234", "code": "4821" }Ответ { "valid": true }, код при этом гасится. Неверный, истёкший или исчерпавший попытки код — VALIDATION_ERROR с объяснением в тексте. Код живёт пять минут, ошибиться можно пять раз. Код уходит только на российский мобильный (+7 9…), на номер — не больше пяти платных кодов и пяти писем в сутки; сверх этого — RATE_LIMITED. Подробнее о пределах — в кодах ошибок.
Возврат
POST /transactions/:id/refund
{ "amount": 350, "comment": "возврат круассана" }Ответ 201
{ "balance": 1799, "cardId": "b7c2...", "refundedAmount": 350, "complete": false }:id — transactionId из ответа на чек. Без amount возвращается весь остаток чека. С amount — часть, и таких возвратов может быть несколько: интернет-магазин возвращает заказ по позициям. Доля считается от суммы чека накопительно: снимается столько же процентов начисленного, возвращается столько же процентов списанного, а когда возвращена вся сумма, чек помечается возвращённым и всё сходится до балла — сколько бы кусков ни было. Округление в пользу заведения. Если начисленное гость уже потратил, баланс уходит в минус. Сумма больше остатка — VALIDATION_ERROR с details.remaining; возврат уже возвращённого чека — TRANSACTION_ALREADY_REFUNDED.
Частичный возврат защищайте заголовком Idempotency-Key, как и чек: повтор с тем же ключом не снимет долю дважды. Полному возврату ключ не нужен — его повтор отвергается статусом.
Кто я
GET /program
{
"company": { "name": "Кофейня «Заря»", "slug": "demo-coffee", "sandbox": false },
"program": {
"id": "…", "name": "Зёрна", "slug": "zarya",
"joinUrl": "https://pass.snovacard.ru/join/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, "cardsLimit": 2000 },
"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. program.joinUrl — публичная страница выдачи карты: её можно показать гостю QR-кодом. company.sandbox — ключ от песочницы. При нескольких программах укажите ?programId=…, иначе — VALIDATION_ERROR.
locations — точки продаж заведения, только действующие. Их идентификаторы и уходят в locationId; список стоит обновлять по расписанию, а не запоминать навсегда: заведение заводит новые точки и закрывает старые. Только список, без программы и лимитов, отдаёт GET /pos/locations — [{ "id": "…", "name": "…" }]; он работает и у компании с несколькими программами. cards — сколько карт выдано и сколько разрешает тариф: предел упирается молча, и гость узнаёт о нём первым.
У каждой точки — свой maxRedeemPercent и maxRedeemPoints: потолок списания с учётом привязанных к ней правил. Нужны, чтобы назвать «до 20%» до того, как покупатель назвался, — в чекауте, на экране кассы, в товароучёте: cards/lookup требует гостя, а число нужно и без него. Берите поле точки как есть — склеивать с процентом программы не надо, у точки без своих правил он уже подставлен.
История карты
GET /cards/:id/history
[
{ "id": "…", "date": "2026-08-27T09:14:02.511Z", "delta": 75, "balanceAfter": 1839,
"reason": "PURCHASE", "comment": null, "amount": 850 },
{ "id": "…", "date": "2026-08-27T09:14:02.511Z", "delta": -100, "balanceAfter": 1764,
"reason": "REDEEM", "comment": null, "amount": 850 }
]Последние 100 движений баллов, новые первыми. reason — причина: PURCHASE, REDEEM, REFUND, EXPIRE (сгорание), MANUAL (правка из кабинета), WELCOME, BIRTHDAY, REFERRAL, COUPON, STAMP_REWARD и другие — список может расти. amount — сумма чека, если движение связано с чеком. Чужая или выдуманная карта — NOT_FOUND.
Проверить на стенде
Проще всего — в песочнице: она выдаёт компанию с ключом, программой и тремя гостями за пару секунд, без регистрации, и живёт неделю. Тот же адрес API и те же коды ошибок, что у боевых клиентов.
Второй путь — своя компания на тарифе «Про»: заведите программу, выдайте карту себе и проводите по ней чеки. Данные такой компании ничем не отличаются от боевых, поэтому не забудьте обнулить её перед запуском.
Машинного описания API (OpenAPI) снаружи нет: контракт — эта страница, вебхуки и коды ошибок.
Частые вопросы
Как передать, что чек пробит в конкретной точке?
Поле locationId в теле запроса, идентификатор берётся из GET /program или GET /pos/locations. Оно определяет часовой пояс для правил вроде «двойные баллы до 11 утра», выручку по точкам в отчётах и сами правила: их можно привязать к точке. Ту же точку называйте и при поиске карты, иначе потолок списания разойдётся с чеком.
Можно ли начислить баллы без чека?
Да, ручной корректировкой из кабинета — она требует комментария и попадает в журнал действий. В API такого метода нет намеренно: начисление без основания должно оставаться решением человека, а не строкой в чужом коде.
Что будет, если гость не найден?
Вернётся CARD_NOT_FOUND со статусом 404. Заводить гостя автоматически при первом чеке мы не станем: карта — это согласие на обработку данных, а его даёт человек.
Можно ли выдать ключу только чтение?
Нет: ключ из кабинета получает постоянный набор — проводить чеки, читать карты и заводить гостей. Разделяйте доступ ключами: по одному на кассу или систему, чтобы потерянный отзывался без остановки остальных.
Первые 20 карт — бесплатно. Программа настраивается за вечер.
Создать программу
Снова