Документ для разработчика на стороне бренда или агентства: как подключить промо-сайт, приложение или бэкенд к сервису проверки чеков FNFD.
Что делает сервис: принимает фискальный чек (QR или фото), проверяет его в ФНС (для UZ — в ОФД ГНК), применяет правила акции (товары, даты, лимиты, антифрод) и возвращает вердикт — принят или отклонён, с баллами и причиной. Результат приходит двумя путями: пуш на ваш callback-URL и запрос статуса по HTTP.
FNFD — развитие APMcheck, контракт приёма унаследован. Если у вас уже есть интеграция с APMcheck, начните с §12 «Миграция с APMcheck»: пути и заголовок те же, а вот состав полей в callback'е изменился, и молча это не пройдёт.
Интерактивный справочник по всем ручкам и схемам — /reference (Scalar) и /openapi.json на хосте API. Этот документ про то, как собрать интеграцию целиком; /reference — про поля конкретного запроса.
Участник → [виджет / ваш фронт / ваш бот]
│ POST чек + api-key
▼
FNFD receipt-api ──► ФНС (whitelisted static IP)
│ │
│ позиции чека
▼ ▼
правила акции + антифрод + (при необходимости) ручная модерация
│
├──► callback на ваш URL: ARRIVED → INSPECTED → REVIEWED
└──► GET /api/receipts/{uuid} (или публичный /r/{uuid}.json)
Жизненный цикл чека:
| state | checkingStatus | что значит |
|---|---|---|
ARRIVED | 0 | чек принят и поставлен в очередь, решения нет |
INSPECTED | 1 | данные из ФНС получены, финального решения ещё нет |
REVIEWED | 2 | финал: approved + причина/баллы |
needsReview: true — чек ушёл на ручную модерацию. Он останется без финального вердикта, пока модераторы не примут решение; после этого придёт REVIEWED.
Приём асинхронный: HTTP-ответ отдаёт uuid сразу, проверка в ФНС и вердикт доезжают позже. Не блокируйте UI ожиданием вердикта — показывайте «чек отправлен» и обновляйте статус по callback'у или поллингу.
Всё это настраивается на стороне FNFD, вы получаете готовые значения. Пришлите список менеджеру акции одним письмом — так вы не будете ждать по одному пункту.
Обязательное:
promoId — числовой идентификатор акции. Он же задаёт правила: товары, период, лимиты, разрешённые точки продаж, тип награды.pk_…. Ключ выпускается на одну акцию: если в запросе не передан promoId, чек привяжется к акции ключа.{"url": "https://ваш-сайт.ru/api/fnfd/callback",
"method": "POST",
"headers": {"X-Callback-Secret": "<длинная случайная строка, придумываете вы>"}}
Метод и заголовки — ваши; FNFD отправляет их как есть, вы по ним проверяете подлинность пуша.
Стоит запросить сразу, иначе всплывёт на приёмке:
rewardKind: "qualify", см. §9). Во втором случае баллов не будет вообще, и UI «начислено N баллов» неуместен.fnsReceipt и qrString в ответах — состав чека из ФНС и исходная QR-строка могут быть вырезаны из выдачи бренду настройками акции. Если вы показываете участнику позиции чека, скажите об этом заранее.disputes_callback, payments_callback) — если участник должен иметь возможность оспорить отказ или если выплаты идут через FNFD.Ключ pk_… — это секрет уровня «отправить чек в акцию». При работе виджета он неизбежно виден в браузере, и это заложено в модель: ключ ограничен одной акцией, дедуп и лимиты живут на стороне FNFD. Но в публичный репозиторий его класть не нужно, а для серверных интеграций держите его только на сервере.
| Хост | Что там |
|---|---|
api.fnfd.ru | приём чеков и статусы |
widget.fnfd.ru | встраиваемый виджет + демо /widget/demo |
qr.fnfd.ru | полигон приёма чека (/playground.html) |
apidoc.fnfd.ru, api.fnfd.ru/reference | справочник API |
Целевой хост и режим работы для боевого запуска согласуйте с командой FNFD отдельно — для пилотов и для промышленной эксплуатации они могут отличаться. В коде вынесите базовый URL в конфиг, не зашивайте его в вызовы.
Виджет закрывает весь фронт приёма: камера-сканер QR, загрузка фото, ручной ввод строки QR, экраны ошибок. Он изолирован Shadow DOM, поэтому не конфликтует со стилями сайта.
<script src="https://widget.fnfd.ru/widget/fnfd-widget.js"></script>
<div id="fnfd"></div>
<script>
FnfdWidget.init({
el: '#fnfd',
apiKey: 'pk_xxx',
promoId: 1003,
userUuid: currentUser.uuid, // ваш идентификатор участника
mode: 'both', // 'qr' | 'photo' | 'both'
submitted: true, // «Чек отправлен», статус — в вашем ЛК
onResult: function (uuid, promoId) {
// чек принят — сохраните uuid у себя, свяжите с участником
fetch('/api/receipts/registered', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({uuid: uuid, promoId: promoId})
});
},
onError: function (codeOrBody) { console.warn('fnfd', codeOrBody); }
});
</script>
Путь к скрипту всегда содержит /widget/: …/widget/fnfd-widget.js. Скрипт отдаёт то же приложение, что принимает чеки, поэтому его можно грузить с любого из хостов §3 — widget.fnfd.ru и qr.fnfd.ru ведут в один бэкенд.
Точки входа: FnfdWidget.init(opts) — панель-лончер; .qr(opts) — сразу камера; .photo(opts) — сразу загрузка фото; .super(opts) — экран выбора «QR или фото».
Опции:
| Опция | Смысл |
|---|---|
el | селектор или DOM-узел контейнера |
apiKey | ключ pk_… (уходит в заголовке api-key) |
promoId | акция; можно опустить, если ключ привязан к одной акции |
userUuid | ваш ID участника; без него генерируется случайный (тогда дедуп «тот же участник» не работает) |
mode | 'both' (по умолчанию) / 'qr' / 'photo' |
autoOpen | 'qr' / 'photo' / 'chooser' — открыть экран сразу, без лончера |
submitted (closeOnSubmit) | не ждать вердикт: показать «Чек отправлен» и закрыться |
country | 'ru' (по умолчанию) или 'uz' — формат проверяемого QR |
dupCheck | false отключает проверку дублей (только для отладки) |
channel | канал приёма: web, telegram, cbot, sms… (по умолчанию web) |
meta | произвольный JSON бренда (utm и т.п.); виджет добавит meta.hash — отпечаток устройства для антифрода |
setMeta(obj) | домержить мету после инициализации (например, utm после согласия) |
onResult(uuid, promoId) | чек принят |
onError(codeOrBody) | ошибка приёма или сети |
onClose() | виджет закрыт участником |
i18n, css | переопределение текстов и стилей внутри Shadow DOM |
apiBase | базовый URL API; по умолчанию — origin, с которого загружен скрипт |
Виджет сам отправляет POST /api/widgets/receipts и, если не включён submitted, опрашивает публичный /r/{uuid}.json.
Обязательный шаг на вашей стороне: в onResult сохранить uuid и связать его с участником. Иначе, когда придёт callback, вы не будете знать, чей это чек.
Авторизация — заголовок api-key: pk_… на каждом запросе. Все поля на проводе в camelCase.
POST /api/receipts/qr — приём QR-строкой, основной методПоля запроса (JSON):
| Поле | Тип | Описание |
|---|---|---|
qrStr | String | фискальная QR-строка чека, обязательно |
userUuid | String | ваш идентификатор участника, обязательно |
promoId | Number | акция; если не передан — берётся акция ключа |
externalId | String | ваш номер заказа/чека; вернётся в статусе и callback'е |
dupCheck | Boolean | false отключает проверку дублей для этой отправки (отладка) |
curl -X POST https://api.fnfd.ru/api/receipts/qr \
-H "api-key: pk_xxx" -H "Content-Type: application/json" \
-d '{
"qrStr": "t=20260803T1230&s=549.00&fn=9960440301234567&i=12345&fp=1234567890&n=1",
"userUuid": "user-42",
"promoId": 1003,
"externalId": "order-8891"
}'
Поля ответа:
| Поле | Тип | Описание |
|---|---|---|
status | String | success |
uuid | String | идентификатор чека в FNFD — ваш ключ для статуса и сопоставления с callback'ом |
receiptId | Number | сквозной порядковый номер (участнику показывать удобнее его) |
replayed | Boolean | true — этот чек этот же участник уже присылал, вернулся прежний результат, новой проверки не было |
code | String | код ошибки, когда status не success |
{"status": "success", "uuid": "3f1c…", "receiptId": 53071}
POST /api/receipts — приём фотоMultipart: photos (до 5 файлов, ≤20 МБ каждый, image/jpeg|png|webp|heic), userUuid, опционально promoId, externalId.
curl -X POST https://api.fnfd.ru/api/receipts \
-H "api-key: pk_xxx" \
-F "userUuid=user-42" -F "promoId=1003" \
-F "photos=@receipt.jpg"
QR распознаётся на сервере: распознан → чек идёт обычным путём (ФНС → вердикт), фото остаются приложенными; не распознан → чек остаётся ARRIVED и уходит на ручную модерацию. Несколько разных фискальных QR на одном кадре → отказ FEW_RECEIPTS: это два чека, их надо слать по одному.
| Ручка | Когда нужна |
|---|---|
POST /api/widgets/receipts | form-data qrAsString или photos[] + userUuid, promoId, source, channel, meta, dupCheck — то, что шлёт виджет; используйте, если пишете свой фронт по его образцу |
POST /api/receipts/url | JSON {urls[], userUuid, promoId?} — фото уже лежит у вас (типично для ботов), FNFD скачает сам |
POST /api/receipts/{uuid}/photos | добавить фото к существующему чеку |
POST /api/qr | синхронно разобрать QR (qrAsString или photo) → {fn, fd, fp, sumKopeks, n}. Чек не создаётся — валидация формата на вашей стороне до отправки |
POST /api/receipts/{uuid}/dispute | участник оспаривает отказ; диспут попадает в админку. Разрешено только по отклонённому чеку, иначе 409 |
| Код | Что случилось |
|---|---|
| 400 | невалидный запрос: нет qrAsString/photos, кривой формат фото, >20 МБ, >5 файлов, meta не JSON |
| 401 | нет или неверный api-key |
| 409 | externalId уже использован в этой акции (при флаге external_id_unique_constraint) либо байтовый дубль фото |
| 422 | POST /api/qr: QR на изображении не найден |
Ответ на 409 по externalId намеренно сохранил legacy-форму — читайте existing_request_uuid, чтобы связать повтор с уже принятым чеком:
{"status": "failure", "existing_request_uuid": "3f1c…"}
С ключом — полная карточка:
curl -H "api-key: pk_xxx" https://api.fnfd.ru/api/receipts/3f1c…
| Поле | Тип | Описание |
|---|---|---|
state | String | ARRIVED / INSPECTED / REVIEWED |
checkingStatus | Number | тот же статус числом: 0 / 1 / 2 |
uuid, receiptId, externalId | String / Number / String | идентификаторы чека |
userUuid | String | ваш идентификатор участника |
promoId, country | Number / String | акция и её страна (ru / uz) |
approved | Boolean | финальное решение |
rejectKey, rejectReason | String | машинный код и текст отказа (см. §8) |
promoPoints | Number | начисленные баллы, целое число (в APMcheck это был объект — см. §12) |
rulesPoints, rulesCount, rules | Number / Number / Array | баллы по правилам, число сработавших правил, имена засчитанных товаров |
verdict | Object | {approved, matchedProducts[], totalPoints, rewardKind, qualifyLabel, rejectKey, antifraudViolations[], needsManualReview} |
verdict.matchedProducts[] | Array | {productId, name, count, subtotalKopeks, points} |
fnsReceipt | Object | реквизиты из ФНС: fn/fd/fp, receiptDate, totalSum, retailerInn/Name/Address, taxSystem, isOnline, isPaid, items[] |
retailer, city | Object | {id, name, inn, store} и {id, city, region, address} |
total | Object | {sum, count, sumSubtotal} |
answers | Object | {productsWithCountAndSubtotals[], products[]} |
matchedPromos | Array | числовые id акций, под которые чек тоже подошёл |
reviewers | Array | кто выносил решение (auto или модераторы) |
needsReview | Boolean | чек у модераторов |
delivered | String | статус доставки последнего callback'а: DELIVERED / FAILED / null |
photos, qrString, meta | Array / String / Object | вложения, исходный QR, ваши данные |
⚠️ Все денежные суммы — целые копейки (totalSum, total.sum, subtotalKopeks, answers[].subtotal, items[].price/sum). В APMcheck это были рубли с дробной частью.
Поля fnsReceipt и qrString могут быть вырезаны из выдачи бренду настройками акции.
Полный пример REVIEWED:
{
"state": "REVIEWED",
"uuid": "3f1c…",
"userUuid": "user-42",
"approved": true,
"promoId": 1003,
"country": "ru",
"receiptId": 53071,
"externalId": "order-8891",
"createdAt": "2026-08-01T12:00:00",
"source": "widget-qr-scan",
"channel": "web",
"fnsReceipt": {
"fn": "9280410301339608", "fd": "8071", "fp": "2193063475",
"receiptDate": "2026-08-01", "totalSum": 54900,
"retailerInn": "7707083893", "retailerName": "Пятёрочка",
"retailerAddress": "Москва, Тверская 1",
"isOnline": false, "isPaid": true,
"items": [{"name": "КОФЕ JACOBS 95Г", "price": 54900, "quantity": 1.0, "sum": 54900}]
},
"verdict": {
"approved": true, "promoId": 1003, "totalPoints": 40, "rewardKind": "points",
"matchedProducts": [{"productId": 4, "name": "Jacobs", "count": 1.0,
"subtotalKopeks": 54900, "points": 40}],
"antifraudViolations": [], "needsManualReview": false
},
"retailer": {"name": "Пятёрочка", "inn": "7707083893", "store": "Москва, Тверская 1"},
"total": {"sum": 54900, "count": 1.0, "sumSubtotal": 54900},
"answers": {
"productsWithCountAndSubtotals": [{"id": 4, "name": "Jacobs", "count": 1.0, "subtotal": 54900}],
"products": [{"id": 4, "name": "Jacobs"}]
},
"rules": ["Jacobs"], "rulesCount": 1, "rulesPoints": 40,
"promoPoints": 40,
"reviewers": ["auto"],
"matchedPromos": [1003],
"needsReview": false,
"checkingStatus": 2
}
Без ключа — лёгкий статус для участника (сам uuid и есть секрет):
GET /r/{uuid}.json → {uuid, receiptId, state, checkingStatus, kind, statusText, approved, points}
GET /r/{uuid} → готовая HTML-страница статуса
kind — pending / approved / rejected, statusText — готовый русский текст. Это то, что опрашивает виджет; можно использовать во фронте, не раскрывая ключ.
Поллинг: интервал ~1,5 с, разумный потолок попыток; дальше переключайтесь на callback или показывайте «проверяем». Чека может не быть в базе ФНС в момент отправки — тогда он честно ждёт (см. §10).
На каждый переход статуса FNFD отправляет на ваш URL тело чека (тот же объект, что в §6) плюс поле state. Метод и заголовки — из конфига акции.
Что приходит на каждом состоянии:
| state | Заполнено | Пусто |
|---|---|---|
ARRIVED | uuid, userUuid, receiptId, promoId, photos, createdAt, source, channel, meta, approved: false | вердикт, реквизиты ФНС |
INSPECTED | то же + receiptDate, fnsReceipt, qrString, при отказе — rejectKey/rejectReason | финальное решение |
REVIEWED | всё: approved, verdict, promoPoints, retailer, city, total, answers, rules, reviewers, matchedPromos | — |
Начисляйте награду только на REVIEWED с approved: true.
Доставка и ретраи:
ARRIVED (считая, что вы «не знаете» этот чек), затем повторяет исходное событие — самовосстановление после потери записи на вашей стороне;delivered чека и в админке.Ответ бренда (необязательный, разбирается FNFD):
{"externalId": "order-8891", "meta": {"anything": "…"}, "status": "BLOCKED_USER"}
externalId и meta сохранятся в чек. status: "BLOCKED_USER" заблокирует участника в этой акции и отклонит его открытые чеки — так бренд пробрасывает свой антифрод.
Проверка подлинности. Подписи HMAC у пушей нет — как и в APMcheck, аутентификация это тот заголовок, который вы задали в конфиге акции (X-Callback-Secret или любой другой). Поэтому:
uuid + state — повтор события возможен всегда (ретрай, 404-восстановление, ручная переотправка из админки);Минимальный обработчик, к которому сводится вся логика:
@app.post("/api/fnfd/callback")
async def fnfd_callback(payload: dict, request: Request):
if request.headers.get("X-Callback-Secret") != CALLBACK_SECRET:
raise HTTPException(403)
uuid, state = payload["uuid"], payload["state"]
receipt = db.get_receipt(uuid)
if receipt is None:
return JSONResponse({"status": "unknown receipt"}, status_code=404) # → FNFD пришлёт ARRIVED
if receipt.state == state:
return {"ok": True} # повтор события — молча подтверждаем
db.update_state(uuid, state, payload)
if state == "REVIEWED" and payload.get("approved"):
award(receipt.user_id, payload.get("promoPoints", 0)) # начисление ровно здесь
return {"externalId": receipt.order_id}
rejectKeyКлюч стабилен — завязывайтесь в коде на него. Текст (rejectReason) настраивается для каждой акции отдельно, поэтому на него завязываться нельзя.
Правила акции и пре-чеки:
rejectKey | Текст по умолчанию |
|---|---|
NO_MATCH | Чек не содержит товаров акции |
DUPLICATE | Дубликат чека |
LINKED_DUPLICATE | Чек уже участвует в связанной акции |
COPYCAT | Чек уже участвует в другой акции |
MATCHED_IN_OTHER_PROMO | Чек засчитан в другой акции |
OUT_OF_PERIOD | Чек вне периода акции |
EXPIRED | Чек просрочен (поздно загружен) |
FUTURE_DATE | Чек из будущего |
MIN_SUM | Сумма чека ниже минимальной для акции |
PRODUCTS_COUNT_MIN | Недостаточно товаров акции |
PRODUCTS_TOTAL_MIN | Сумма товаров акции ниже минимума |
CHAIN_NOT_ALLOWED | Чек не из сети акции |
INN_NOT_ALLOWED | Чек не из магазина акции |
INN_BLACKLISTED | Магазин в чёрном списке |
FN_NOT_WHITELISTED | Касса не участвует в акции |
GEO_CITY / GEO_STORE | Магазин не в городе акции / магазин не участвует |
ONLINE_RETAILER | Чек из интернет-магазина не участвует |
NON_PAID | Чек не оплачен (возврат) |
RECEIPT_TYPE_NOT_ALLOWED | Тип чека не участвует в акции |
USER_BLOCKED | Пользователь заблокирован в акции |
FORMAT | Некорректный QR-код чека |
FEW_RECEIPTS | На фото несколько разных чеков — загрузите по одному |
FULL_RECEIPT_REQUIRED | В чеке нет позиций |
MANUAL_REJECT | Отклонён модератором |
REJECTED | Прочее (ключ-фолбэк) |
ФНС: FNS_SHORT_CHECK_FAILED (чек не прошёл проверку), FNS_NOT_FOUND / FNS_NOT_FOUND_TIMEOUT (не появился в базе за отведённое время), FNS_NOT_READY (промежуточное: ещё ждём), FNS_ACCESS, FNS_BAD_INPUT.
Лимиты и антифрод — ключи в нижнем регистре, это не опечатка, а отдельная ось правил: max_per_user_per_day, max_per_user_per_week, max_per_user_per_promo, max_per_user_per_date, max_per_user_per_retailer, max_per_fn_per_day, max_rejected_per_user_per_day, delay_between_receipts, duplicate_photo. Обрабатывая ключи, сравнивайте регистр как есть.
Их задаёт оператор, но знать о них разработчику нужно:
| Настройка | Эффект на интеграцию |
|---|---|
qr_required | фото без распознанного QR не принимается (400) |
duplicates_check_disabled | дедуп по фискальному ключу выключен — один чек можно слать повторно |
external_id_unique_constraint | повтор externalId в акции → 409 |
лимиты участника (receipts_per_day/week/promo, delay_between_receipts) | превышение = отказ с lowercase-ключом, а не ошибка HTTP |
allowed_store_inns, check_allowed_inns, cities, stores | чек примут только из разрешённых точек — ИНН известен только после ответа ФНС, поэтому отсечка приходит в REVIEWED, не в момент приёма |
reward_kind: "qualify" | акция без баллов: успешный чек = допуск к розыгрышу, qualifyLabel — текст исхода. Не показывайте «начислено 0 баллов» |
copycat_promos, matching_promos | один скан участвует в нескольких акциях / попадает в matchedPromos |
suspicious_product_count, auto_review_enabled | часть чеков уходит к модераторам — вердикт придёт с задержкой в часы |
auto_reject_after_days | сколько ждём появления чека в ФНС до авто-отказа |
include_fns_receipt, include_qr_string | вырезают fnsReceipt / qrString из выдачи бренду |
reject_texts | свои формулировки отказов под ключи из §8 |
userUuid → идемпотентный повтор: тот же uuid, replayed: true, новой проверки нет.DUPLICATE.dupCheck: false отключает проверку дублей для конкретной отправки. Это отладочный тумблер, в бою не используйте.30с → 5м → 30м → 3ч → 24ч → 72ч. Если за это время он не появился — авто-отказ. Для участника это значит: «проверяем» может честно длиться часами, и UI должен это переживать.Акция может быть страны uz. Тогда:
https://ofd.soliq.uz/check?…;country: 'uz'.Дверь проверки выбирается по стране акции автоматически — отдельных ручек нет.
POST /api/widgets/receipts, /api/receipts, /api/receipts/qr, /api/receipts/url, /api/qr, GET /api/receipts/{uuid}.api-key, ответ {status: "success", uuid, receiptId}, 409 с existing_request_uuid при повторе внешнего номера.ARRIVED → INSPECTED → REVIEWED, дискриминатор — поле state. URL, метод и заголовки задаются для каждой акции; своей подписи FNFD не добавляет.{externalId, meta} сохраняется в чек; 404 на не-ARRIVED пуше запускает досыл ARRIVED; 424 не ретраится.uuid, userUuid, receiptId, promoId, photos, createdAt, receiptDate, source, channel, meta, approved, rejectReason, rejectKey, retailer, city, total, answers, rules, rulesCount, rulesPoints, reviewers, qrString, duplicate.| Что | APMcheck | FNFD |
|---|---|---|
| Денежные суммы | рубли с дробной частью ("sum": 500.0) | целые копейки ("sum": 50000) |
promoPoints | объект {earned, totalEarned, leftovers, leftoversInt} | число — начисленные баллы |
matchedPromos | массив объектов {id, name, meta} | массив числовых id |
rules | массив объектов {id, name, count, points} | массив строк — имён засчитанных товаров |
answers.products_with_count_and_subtotals | snake_case | answers.productsWithCountAndSubtotals |
total.sum_subtotal | snake_case | total.sumSubtotal |
answers.products[] | {id, name, count} | {id, name} |
duplicate | объект с данными оригинала | строка (uuid оригинала) или null |
| Заголовок авторизации | api-key для приёма, Authorization для служебных ручек | везде api-key |
| Ключи отказов | см. таблицу ниже | переименованы |
| Новое | — | verdict{}, country, needsReview, delivered, checkingStatus, rewardKind/qualifyLabel, публичный статус /r/{uuid}.json, replayed |
Практически: код, который читал promoPoints.earned, получит undefined; код, который выводил total.sum как рубли, покажет сумму в сто раз больше; код, который искал answers.products_with_count_and_subtotals, не найдёт ничего. Это три места, где миграция ломается молча — начните с них.
| APMcheck | FNFD |
|---|---|
BRANDS | NO_MATCH |
OUTSIDE_INTERVAL | OUT_OF_PERIOD |
FUTURE | FUTURE_DATE |
FEWRECEIPTS | FEW_RECEIPTS |
DUPLICATE_PHOTO | duplicate_photo (lowercase, ось антифрода) |
INCORRECTSTORE | INN_NOT_ALLOWED / GEO_STORE |
INCORRECTCITY | GEO_CITY |
TOTAL_PRODUCTS_COUNT_MIN | PRODUCTS_COUNT_MIN |
TOTAL_PRODUCTS_PRICE_MIN | PRODUCTS_TOTAL_MIN |
NOT_ALLOWED_RECEIPT_TYPE | RECEIPT_TYPE_NOT_ALLOWED |
MAX_RECEIPTS_PER_DATE | max_per_user_per_date |
NOT_QUANTITY_PRODUCT | PRODUCTS_COUNT_MIN |
AUTO_REJECT | FNS_NOT_FOUND_TIMEOUT |
FNS_QR_MISSING | FORMAT (или ручная модерация, если фото без QR) |
REJECT_RULES | NO_MATCH / PRODUCTS_COUNT_MIN — по причине |
DUPLICATE, EXPIRED, ONLINE_RETAILER, USER_BLOCKED, FNS_SHORT_CHECK_FAILED | без изменений |
MODIFICATION, NOTARECEIPT, UNREADABLE, NOTALLPARTS | решения ручной модерации → MANUAL_REJECT с текстом |
revizorWidget (только фото) и qrWidget (камера + ручной ввод + фото) заменены одним FnfdWidget — он умеет всё, что оба, и не тянет jQuery, ZXing и InputMask.
| APMcheck | FNFD |
|---|---|
revizorWidget.init('#el', {apiKey, userUuid, successCallback, errorCallback}) | FnfdWidget.init({el, apiKey, promoId, userUuid, onResult, onError}) |
qrWidget.init('el', {api, apiKey, userUuid}) | то же; базовый URL — apiBase (по умолчанию origin скрипта) |
qrWidget.setMeta({...}) | setMeta({...}) — без изменений |
callbacks.onReceiptSentSuccess(res) | onResult(uuid, promoId) |
callbacks.onReceiptSentError(res) | onError(codeOrBody) |
channel, meta, i18n, styles | channel, meta, i18n, css |
| поллинга нет (fire-and-forget) | поллинг статуса встроен; submitted: true возвращает старое поведение |
POST /api/qr, проверить, что ваш сканер отдаёт валидную строку.qr.fnfd.ru (/playground.html): отправка чека в выбранную акцию руками. Список доступных акций — публичный GET /api/public/promos (без ключа).widget.fnfd.ru/widget/demo.ARRIVED), таймаут.Чеки, отправленные в тесте, расходуют квоту обращений к ФНС — не гоняйте циклы и заранее договоритесь с FNFD, что тестовые чеки вычистят после приёмки.
promoId и ключ pk_… получены; ключ на сервере или в env, не в gituserUuid — стабильный идентификатор вашего участника, не случайный на каждый визитuuid чека сохраняется у вас в момент приёма и связан с участникомuuid + stateREVIEWED + approved: truepromoPoints — как число (если мигрируете с APMcheck, см. §12)rejectKey — хотя бы DUPLICATE, NO_MATCH, OUT_OF_PERIOD, MIN_SUM,INN_NOT_ALLOWED, FEW_RECEIPTS и lowercase-лимиты
dupCheck: false нигде не осталсяhttps://api.fnfd.ru/reference (Scalar)https://api.fnfd.ru/openapi.json — из неё генерируется клиент на вашем языкеhttps://api.fnfd.ru/api-docshttps://widget.fnfd.ru/widget/demohttps://qr.fnfd.ruВопросы по интеграции — менеджеру вашей акции на стороне FNFD. При обращении по конкретному чеку указывайте uuid или receiptId — по ним видна вся история проверки и доставки callback'ов.