ВВалидатор для разработчиков
открытая документация О продукте Интерактивная схема API Песочница

Проверка подлинности чеков - API

Валидатор встраивается в платёжную систему как микросервис и отвечает вердиктом - настоящий чек, поддельный или нужен повторный снимок. Чек он принимает сам: обычно плательщик прикладывает файл напрямую нам - по ссылке, рамкой на вашем сайте или письмом, - и через вашу систему файл не проходит вовсе. Если чек уже у вас, отдайте его одним запросом. Один REST-интерфейс, вердикт с человекочитаемым пояснением, колбэки с подписью.

Куда слать запросы Боевой адрес API выдаётся при подключении вместе с ключом. Все примеры этой документации работают против песочницы https://validator.testoporat.com/sandbox - на ней собирается интеграция до подключения. Путь внедрения по шагам » Открыть песочницу » Чем бой отличается от песочницы »

Как это работает

Путей приёма два, и выбираете их вы. Основной - чек приносит плательщик: он прикладывает файл прямо Валидатору, а вы файла не касаетесь вовсе. Второй - чек отдаёте вы одним запросом, когда файл уже лежит у вас. Дальше пути сходятся в одно: та же проверка, те же вердикты, те же колбэки и та же карточка чека.

Путь 1, основной - чек прикладывает плательщик

Ваш сервер файл не принимает, не хранит и не пересылает: вы просите у нас адрес под заявку и показываете его плательщику. Своя форма загрузки и своё место под чужие чеки не нужны.

  1. Вы заводите заявку - один запрос POST /v1/upload-links с теми же полями заявки, что и у обычной подачи. В ответ 201 и сразу все адреса: upload_url - страница загрузки для плательщика, embed_url - она же рамкой на вашем сайте, email_address - адрес заявки для письма (если почтовый канал включён), а для вас - status_url и receipt_url. Адреса известны с этой секунды и больше не меняются. Ссылки загрузки »
  2. Плательщик прикладывает чек нам - открывает выданную страницу (GET /u/{token}) и загружает фото, снимок экрана или PDF; та же страница встаёт рамкой в ваш интерфейс, а вместо загрузки чек можно прислать письмом прямо из банка на адрес заявки. Ключ API плательщику не нужен и не выдаётся: пропуск - токен ссылки с ограниченным сроком жизни и лимитом попыток. Как выглядит экран »
  3. Проверка идёт у нас - тот же конвейер, что у подачи по API. Плательщик видит нейтральный экран «проверяем чек»; подробности проверки уходят только вам, по вашему ключу.
  4. Судьба заявки - одним адресом: GET /v1/upload-links/{token}. file_received - плательщик приложил чек (по нему останавливайте свой таймер ожидания, не дожидаясь вердикта), checking и eta_seconds - проверка идёт, job_id и verdict - готово. Колбэки на этом пути те же самые и приходят на тот же ваш адрес.
  5. Вердикт - те же четыре исхода, что и на втором пути. Если чек не прошёл, плательщик присылает новый по той же ссылке: сколько попыток истрачено и сколько осталось времени на новый чек, видно полями attempts и resend_left_seconds.
  6. Вы сообщаете итог ордера обратной связью - она нужна для сверки качества и разбора спорных случаев.

Когда выбирают этот путь. Плательщик сидит в мессенджере, в чате поддержки или у оператора, и формы для файла у вас нет; вы не хотите принимать и хранить чужие чеки у себя; интеграцию надо собрать за день - с вашей стороны это один запрос и показ ссылки.

Путь 2 - чек отдаёте вы одним запросом

Файл уже у вас: плательщик приложил его в вашей форме или он лежит в вашем хранилище.

  1. Плательщик приложил чек к заявке в вашей системе - фото, снимок экрана или PDF.
  2. Вы отдаёте чек Валидатору одним запросом POST /v1/receipts:validate: файл плюс ожидаемая сумма и реквизит; вместо файла можно прислать временную ссылку на ваше хранилище. Ответ приходит сразу - задача принята, ссылки на статус и файл уже в нём. Подача чека »
  3. Проверка идёт в фоне - выполняется асинхронно, ваша сделка стоит в ожидании, клиент ничего не перезагружает. Нужен ответ одним запросом - попросите подождать вердикт прямо в подаче полем wait_seconds.
  4. Вердикт приходит колбэком (или вы забираете его запросом статуса GET /v1/receipts/{job_id}): accepted - признаков подделки не найдено; resend_requested - заключения по этому файлу нет, нужен новый чек; rejected - чек не принят. Редкий четвёртый исход - unverified, разбор ниже.
  5. Вы сообщаете итог ордера обратной связью - она нужна для сверки качества и разбора спорных случаев.

Когда выбирают этот путь. Своя форма загрузки уже есть и менять её не хотят; чек приходит вам не от человека, а другой системой; нужен синхронный фильтр - вердикт прямо в ответе подачи по wait_seconds; чек надо проверить задним числом, из архива.

Варианты встраивания

Подключение - лесенка из четырёх уровней: можно остановиться на любом и подняться позже. Лесенка про то, сколько работы вы отдаёте нам, а не про очерёдность: путь плательщика (уровень 2) берут и первым шагом, не проходя нулевой и первый. На каждом уровне наша недоступность не останавливает ваши сделки - это свойство конструкции, а не обещание.

Уровень 0час работы
ЗеркалоВы ничего не меняете в своём процессе: в момент, когда чек попал к вам, копия файла уходит нам одним запросом, ответа проверки не ждёте. Вердикт придёт колбэком или опросом. Уже даёт заключение по каждому чеку, общую память о подделках и карточку чека по share_url.
Уровень 1рекомендуемый
ФильтрТот же единственный запрос, но с полем wait_seconds: вердикт приходит прямо в ответе приёма. PDF решается за доли секунды - основной поток фильтруется синхронно. Не успели или недоступны - вы пропускаете файл по своему регламенту, как будто нас нет.
Уровень 2
Плательщик грузит чек через насСсылки загрузки: плательщик прикладывает чек на нашей странице или в рамке на вашем сайте - вам не нужна своя форма и передача файла через ваш бэкенд. От нашей недоступности защищает резервный блок: он принимает файл в вашу систему и досылает нам потом.
Уровень 3
Полный контурКолбэки с подписью, карточка чека в вашей админке, кабинет партнёра со статистикой и настройками, обратная связь по итогам ордеров.

