API-справочникШпаргалкаДемо виджета

FNFD — руководство по интеграции

Документ для разработчика на стороне бренда или агентства: как подключить промо-сайт, приложение или бэкенд к сервису проверки чеков FNFD.

Что делает сервис: принимает фискальный чек (QR или фото), проверяет его в ФНС (для UZ — в ОФД ГНК), применяет правила акции (товары, даты, лимиты, антифрод) и возвращает вердикт — принят или отклонён, с баллами и причиной. Результат приходит двумя путями: пуш на ваш callback-URL и запрос статуса по HTTP.

FNFD — развитие APMcheck, контракт приёма унаследован. Если у вас уже есть интеграция с APMcheck, начните с §12 «Миграция с APMcheck»: пути и заголовок те же, а вот состав полей в callback'е изменился, и молча это не пройдёт.

Интерактивный справочник по всем ручкам и схемам — /reference (Scalar) и /openapi.json на хосте API. Этот документ про то, как собрать интеграцию целиком; /reference — про поля конкретного запроса.

Содержание
  1. Поток данных
  2. Что запросить у FNFD до начала работ
  3. Хосты
  4. Способ A — виджет (быстрый путь)
  5. Способ B — серверный API
  6. Статус чека
  7. Callback — как принимать пуши
  8. Справочник rejectKey
  9. Настройки акции, которые влияют на вашу интеграцию
  10. Дубли, идемпотентность, ожидание ФНС
  11. Узбекистан (UZ)
  12. Миграция с APMcheck
  13. Как тестировать
  14. Чек-лист перед запуском
  15. Ссылки

1. Поток данных

Участник → [виджет / ваш фронт / ваш бот]
              │  POST чек + api-key
              ▼
         FNFD receipt-api ──► ФНС (whitelisted static IP)
              │                    │
              │              позиции чека
              ▼                    ▼
        правила акции + антифрод + (при необходимости) ручная модерация
              │
              ├──► callback на ваш URL:  ARRIVED → INSPECTED → REVIEWED
              └──► GET /api/receipts/{uuid}  (или публичный /r/{uuid}.json)

Жизненный цикл чека:

statecheckingStatusчто значит
ARRIVED0чек принят и поставлен в очередь, решения нет
INSPECTED1данные из ФНС получены, финального решения ещё нет
REVIEWED2финал: approved + причина/баллы

needsReview: true — чек ушёл на ручную модерацию. Он останется без финального вердикта, пока модераторы не примут решение; после этого придёт REVIEWED.

Приём асинхронный: HTTP-ответ отдаёт uuid сразу, проверка в ФНС и вердикт доезжают позже. Не блокируйте UI ожиданием вердикта — показывайте «чек отправлен» и обновляйте статус по callback'у или поллингу.


2. Что запросить у FNFD до начала работ

Всё это настраивается на стороне FNFD, вы получаете готовые значения. Пришлите список менеджеру акции одним письмом — так вы не будете ждать по одному пункту.

Обязательное:

  1. promoId — числовой идентификатор акции. Он же задаёт правила: товары, период, лимиты, разрешённые точки продаж, тип награды.
  2. API-ключ вида pk_…. Ключ выпускается на одну акцию: если в запросе не передан promoId, чек привяжется к акции ключа.
  3. Базовый URL API для вашей интеграции (см. §3).
  4. Callback настроен на ваш URL. Передайте FNFD:
{"url": "https://ваш-сайт.ru/api/fnfd/callback",
 "method": "POST",
 "headers": {"X-Callback-Secret": "<длинная случайная строка, придумываете вы>"}}

Метод и заголовки — ваши; FNFD отправляет их как есть, вы по ним проверяете подлинность пуша.

Стоит запросить сразу, иначе всплывёт на приёмке:

  1. Правила акции в человеческом виде — период, какие товары считаются промо-товарами, минимальная сумма, лимиты чеков на участника в день/неделю/акцию, ограничение по сетям и ИНН точек. От этого зависят тексты, которые вы показываете участнику.
  2. Тип награды — баллы или «допуск» (rewardKind: "qualify", см. §9). Во втором случае баллов не будет вообще, и UI «начислено N баллов» неуместен.
  3. Тестовый доступ — тестовая акция или тестовый ключ, чтобы не жечь квоту обращений к ФНС на боевой акции, плюс договорённость почистить тестовые чеки после приёмки.
  4. Нужны ли вам fnsReceipt и qrString в ответах — состав чека из ФНС и исходная QR-строка могут быть вырезаны из выдачи бренду настройками акции. Если вы показываете участнику позиции чека, скажите об этом заранее.
  5. Каналы для диспутов и выплат (disputes_callback, payments_callback) — если участник должен иметь возможность оспорить отказ или если выплаты идут через FNFD.

Ключ pk_… — это секрет уровня «отправить чек в акцию». При работе виджета он неизбежно виден в браузере, и это заложено в модель: ключ ограничен одной акцией, дедуп и лимиты живут на стороне FNFD. Но в публичный репозиторий его класть не нужно, а для серверных интеграций держите его только на сервере.


3. Хосты