Файл чека может попасть к нам пятью путями - все они сходятся в одну и ту же проверку, с теми же вердиктами и колбэками:

Ссылка загрузки для плательщика страница /u/… или рамка на вашем сайте - основной путь: файла у вас нет и не нужно.
Письмом одноразовый почтовый адрес под заявку - для потоков, где чек приходит почтой.
Адрес отдаёт та же ссылка загрузки - поле email_address.
Файлом из вашей системы multipart-запрос с файлом и полями заявки - когда файл уже у вас.
Ссылкой на ваше хранилище JSON с file_url - байты не гоняются через ваш бэкенд; домен согласуется заранее.
Резервный блок на вашей странице готовый блок из интеграционного пакета: мы доступны - чек идёт нам, недоступны - в вашу систему с досылкой после восстановления.
Пакет выдаётся при подключении вместе с ключом.
Машинная схема API openapi.yaml понимают Swagger, Postman и генераторы клиентов - готовый код для вашего языка без ручного переписывания примеров.

Интерфейсы: что видят люди

API - не единственная поверхность продукта. Ниже - четыре готовых экрана, которые достаются вам без разработки. Макеты схематичны, данные выдуманы.

…/r/<токен>
Карточка проверки проверен
ЗаявкаORDER-1001
Суммасовпала
Реквизит получателясовпал
Время оплатысверено
Признаки подделкине найдены
Файл чекаоткрыть · скачать
Открывается по share_url без ключа API - кладите ссылку «посмотреть чек» в карточку операции своей админки. Про ссылку »
…/u/<токен>
Страница загрузки для плательщика

Приложите чек об оплате по заявке ORDER-1002

перетащите файл сюда или выберите
фото, снимок экрана или PDF
Отправить чек
Живёт по ссылке загрузки; та же страница встаёт рамкой на ваш сайт (embed_url). Про ссылки загрузки »
кабинет партнёра
Кабинет партнёра
1 247
чеков хранится
312 МБ
занимают
180
дней хранения
Адрес колбэковhttps://…/hooks
Секрет подписиwhsec_1a… · сменить
Журнал проверокза сегодня: 214
Статистика проверок, журнал, чеки на хранении и настройки потока - сроки, колбэки, чистка служебных полей. Доступ выдаётся при подключении.
ваш сайт · страница оплаты
Резервный блок приёма на вашей странице
Приложите чек об оплате
заявка, сумма, зона файла - как всегда
Мы доступнычек уходит на проверку
Мы недоступнычек принимается в вашу систему
Плательщик не видит разницы; принятое без нас досылается на проверку после восстановления. Блок из интеграционного пакета, три варианта посадки на страницу.

Облачное хранилище чеков

Проверенный чек не обязан жить у вас: он остаётся в нашем хранилище - зашифрованным, - а вы храните только ссылку. Своё файлохранилище чеков, резервные копии к нему и права доступа строить не нужно.

Как устроено шифрование хранения

Что у вас естьКак работает
Шифрование хранениявключено всегда, у всех партнёров, отключить нельзя - это свойство хранилища, а не настройка.
Файл по ключу APIreceipt_url - машинный доступ к байтам чека, в любой момент срока хранения. Подробнее »
Карточка без ключаshare_url - человекочитаемая страница «статус + заключение + файл» для вашей админки; живёт ровно столько, сколько чек лежит у нас. Ссылка равноценна документу - храните как секрет.
Срок хранения - ваша настройкаот «не хранить вовсе» до бессрочно, меняется в кабинете. Отдельная настройка - снимать чек с хранения сразу после того, как вы забрали файл по ключу.
Досрочное удалениеDELETE /v1/receipts/{job_id}/file - забрали файл к себе или удаляете по требованию плательщика.
Чистка служебных полейпо настройке исходящий файл отдаётся без метаданных (EXIF, GPS, служебные поля PDF) - данные плательщиков не едут дальше необходимого.
Сколько занятокабинет показывает число чеков на хранении, объём и действующий срок - видно, чем вы у нас пользуетесь.

Границы честно: хранилище - для чеков, прошедших через проверку, произвольные файлы оно не принимает. Что остаётся после нулевого срока и зачем - в разделе «Безопасность и данные».

Хранилище - приоритетное направление, шифрование - его первый принцип Мы развиваем хранилище в первую очередь, и каждая новая возможность строится поверх шифрования, а не в обход него. Если вашему потоку нужен особый режим - выгрузка архива разом, свои сроки по типам операций, отчёт о хранимом - напишите нам при подключении: такие запросы сейчас и определяют, что появится следующим.

Путь внедрения - от ключа до боевого режима

Семь шагов по порядку. У каждого есть проверяемый результат: пока он не получен, следующий шаг делать рано. Ссылка в шаге ведёт в раздел, где то же самое разобрано подробно и показано примером - справочник ниже читается уже по надобности, а не подряд.

  1. Ключ и адреса
    Подключение оформляем мы: выдаём ключ API, боевой адрес и интеграционный пакет (резервный блок для вашей страницы, закрытый справочник кодов). Тем же разговором согласуются четыре вещи, которые потом меняются дольше кода: список ваших исходящих IP, срок хранения чеков, домен вашего хранилища (если чек будете отдавать ссылкой) и секрет подписи колбэков. Сам адрес колбэков вы задаёте себе в кабинете партнёра, без нас. Ключ передаётся заголовком, храните его в секретах.
    Готово, когда: ключ лежит в секретах вашего стенда, боевой адрес записан в настройки отдельным значением - не в коде. Авторизация »
  2. Песочница вместо ожидания
    Ждать ключ не нужно: интеграция собирается на песочнице https://validator.testoporat.com/sandbox - те же адреса и те же имена полей, ключом служит любая строка от 8 символов. Настоящие чеки туда не загружайте, она открыта.
    Готово, когда: ваш код ходит в песочницу и получает от неё ответы. Песочница »
  3. Первый чек
    Один запрос POST /v1/receipts:validate: файл плюс поля заявки. Ответ приходит сразу - 202, задача создана, в теле job_id и адреса статуса, файла и карточки. Всегда передавайте request_id: он включает защиту от дублей, и повтор запроса по обрыву сети не заводит вторую задачу. Если у вас путь плательщика, первый чек идёт так же, только заявка заводится запросом POST /v1/upload-links, а файл прикладываете вы сами - открыв выданный upload_url вместо плательщика.
    Готово, когда: в ответе есть job_id, а повторная отправка того же запроса возвращает его же с пометкой "repeated": true. Три запроса подряд » · Подача файлом » · Ссылки загрузки »
  4. Вердикт
    Проверка идёт в фоне, результат забирается по status_url: пока "status":"processing" - ждите, при "status":"done" разбирайте вердикт. Вердиктов четыре, и обработаны должны быть все четыре, а у accepted ещё и status_color: жёлтый значит «чек подлинный, но данные разошлись с заявкой», а не «всё сошлось». На пути плательщика тот же вердикт забирается по адресу заявки - GET /v1/upload-links/{token}, отдельного опроса задачи не нужно.
    Готово, когда: в вашей системе есть ветка на каждый вердикт и на жёлтый цвет. Получение результата » · Что делать с каждым вердиктом »
  5. Колбэки вместо опроса
    Опрос статуса нужен только на время сборки. В бою вердикт приходит сам: адрес регистрируется в кабинете партнёра, секрет подписи выдаётся при подключении. Подпись проверяйте всегда и по сырому телу запроса - непроверенная подпись означает, что вердикт вам может прислать кто угодно.
    Готово, когда: событие с чужой подписью ваш приёмник отвергает, повторная доставка того же события ничего не ломает, ответ уходит быстрее секунды. Колбэки » · Порядок обработки »
  6. Обратная связь по итогу ордера
    Чем закончилась сделка, знаете только вы. Один запрос после вердикта - и спорный случай потом можно разобрать по фактам, а не по памяти.
    Готово, когда: итог ордера уходит нам автоматически, а не руками оператора. Обратная связь »
  7. Переход на боевой контур
    Меняются ровно две вещи - адрес и ключ. Но боевой контур строже песочницы: он требует обязательные поля заявки и по-настоящему проверяет подлинность. Сверьтесь с таблицей различий и пройдите чек-лист до выката, а не после.
    Готово, когда: адрес и ключ песочницы в боевой конфигурации не встречаются ни разу, чек-лист пройден целиком. Различия песочницы и боя » · Чек-лист внедрения »

Быстрый старт - три запроса в песочницу

Подать чек в песочницу, забрать вердикт, отправить обратную связь. Это второй путь - когда файл уже у вас; первый чек по пути плательщика начинается с создания ссылки загрузки, и песочница умеет его тоже.

# 1. Подать чек (файлом)
curl -s https://validator.testoporat.com/sandbox/v1/receipts:validate \
  -H "Authorization: Bearer sandbox-demo-key" \
  -F "file=@check.jpg" \
  -F "request_id=ORDER-1001" \
  -F "expected_amount=1500.00" \
  -F "requisite=79001234567" \
  -F "initiated_at=2026-08-23T10:15:00"

# Ответ: {"job_id":"...","status":"pending","eta_seconds":3.5,"status_url":"...",...}

# 2. Забрать результат (через пару секунд)
curl -s https://validator.testoporat.com/sandbox/v1/receipts/<job_id> \
  -H "Authorization: Bearer sandbox-demo-key"

# Ответ: {"status":"done","verdict":"accepted","status_color":"green",...}

# 3. Сообщить итог ордера
curl -s https://validator.testoporat.com/sandbox/v1/receipts/<job_id>/feedback \
  -H "Authorization: Bearer sandbox-demo-key" \
  -H "Content-Type: application/json" \
  -d '{"order_outcome":"approved","comment":"деньги пришли"}'

Авторизация

Ключ API выдаётся на подключение (у нас это называется «партнёр»). Передавайте его любым из двух заголовков - подойдёт любой, а если пришли оба, читается X-API-Key:

Authorization: Bearer ВАШ_КЛЮЧ
# или
X-API-Key: ВАШ_КЛЮЧ

Подача чека

POST/v1/receipts:validate

Принимает чек и заявку, отвечает сразу (202): задача создана, проверка пошла в фоне. Тот же 202 приходит и при отказе приёма - тогда в теле "status": "done", "verdict": "rejected" и машинный код причины. Решение принимайте по телу ответа, а не по коду HTTP. Два равноценных способа передать файл.

Способ 1 - файл телом запроса (multipart/form-data)

Основной способ по контракту. Файл кладётся в поле file, остальные поля - строками рядом.

curl -s https://validator.testoporat.com/sandbox/v1/receipts:validate \
  -H "Authorization: Bearer sandbox-demo-key" \
  -F "file=@check.jpg" \
  -F "request_id=ORDER-1001" \
  -F "expected_amount=1500.00" \
  -F "requisite=79001234567" \
  -F "initiated_at=2026-08-23T10:15:00" \
  -F "partner_meta={\"order\":1001}"

Способ 2 - временная ссылка (application/json)

Если чек уже лежит в вашем хранилище - передайте временную ссылку, сервис заберёт файл сам.