ХостЧто там
api.fnfd.ruприём чеков и статусы
widget.fnfd.ruвстраиваемый виджет + демо /widget/demo
qr.fnfd.ruполигон приёма чека (/playground.html)
apidoc.fnfd.ru, api.fnfd.ru/referenceсправочник API

Целевой хост и режим работы для боевого запуска согласуйте с командой FNFD отдельно — для пилотов и для промышленной эксплуатации они могут отличаться. В коде вынесите базовый URL в конфиг, не зашивайте его в вызовы.


4. Способ A — виджет (быстрый путь)

Виджет закрывает весь фронт приёма: камера-сканер 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
dupCheckfalse отключает проверку дублей (только для отладки)
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, вы не будете знать, чей это чек.


5. Способ B — серверный API

Авторизация — заголовок api-key: pk_… на каждом запросе. Все поля на проводе в camelCase.

5.1 POST /api/receipts/qr — приём QR-строкой, основной метод

Поля запроса (JSON):

ПолеТипОписание
qrStrStringфискальная QR-строка чека, обязательно
userUuidStringваш идентификатор участника, обязательно
promoIdNumberакция; если не передан — берётся акция ключа
externalIdStringваш номер заказа/чека; вернётся в статусе и callback'е
dupCheckBooleanfalse отключает проверку дублей для этой отправки (отладка)
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"
      }'

Поля ответа:

ПолеТипОписание
statusStringsuccess
uuidStringидентификатор чека в FNFD — ваш ключ для статуса и сопоставления с callback'ом
receiptIdNumberсквозной порядковый номер (участнику показывать удобнее его)
replayedBooleantrue — этот чек этот же участник уже присылал, вернулся прежний результат, новой проверки не было
codeStringкод ошибки, когда status не success
{"status": "success", "uuid": "3f1c…", "receiptId": 53071}

5.2 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: это два чека, их надо слать по одному.

5.3 Остальные ручки приёма

РучкаКогда нужна
POST /api/widgets/receiptsform-data qrAsString или photos[] + userUuid, promoId, source, channel, meta, dupCheck — то, что шлёт виджет; используйте, если пишете свой фронт по его образцу
POST /api/receipts/urlJSON {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

5.4 Ошибки приёма

КодЧто случилось
400невалидный запрос: нет qrAsString/photos, кривой формат фото, >20 МБ, >5 файлов, meta не JSON
401нет или неверный api-key
409externalId уже использован в этой акции (при флаге external_id_unique_constraint) либо байтовый дубль фото
422POST /api/qr: QR на изображении не найден

Ответ на 409 по externalId намеренно сохранил legacy-форму — читайте existing_request_uuid, чтобы связать повтор с уже принятым чеком:

{"status": "failure", "existing_request_uuid": "3f1c…"}

6. Статус чека

С ключом — полная карточка:

curl -H "api-key: pk_xxx" https://api.fnfd.ru/api/receipts/3f1c…
ПолеТипОписание
stateStringARRIVED / INSPECTED / REVIEWED
checkingStatusNumberтот же статус числом: 0 / 1 / 2
uuid, receiptId, externalIdString / Number / Stringидентификаторы чека
userUuidStringваш идентификатор участника
promoId, countryNumber / Stringакция и её страна (ru / uz)
approvedBooleanфинальное решение
rejectKey, rejectReasonStringмашинный код и текст отказа (см. §8)
promoPointsNumberначисленные баллы, целое число (в APMcheck это был объект — см. §12)
rulesPoints, rulesCount, rulesNumber / Number / Arrayбаллы по правилам, число сработавших правил, имена засчитанных товаров
verdictObject{approved, matchedProducts[], totalPoints, rewardKind, qualifyLabel, rejectKey, antifraudViolations[], needsManualReview}
verdict.matchedProducts[]Array{productId, name, count, subtotalKopeks, points}
fnsReceiptObjectреквизиты из ФНС: fn/fd/fp, receiptDate, totalSum, retailerInn/Name/Address, taxSystem, isOnline, isPaid, items[]
retailer, cityObject{id, name, inn, store} и {id, city, region, address}
totalObject{sum, count, sumSubtotal}
answersObject{productsWithCountAndSubtotals[], products[]}
matchedPromosArrayчисловые id акций, под которые чек тоже подошёл
reviewersArrayкто выносил решение (auto или модераторы)
needsReviewBooleanчек у модераторов
deliveredStringстатус доставки последнего callback'а: DELIVERED / FAILED / null
photos, qrString, metaArray / 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-страница статуса

kindpending / approved / rejected, statusText — готовый русский текст. Это то, что опрашивает виджет; можно использовать во фронте, не раскрывая ключ.

Поллинг: интервал ~1,5 с, разумный потолок попыток; дальше переключайтесь на callback или показывайте «проверяем». Чека может не быть в базе ФНС в момент отправки — тогда он честно ждёт (см. §10).


7. Callback — как принимать пуши

На каждый переход статуса FNFD отправляет на ваш URL тело чека (тот же объект, что в §6) плюс поле state. Метод и заголовки — из конфига акции.

Что приходит на каждом состоянии:

stateЗаполненоПусто
ARRIVEDuuid, 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.

Доставка и ретраи:

Ответ бренда (необязательный, разбирается FNFD):

{"externalId": "order-8891", "meta": {"anything": "…"}, "status": "BLOCKED_USER"}

externalId и meta сохранятся в чек. status: "BLOCKED_USER" заблокирует участника в этой акции и отклонит его открытые чеки — так бренд пробрасывает свой антифрод.

Проверка подлинности. Подписи HMAC у пушей нет — как и в APMcheck, аутентификация это тот заголовок, который вы задали в конфиге акции (X-Callback-Secret или любой другой). Поэтому:

  1. сверяйте секрет из заголовка на каждом запросе, отвечайте 401/403 при несовпадении;
  2. секрет держите в env на сервере, не в git;
  3. обрабатывайте пуши идемпотентно по uuid + state — повтор события возможен всегда (ретрай, 404-восстановление, ручная переотправка из админки);
  4. отвечайте 2xx быстро, тяжёлую работу уносите в фон: таймаут доставки ~8–10 с.

Минимальный обработчик, к которому сводится вся логика:

@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}