curl -s https://validator.testoporat.com/sandbox/v1/receipts:validate \
  -H "Authorization: Bearer sandbox-demo-key" \
  -H "Content-Type: application/json" \
  -d '{
    "file_url": "https://storage.example.com/tmp/check.jpg?sig=...",
    "request_id": "ORDER-1001",
    "expected_amount": "1500.00",
    "requisite": "79001234567",
    "initiated_at": "2026-08-23T10:15:00"
  }'
Про ссылки строго Загрузка по ссылке защищена от SSRF: только https, домен - из согласованного списка вашего хранилища, адреса внутренних сетей отклоняются, переходы по редиректам не выполняются. Пока список доменов не согласован при подключении, приём по ссылке выключен и отвечает кодом FETCH_DISABLED. Коды отказов - в таблице кодов приёма. Песочница по ссылке файл не скачивает, а строит ответ по имени файла в ней.

Поля заявки

ПолеОбязательноеЧто это
file / file_urlдасам чек: PDF, JPEG, PNG, WebP, GIF или HEIC (штатный снимок айфона - переводим в JPEG сами). Список можно сузить под себя в кабинете («Настройки подключения» → «Какие файлы принимаем»): тогда лишний формат отклоняется кодом FORMAT_NOT_ALLOWED на всех путях приёма
expected_amountда, в боевоможидаемая сумма платежа: десятичная строка, копейки через точку или запятую - "1500.00" и "1500,00" равноценны
requisiteда, в боевомреквизит получателя, который выдали плательщику: карта, счёт или телефон. Берётся строкой как есть - маску и пробелы приводить не надо
initiated_atда, в боевомкогда заявка создана, ISO 8601 (2026-08-23T10:15:00). Время без смещения считается локальным временем сервиса, со смещением - приводится к нему
request_idнастоятельноID заявки в вашей системе; едет во все ответы и колбэки, включает защиту от дублей
wait_secondsнетподождать вердикт прямо в ответе подачи: от 0 до 25 секунд, большее срезается до 25. Не успели - обычный pending, вердикт забирается по status_url или приходит колбэком
currencyнетвалюта заявки
bankнетбанк, если знаете его заранее - повышает точность
file_nameнетимя файла для журналов, до 80 символов; без него подставляется request_id
partner_metaнетваше сквозное поле до 4096 символов: вернётся как есть в статусе и колбэках - привязывайте наш ответ к своим сущностям без таблицы соответствий. Длиннее 4096 символов обрезается; у себя считайте это значение недоверенным вводом и экранируйте при выводе
Песочница мягче боевого - проверьте поля до выката В боевом контуре expected_amount, requisite и initiated_at обязательны все три: без любого из них подача отклоняется на приёме с кодом CONTEXT_FIELDS_MISSING. Песочница обязательные поля не проверяет, поэтому интеграция, собранная без них, работает в песочнице и падает в бою.

Ответ подачи

{
  "request_id": "ORDER-1001",
  "job_id": "3f8a1c2e-...",
  "status": "pending",
  "comment": "Чек принят, идёт проверка",
  "eta_seconds": 3.5,
  "status_url":  "https://.../v1/receipts/3f8a1c2e-...",
  "receipt_url": "https://.../v1/receipts/3f8a1c2e-.../file",
  "share_url":   "https://.../r/<токен>"
}
Где ждать отказ по самому файлу При подаче файлом формат, размер и тип содержимого проверяются уже в фоне, поэтому коды UNSUPPORTED_FORMAT, FILE_TOO_LARGE, IMAGE_TOO_LARGE и CONTENT_TYPE_MISMATCH приходят не ответом подачи, а вердиктом задачи - по status_url или колбэком. В ответе подачи такие отказы видны только у подачи ссылкой: там файл забирается сразу. Сверка заявленного типа мягкая - расхождение внутри картинок прощается, CONTENT_TYPE_MISMATCH бывает на границе «картинка - PDF».

Получение результата

GET/v1/receipts/{job_id}

Пока проверка идёт - "status":"processing". Когда закончена - "status":"done" и полный вердикт. Третье состояние - "status":"not_found": такой задачи нет, она чужая или ей больше суток (результат доступен по status_url сутки с момента подачи, сам файл чека живёт столько, сколько попросил партнёр). Ответ всегда 200 - разбирайте по полю status:

{
  "job_id": "3f8a1c2e-...",
  "request_id": "ORDER-1001",
  "status": "done",
  "verdict": "accepted",
  "reason_code": null,
  "status_color": "green",
  "comment": "Признаков подделки не найдено",
  "points": [],
  "mismatches": [],
  "extracted": {
    "amount": "1500.00",
    "payment_time": "2026-08-23T10:17:41",
    "bank": "...",
    "transfer_type": "..."
  },
  "file_available": true,
  "file_cropped": false,
  "partner_meta": "{\"order\":1001}"
}

Рядом едут служебные поля (отпечаток файла, длительность проверки и другие) - их состав может меняться, опирайтесь на перечисленные ниже. Блок extracted приходит только тогда, когда из чека удалось что-то извлечь.

ПолеЧто это
verdictaccepted | resend_requested | rejected | unverified - разбор в следующем разделе
reason_codeмашинный код главной причины (для resend_requested, rejected и unverified), список - ниже
status_colorgreen | yellow | red | resend | gray - готовый цвет для интерфейса оператора: yellow - данные чека разошлись с заявкой, resend - нужен новый чек, gray - заключения нет
commentчеловекочитаемое пояснение - можно показывать оператору как есть
mismatchesрасхождения с заявкой человеческим языком: непустой список при status_color: yellow - единственное место, где видно, что именно не сошлось
pointsпояснения к решению по пунктам для показа оператору, серьёзное сверху. Состав не фиксирован; у чистого accepted список пустой - это не перечень выполненных проверок
authenticityслужебная величина; для rejected и resend_requested приходит null. Решение принимается только по verdict и status_color
file_availableотдадим ли файл чека по receipt_url: при resend_requested - нет
file_croppedчек вырезан из кадра - вам уходит очищенный файл
extractedчто распознали: сумма, время оплаты, банк, вид перевода
partner_metaваше сквозное поле - как прислали, так и вернулось
Отказ приёма выглядит иначе Если файл или заявка не прошли приём, в ответе статуса приходят только status: "done", verdict: "rejected", reason_code, stage: "intake" и человекочитаемый detail. Полей comment, status_color и points в этом случае нет.

Вердикты и коды причин

ВердиктЧто делать вашей системе
acceptedпризнаков подделки не найдено. Дальше смотрите status_color: green - чек сошёлся с заявкой, проводите по вашему регламенту; yellow - данные чека расходятся с заявкой (что именно - в mismatches), решение за вами. При yellow поле reason_code пустое
resend_requestedзаключения по этому файлу нет - нужен новый чек. Покажите плательщику просьбу прислать чек заново: comment уже сформулирован по-человечески
rejectedчек не принят - дальше по вашему регламенту работы с подозрительными операциями. Сюда же попадает отказ приёма файла или заявки, тогда в reason_code код из таблицы приёма. Расхождение с заявкой сюда НЕ попадает - см. accepted с жёлтым цветом
unverifiedпроверку не удалось довести до конца по нашей технической причине: чек передан без заключения (status_color: gray), решение принимает ваш регламент

Что делать с каждым вердиктом

Та же четвёрка, но со стороны вашего процесса: что происходит со сделкой, что видит плательщик и есть ли у вас файл чека. Столбец «что видит плательщик» - про ваш экран; на пути плательщика просьбу прислать чек заново он уже увидел на нашей странице, и ваше дело - дождаться нового файла. Обратная связь по итогу ордера отправляется при любом вердикте - это единственное действие, одинаковое для всех.

ВердиктСделкаЧто видит плательщикФайл чека
accepted
status_color: green
идёт дальше по вашему обычному регламенту ничего особенного: чек принят есть - по receipt_url и share_url
accepted
status_color: yellow
решение за вами: подделки нет, но данные чека разошлись с заявкой. Что именно разошлось - в mismatches по вашему регламенту: чаще всего это ручная проверка оператором, а не отказ клиенту есть
resend_requested стоит в ожидании нового файла, отказом не считается просьбу прислать чек заново. Текст готов - поле comment написано человеческим языком, его можно показывать как есть нет: 409 с вердиктом и кодом причины
rejected дальше по вашему регламенту работы с подозрительными операциями решаете вы. Машинный код причины подделки не расшифровывается открыто, показывать его плательщику незачем нет: 409. Отказ приёма файла (коды приёма) - тот же вердикт, но причина техническая, и правится она на вашей стороне
unverified
status_color: gray
не должна стоять: заключения нет по нашей технической причине, а не по вине чека ничего: для плательщика это обычная заявка обычно есть - смотрите file_available
Три ошибки, на которых спотыкаются чаще всего Жёлтый accepted считают зелёным - и пропускают расхождение с заявкой. resend_requested показывают плательщику как отказ - и теряют честную сделку. Ветку unverified не пишут вовсе - и сделка встаёт из-за нашего сбоя, хотя именно этого конструкция и не допускает.

Коды причин: приём файла

Отказ приёма приходит обычным ответом подачи со status: "done", вердиктом rejected и машинным кодом.

reason_codeЧто случилосьЧто делать
EMPTY_FILEфайл не приложен или пустпроверьте поле file в запросе
FILE_TOO_LARGEфайл больше лимита подключенияпопросите чек из приложения банка, а не фото экрана в полном разрешении
IMAGE_TOO_LARGEизображение больше допустимого размера в пикселяхто же
UNSUPPORTED_FORMATформат файла не поддерживается либо тело запроса не того типашлите multipart/form-data или application/json и файл в PDF, JPEG, PNG, WebP, GIF или HEIC
FORMAT_NOT_ALLOWEDформат сервисом поддерживается, но ваша площадка его не принимает - так выбрано в кабинете («Настройки подключения» → «Какие файлы принимаем»)причина словами приходит в detail - её можно показать плательщику; список форматов меняется в кабинете
CONTENT_TYPE_MISMATCHзаявленный тип не совпал с содержимым файлане подставляйте тип вручную - берите его у источника файла
CONTEXT_FIELDS_MISSINGв заявке нет expected_amount, requisite или initiated_atв боевом контуре эти поля обязательны
CONTEXT_FIELDS_INVALIDсумма или дата заявки не разбираютсясумма - десятичная строка, дата - ISO 8601
URL_INVALIDв JSON-подаче нет file_url или в нём не разобрать хостпроверьте ссылку
URL_SCHEME_NOT_ALLOWEDссылка не по httpsвременные ссылки принимаются только по https
DOMAIN_NOT_ALLOWEDдомен ссылки не из согласованного спискасогласуйте домен хранилища при подключении
DNS_RESOLUTION_FAILEDдомен ссылки не резолвитсяпроверьте доступность хранилища снаружи
URL_RESOLVES_TO_INTERNAL_IPссылка ведёт во внутреннюю сеть - загрузка запрещенадавайте ссылку на публичное хранилище
FETCH_DISABLEDприём по ссылке не включён для вашего подключенияподавайте файл телом запроса или попросите включить
FETCH_FAILEDваше хранилище не отдало файл: ошибка, обрыв или таймаутповторите подачу, когда хранилище ответит

Коды причин: нужен новый чек (resend_requested)

Файл не удалось прочитать или чек из него не выделить. Поле comment уже сформулировано человеческим языком - его можно показывать как есть.

reason_codeЧто случилосьЧто делать
NOT_A_RECEIPTфайл читается, но это не чекпопросите прислать именно чек об оплате
LOW_EXTRACTION_CONFIDENCEчек не читаетсяпопросите файл или снимок экрана из приложения банка
AMOUNT_NOT_FOUNDсумма платежа в чеке не распозналасьто же
FOREIGN_BACKGROUNDв кадре снята обстановка вокруг чека, выделить сам чек не удалосьчек с посторонней обстановкой мы не передаём - попросите чек из приложения банка
FOREIGN_INTERFACE_VISIBLEв кадре видно постороннее приложение или сервиснужен чистый чек, без посторонних элементов на снимке