8. Справочник 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. Обрабатывая ключи, сравнивайте регистр как есть.


9. Настройки акции, которые влияют на вашу интеграцию

Их задаёт оператор, но знать о них разработчику нужно:

НастройкаЭффект на интеграцию
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

10. Дубли, идемпотентность, ожидание ФНС


11. Узбекистан (UZ)

Акция может быть страны uz. Тогда:

Дверь проверки выбирается по стране акции автоматически — отдельных ручек нет.


12. Миграция с APMcheck

12.1 Что осталось прежним

12.2 Что изменилось — проверьте это до переключения

ЧтоAPMcheckFNFD
Денежные суммырубли с дробной частью ("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_subtotalssnake_caseanswers.productsWithCountAndSubtotals
total.sum_subtotalsnake_casetotal.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, не найдёт ничего. Это три места, где миграция ломается молча — начните с них.

12.3 Ключи отказов: соответствие

APMcheckFNFD
BRANDSNO_MATCH
OUTSIDE_INTERVALOUT_OF_PERIOD
FUTUREFUTURE_DATE
FEWRECEIPTSFEW_RECEIPTS
DUPLICATE_PHOTOduplicate_photo (lowercase, ось антифрода)
INCORRECTSTOREINN_NOT_ALLOWED / GEO_STORE
INCORRECTCITYGEO_CITY
TOTAL_PRODUCTS_COUNT_MINPRODUCTS_COUNT_MIN
TOTAL_PRODUCTS_PRICE_MINPRODUCTS_TOTAL_MIN
NOT_ALLOWED_RECEIPT_TYPERECEIPT_TYPE_NOT_ALLOWED
MAX_RECEIPTS_PER_DATEmax_per_user_per_date
NOT_QUANTITY_PRODUCTPRODUCTS_COUNT_MIN
AUTO_REJECTFNS_NOT_FOUND_TIMEOUT
FNS_QR_MISSINGFORMAT (или ручная модерация, если фото без QR)
REJECT_RULESNO_MATCH / PRODUCTS_COUNT_MIN — по причине
DUPLICATE, EXPIRED, ONLINE_RETAILER, USER_BLOCKED, FNS_SHORT_CHECK_FAILEDбез изменений
MODIFICATION, NOTARECEIPT, UNREADABLE, NOTALLPARTSрешения ручной модерации → MANUAL_REJECT с текстом

12.4 Виджеты

revizorWidget (только фото) и qrWidget (камера + ручной ввод + фото) заменены одним FnfdWidget — он умеет всё, что оба, и не тянет jQuery, ZXing и InputMask.

APMcheckFNFD
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, styleschannel, meta, i18n, css
поллинга нет (fire-and-forget)поллинг статуса встроен; submitted: true возвращает старое поведение

13. Как тестировать

  1. Разобрать QR без создания чекаPOST /api/qr, проверить, что ваш сканер отдаёт валидную строку.
  2. Полигонqr.fnfd.ru (/playground.html): отправка чека в выбранную акцию руками. Список доступных акций — публичный GET /api/public/promos (без ключа).
  3. Демо виджетаwidget.fnfd.ru/widget/demo.
  4. Callback — поднимите приёмник (или временный URL) и попросите FNFD прописать его в акцию. Проверьте три сценария: 2xx, 404 (должен прийти повторный ARRIVED), таймаут.
  5. Реальные чеки — только они ловят несовпадение названий товаров с правилами акции. Проверять матчинг на выдуманных строках бессмысленно: названия в чеке сокращены и у каждой сети свои.

Чеки, отправленные в тесте, расходуют квоту обращений к ФНС — не гоняйте циклы и заранее договоритесь с FNFD, что тестовые чеки вычистят после приёмки.


14. Чек-лист перед запуском

INN_NOT_ALLOWED, FEW_RECEIPTS и lowercase-лимиты


15. Ссылки

Вопросы по интеграции — менеджеру вашей акции на стороне FNFD. При обращении по конкретному чеку указывайте uuid или receiptId — по ним видна вся история проверки и доставки callback'ов.