Коды причин: наши технические обстоятельства

Сюда попадают случаи, когда проверку не удалось довести до конца. Сделка из-за этого стоять не должна: часть таких чеков доставляется по ссылкам и без заключения - смотрите поле file_available.

reason_codeЧто случилосьЧто делать
QUEUE_FULLсервис не взял чек в проверку, ответ 503 сразуповторите подачу позже, заголовок Retry-After подскажет когда
PIPELINE_ERRORпроверка не завершилась из-за нашего сбояподайте чек ещё раз
CHECK_UNAVAILABLEзаключения нет, но чек передан: вердикт unverified, цвет grayрешайте по своему регламенту, файл доступен по ссылкам
CHECK_STALLEDпроверка затянулась дольше допустимого и была закрытато же
CHECK_INTERRUPTEDперезапуск сервиса оборвал незавершённую проверкуто же
Кодов признаков подделки в этом списке нет При вердикте rejected по результату анализа подлинности машинный код причины партнёру приходит, но его расшифровка не публикуется: открытый список того, что именно мы проверяем в чеке, был бы инструкцией для изготовителей подделок. Ориентируйтесь на verdict и человекочитаемый comment; полный справочник входит в пакет подключения под соглашением.

Повторные подачи - защита от дублей

Сетевые ретраи безопасны: повтор той же подачи не создаёт новую задачу - вернётся та же job_id с пометкой "repeated": true и тем же 202. Пока проверка идёт, ответ повтора приходит со "status":"pending" и без вердикта; когда готова - со "status":"done", verdict и reason_code, а полный результат забирается по status_url. Память о поданной заявке держится сутки, как и сам результат. Без request_id защиты от дублей нет.

# повторная отправка того же запроса по обрыву сети - безопасна
{"request_id":"ORDER-1001","job_id":"3f8a1c2e-...","status":"done",
 "repeated":true,"verdict":"accepted",...}

Файл чека и ссылки

GET/v1/receipts/{job_id}/file

Отдаёт файл чека по ключу API. Пока проверка не закончена - 202 и JSON со статусом processing. Если проверка затягивается, файл может быть отдан раньше заключения: такой ответ помечен заголовком X-Validator-Verdict: pending, вердикт придёт отдельно. Когда файла не будет по построению (нужен новый чек, отказ) - 409 с вердиктом и кодом причины; чужая или неизвестная задача - 404.

Срок хранения файла - настройка партнёра: от «не хранить вовсе» до бессрочно. По той же настройке чек может сниматься с хранения сразу после того, как вы забрали его по ключу API - тогда повторный запрос того же файла ответит 409 или 404. Открытие share_url выдачей партнёру не считается и файл не снимает.

DELETE/v1/receipts/{job_id}/file

Снять чек с хранения самим: вы забрали файл к себе или обязаны удалить его по требованию плательщика. Ответ - {"job_id": "...", "stored": false, "deleted": true}.

Ссылка без ключа - share_url из ответа подачи (вида /r/<токен>): в браузере открывается как карточка проверки - статус, заключение и сам чек, удобно для карточки операции в вашей админке; автоматизации по тому же адресу приходит файл. Токен непрозрачен и подделке не поддаётся, но он и есть весь пропуск: ссылка равноценна самому документу - храните её как секрет и не пересылайте плательщику. Живёт ровно столько, сколько чек лежит у нас на хранении.

Чистка служебных полей По настройке партнёра исходящий файл может отдаваться очищенным от служебных полей (метаданных) - чтобы не распространять данные плательщиков дальше необходимого. Включается в кабинете, по умолчанию файл отдаётся байт в байт как получен.

Колбэки - вердикт сам приходит к вам

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

СобытиеКогда приходит
receipt.acceptedфайл принят, проверка пошла - переводите сделку в ожидание
receipt.verdictпроверка закончена: verdict, status_color, reason_code, comment, points, file_available. Это выжимка - расхождения с заявкой и распознанные значения забираются по status_url
receipt.delayedпроверка затянулась - сделка не должна стоять из-за нашей очереди. Доступен ли уже сам чек, видно по полю file_available в теле события; вердикт в любом случае придёт отдельным receipt.verdict
# тело события: имя дублируется полем event и заголовком X-Validator-Event
{
  "event": "receipt.verdict",
  "job_id": "3f8a1c2e-...",
  "request_id": "ORDER-1001",
  "status": "done",
  "verdict": "accepted",
  "status_color": "green",
  "reason_code": null,
  "comment": "Признаков подделки не найдено",
  "points": [],
  "file_available": true,
  "status_url":  "https://.../v1/receipts/3f8a1c2e-...",
  "receipt_url": "https://.../v1/receipts/3f8a1c2e-.../file",
  "share_url":   "https://.../r/<токен>"
}
# у receipt.accepted вместо вердикта - "status": "pending" и comment;
# partner_meta едет в каждом событии, если вы его прислали

Подпись - проверять обязательно

Каждый колбэк подписан: заголовок X-Validator-Signature содержит HMAC-SHA256 от тела запроса, ключ - ваш секрет колбэков. Непроверенная подпись означает, что вердикт вам может прислать кто угодно.

# Python: проверка подписи
import hmac, hashlib

def подпись_верна(тело: bytes, заголовок: str, секрет: str) -> bool:
    ожидаемая = hmac.new(секрет.encode(), тело, hashlib.sha256).hexdigest()
    return hmac.compare_digest(ожидаемая, заголовок)

Подписывается сырое тело запроса в том виде, в каком оно пришло: считайте подпись ДО разбора JSON - пересобранное тело даёт другие байты, и подпись не сойдётся. Подпись - шестнадцатеричная строка в нижнем регистре; событие без заголовка подписи считайте чужим.

Доставка

Доставка «как минимум один раз», в два круга. Сразу - три попытки с паузами в секунды: это переживает моргание сети. Если приёмник не ответил, событие ложится в очередь досылки и повторяется с растущими паузами - через полминуты, две минуты, десять, полчаса, два часа, шесть, двенадцать, - пока не кончится окно доставки в сутки. Выкат новой версии вашего приёмника или перезагрузка сервера событие больше не теряют: поднявшись, вы получите всё накопленное. Очередь живёт в базе, поэтому переживает и наш перезапуск.

Приёмник колбэка - порядок действий

Порядок именно такой: каждый следующий шаг опирается на предыдущий.

  1. Прочитать сырое тело запроса байтами и проверить по нему подпись из X-Validator-Signature. До разбора JSON: пересобранное тело даёт другие байты, и подпись не сойдётся. Подпись не сошлась или заголовка нет - событие чужое, дальше не идём.
  2. Посмотреть X-Validator-Event-Id: это событие уже обрабатывали - ответить 2xx и выйти. Доставка «как минимум один раз», повторы приходят с тем же идентификатором, и защита от повтора - на вашей стороне.
  3. Разобрать тело: event говорит, что случилось, job_id и request_id - по какой заявке, partner_meta вернётся тем же, что вы прислали.
  4. Записать событие у себя и сразу ответить 2xx. Тяжёлую работу - в фон: успехом считается только быстрый ответ, медленный приёмник ловит повторные доставки на ровном месте.
  5. Уже в фоне применить вердикт к сделке. Принятый вердикт назад не откатывать: более позднее событие того же типа - это повтор, а не новое решение.

Что делать, если колбэк не пришёл вовсе: вердикт никуда не девается и в любой момент забирается по status_url. Опрос статуса - штатный запасной путь, а не поломка.

Ссылки загрузки - чек без вашей формы

POST/v1/upload-links

Если вам неоткуда взять файл (клиент в мессенджере, у оператора нет формы) - создайте ссылку загрузки: плательщик открывает её и прикладывает чек прямо нам, дальше всё идёт обычным путём - с вердиктом, статусом и колбэками. Поля заявки те же и так же обязательны; ttl_minutes задаёт срок жизни ссылки - по умолчанию час, не больше суток.

curl -s https://validator.testoporat.com/sandbox/v1/upload-links \
  -H "Authorization: Bearer sandbox-demo-key" \
  -H "Content-Type: application/json" \
  -d '{
    "request_id": "ORDER-1002",
    "expected_amount": "900.00",
    "requisite": "79007654321",
    "initiated_at": "2026-08-23T12:00:00",
    "ttl_minutes": 60
  }'

# Ответ 201:
{
  "token": "kZ2r...",                                # непрозрачная строка, на формат не опирайтесь
  "upload_url":    "https://.../u/kZ2r...",          # страница загрузки для плательщика
  "embed_url":     "https://.../u/kZ2r...?embed=1",  # та же страница в iframe на вашем сайте
  "expires_at":    "2026-08-23T13:00:00",            # до этого момента ссылка ждёт чек
  "max_attempts":  3,                                # попыток загрузки на одну ссылку
  "email_address": "order-1002.a1b2c3@...",          # чек можно прислать письмом; null - канал выключен
  "status_url":    "https://.../v1/upload-links/kZ2r...",
  "receipt_url":   "https://.../v1/upload-links/kZ2r.../file"
}

Оба партнёрских адреса известны сразу, ещё до того как плательщик приложит чек: интеграция сводится к одному адресу на заявку. Попытки загрузки ограничены (max_attempts) - когда они кончились, выпускайте новую ссылку.

GET/v1/upload-links/{token}

Состояние заявки одним запросом. Поле status: pending - ссылка живёт, done - проверка закончена, expired - срок вышел, а чек так и не пришёл, not_found - такой ссылки нет. Факт загрузки - отдельным полем file_receiveduploaded_at): по нему останавливайте свой таймер ожидания чека, не дожидаясь вердикта. Рядом едут attempts, expires_at, job_id, verdict, checking, eta_seconds, resend_deadline и resend_left_seconds (окно на повторный чек), а также status_url, receipt_url и share_url.

GET/v1/upload-links/{token}/file

Чек по адресу заявки: 202 со статусом waiting, пока плательщик не приложил файл, 202 processing, пока идёт проверка, 410, если срок ссылки вышел, а чек так и не пришёл; дальше - как у обычной ссылки на файл чека.

Обратная связь - итог ордера

POST/v1/receipts/{job_id}/feedback

Сообщите, чем закончился ордер на вашей стороне: это нужно для сверки качества и разбора спорных случаев. Обратная связь отправляется после вердикта - по задаче, о которой у нас есть записанное решение.

{"order_outcome": "approved", "comment": "деньги поступили"}
# order_outcome: approved | declined; comment - до 1000 символов, лишнее обрезаем

# Ответ: {"job_id": "...", "feedback": "recorded", "order_outcome": "approved"}
# 422 - order_outcome не approved и не declined
# 404 - по этой задаче у нас нет записанного решения

Служебные адреса

АдресЧто отвечает
GET /health{"status":"ok"} - сервис жив; ключ API не нужен
GET /readiness{"status":"ready"} - сервис поднят и принимает запросы; ключ API не нужен. Не готов - 503 и detail с причиной: придержите поток и повторите позже (для проверки живости есть /health)

Интерактивная схема API - все маршруты с полями, телами и кодами ответов; кнопка «Try it out» выполняет запросы в песочнице, а не в бою. Та же схема файлом: openapi.yaml - её понимают Swagger, Postman, Insomnia и генераторы клиентов, так что готовый код для своего языка получается без переписывания примеров руками. Файл собирается из кода при каждой сборке, поэтому не расходится с тем, что сервис делает на самом деле.

Ошибки и статусы HTTP

HTTPКогда
200статус задачи и состояние ссылки загрузки: разбирайте по полю status в теле, в том числе not_found
201ссылка загрузки создана
202чек принят и проверка идёт; тот же код у отказа приёма - смотрите тело
401нет ключа или ключ неверен
403доступ запрещён: IP не в списке разрешённых (проверяется раньше ключа)
404файл чека или решение не найдены, в том числе если задача чужая - чужие задачи выглядят как несуществующие. Опрос статуса задачи отвечает иначе: 200 и {"status":"not_found"}
409файла не будет: нужен новый чек, отказ или чек уже снят с хранения. В теле - verdict и reason_code
410срок ссылки загрузки вышел, а чек так и не пришёл
413запрос заведомо больше лимита; файл, превысивший лимит немного, отклоняется обычным ответом подачи с кодом FILE_TOO_LARGE
422тело запроса не по контракту: неверный order_outcome в обратной связи или неразбираемое тело при создании ссылки загрузки
429слишком много обращений к несуществующим ссылкам с одного адреса
503сервис временно не принимает: в теле "status": "busy" и "reason_code": "QUEUE_FULL", в заголовке Retry-After. Повторите запрос позже, соблюдая паузу

Форм тела ошибки три, разборщик должен знать все:

Машинные коды причин собраны в разделе «Вердикты и коды причин»: приём файла, нужен новый чек, наши технические обстоятельства.

Песочница

Открыть песочницу »

https://validator.testoporat.com/sandbox - макет API для отладки интеграции: те же адреса и те же имена полей. Подлинность файлов песочница не проверяет - вердикт выбирается по имени файла, чтобы каждый сценарий был воспроизводим:

Имя файла содержитВердиктreason_code
fakerejectedSANDBOX_MARKED_FAKE
resendresend_requestedSANDBOX_UNREADABLE
что угодно ещёacceptednull
# сценарий «подделка»: подаём файл, вердикт забираем по status_url через ~4 секунды
curl -s https://validator.testoporat.com/sandbox/v1/receipts:validate \
  -H "Authorization: Bearer sandbox-demo-key" \
  -F "file=@fake-check.jpg;filename=fake-check.jpg" \
  -F "request_id=TEST-FAKE-1"

Переход на боевой контур

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

ЧтоВ песочницеВ боевом контуре
Адресhttps://validator.testoporat.com/sandbox, открыт всемвыдаётся при подключении вместе с ключом; пути маршрутов те же
Ключлюбая строка от 8 символоввыданный вам ключ; за подключением может быть закреплён список разрешённых IP, с чужого адреса - 403
Поля заявкине проверяютсяexpected_amount, requisite и initiated_at обязательны все три, иначе CONTEXT_FIELDS_MISSING
Вердиктвыбирается по имени файла, подлинность не проверяетсянастоящая проверка; вердикт зависит от чека, а не от имени файла
Коды причинSANDBOX_* - существуют только тамкоды из таблиц этой страницы и закрытые коды признаков подделки
wait_secondsне поддерживаетсяработает: вердикт можно получить прямо в ответе подачи
Подача ссылкойфайл не скачивается, ответ строится по имени в ссылкефайл забирается по-настоящему, домен хранилища должен быть в согласованном списке
Адрес колбэковполе callback_url прямо в подаче - удобство отладкитакого поля нет: адрес задаётся один раз в кабинете партнёра
Подпись колбэкасчитается общим секретом песочницы и потому ничего не доказываетваш секрет подписи, выданный при подключении; проверка обязательна
Хранениев памяти около часа, ничего не сохраняетсяфайл шифруется, срок хранения - ваша настройка вплоть до «не хранить вовсе»
Метка ответазаголовок X-Sandbox: true в каждом ответетакого заголовка нет
Проверка одной командой Поиск по вашей боевой конфигурации не должен находить ни слова sandbox, ни ключей песочницы. Заголовок X-Sandbox годится и как предохранитель: если он пришёл на боевом ключе, значит запрос ушёл не туда - такую задачу лучше не проводить, а поднять тревогу.

Что сделать в день перехода: переключить адрес и ключ значениями настроек (без выката кода), прогнать на боевом контуре один настоящий чек и убедиться, что вердикт дошёл обоими путями - и по status_url, и колбэком с верной подписью. Дальше - чек-лист внедрения целиком.

Чек-лист внедрения

  1. Заявка полная: expected_amount, requisite и initiated_at уходят всегда - без любого из них боевой контур подачу отклоняет.
  2. Транспорт: путь приёма выбран и отработан в песочнице. Подача файлом - обычный multipart. Подача ссылкой согласована с нами заранее: домен вашего хранилища вносится в список разрешённых, иначе придёт FETCH_DISABLED или DOMAIN_NOT_ALLOWED. Путь плательщика - решено, где показываете upload_url (страница, рамка или письмо), а срок жизни ссылки и число попыток выставлены в кабинете под ваши сделки.
  3. request_id передаётся всегда; ретраи по обрыву сети не плодят у вас дублей (проверьте обработку "repeated": true).
  4. Все четыре вердикта обработаны, а у accepted разобран и status_color: жёлтый - это расхождение с заявкой, а не «всё сошлось». На resend_requested плательщик видит просьбу прислать чек заново, а не «отказ».
  5. Колбэки: адрес зарегистрирован в кабинете, секрет подписи получен и хранится в секретах, подпись проверяется по сырому телу, повторная доставка события безопасна, ответ быстрый.
  6. Список ваших исходящих IP согласован с нами, срок хранения чеков выбран.
  7. Падение и недоступность Валидатора не останавливают ваши сделки: таймауты и повторы настроены.
  8. Обратная связь по итогу ордера отправляется после вердикта.
  9. Ключ хранится в секретах, не в коде; на смену ключа ваша система способна без выката.

Безопасность и данные