Валидатор API
Проверка подлинности чеков об оплате. Вы отдаёте чек и данные заявки - получаете один статус и одно действие: проводить платёж, посмотреть руками или попросить новый чек.
https://chekvalidator.com/sandbox:
ключ - любая строка от 8 символов, договор не нужен. Боевой адрес и ключ выдаются при
подключении; пути, поля и ответы те же.
Как это работает
Путей приёма чека два, дальше они сходятся: та же проверка, те же статусы, те же вебхуки.
Путь 1, основной - чек прикладывает плательщик
Файл к вам не попадает вовсе: своя форма загрузки и хранилище чужих чеков не нужны.
- Вы заводите заявку запросом
POST /v1/upload-linksс полями заявки. В ответе сразу все адреса:upload_url- страница для плательщика,embed_url- она же рамкой на вашем сайте,email_address- адрес для письма из банка, а для васstatus_urlиreceipt_url. - Плательщик открывает страницу (
GET /u/{token}) и прикладывает фото, снимок экрана или PDF. Ключ API ему не нужен: пропуск - токен ссылки со сроком и лимитом попыток. - Судьба заявки - одним адресом
GET /v1/upload-links/{token}:file_received- чек приложен (останавливайте свой таймер),checkingиeta_seconds- идёт проверка,job_idиverdict- готово. Не подошёл чек - плательщик шлёт новый по той же ссылке, счёт виден вattemptsиresend_left_seconds.
Путь 2 - чек отдаёте вы
Файл уже у вас: из вашей формы, от другой системы или из архива.
- Вы отдаёте чек запросом
POST /v1/receipts:validate: файл (или временная ссылка на него) плюс поля заявки. Ответ приходит сразу, проверка идёт в фоне. - Вердикт приходит вебхуком или забирается запросом
GET /v1/receipts/{job_id}. Нужен ответ одним запросом - добавьтеwait_seconds.
| Ваш случай | Что взять |
|---|---|
| Своей формы для файла нет: плательщик в мессенджере, у оператора, в чате поддержки | Ссылка загрузки - путь 1 |
| Форма есть, файл приходит к вам | Подача файлом - путь 2 |
| Чек лежит в вашем хранилище | Подача ссылкой - файл заберём сами |
| Нужен синхронный фильтр | Поле wait_seconds: PDF решается за доли секунды прямо в ответе подачи |
| Сделки не должны вставать, если мы недоступны | Резервный блок из пакета подключения: принимает чек в вашу систему и досылает нам позже |
Быстрый старт
Три запроса в песочницу: подать чек, забрать вердикт, сообщить итог ордера. Язык примеров переключается над блоком кода - выбор запоминается на всей странице.
# 1. Подать чек (файлом)
curl -s https://chekvalidator.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://chekvalidator.com/sandbox/v1/receipts/<job_id> \
-H "Authorization: Bearer sandbox-demo-key"
# Ответ: {"status":"done","verdict":"accepted","status_color":"green",...}
# 3. Сообщить итог ордера
curl -s https://chekvalidator.com/sandbox/v1/receipts/<job_id>/feedback \
-H "Authorization: Bearer sandbox-demo-key" \
-H "Content-Type: application/json" \
-d '{"order_outcome":"approved","comment":"деньги пришли"}'
import requests
# 1. Подать чек (файлом)
r = requests.post(
"https://chekvalidator.com/sandbox/v1/receipts:validate",
headers={
"Authorization": "Bearer sandbox-demo-key"
},
files={"file": open("check.jpg", "rb")},
data={
"request_id": "ORDER-1001",
"expected_amount": "1500.00",
"requisite": "79001234567",
"initiated_at": "2026-08-23T10:15:00"
},
)
print(r.status_code, r.json())
job_id = r.json()["job_id"]
# Ответ: {"job_id":"...","status":"pending","eta_seconds":3.5,"status_url":"...",...}
# 2. Забрать результат (через пару секунд)
r = requests.get(
f"https://chekvalidator.com/sandbox/v1/receipts/{job_id}",
headers={
"Authorization": "Bearer sandbox-demo-key"
},
)
print(r.status_code, r.json())
# Ответ: {"status":"done","verdict":"accepted","status_color":"green",...}
# 3. Сообщить итог ордера
r = requests.post(
f"https://chekvalidator.com/sandbox/v1/receipts/{job_id}/feedback",
headers={
"Authorization": "Bearer sandbox-demo-key"
},
json={
"order_outcome": "approved",
"comment": "деньги пришли"
},
)
print(r.status_code, r.json())
<?php
// 1. Подать чек (файлом)
$ch = curl_init("https://chekvalidator.com/sandbox/v1/receipts:validate");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer sandbox-demo-key",
],
CURLOPT_POSTFIELDS => [
"file" => new CURLFile("check.jpg"),
"request_id" => "ORDER-1001",
"expected_amount" => "1500.00",
"requisite" => "79001234567",
"initiated_at" => "2026-08-23T10:15:00",
],
CURLOPT_RETURNTRANSFER => true,
]);
$data = json_decode(curl_exec($ch), true);
$job_id = $data["job_id"];
// Ответ: {"job_id":"...","status":"pending","eta_seconds":3.5,"status_url":"...",...}
// 2. Забрать результат (через пару секунд)
$ch = curl_init("https://chekvalidator.com/sandbox/v1/receipts/{$job_id}");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => [
"Authorization: Bearer sandbox-demo-key",
],
CURLOPT_RETURNTRANSFER => true,
]);
$data = json_decode(curl_exec($ch), true);
// Ответ: {"status":"done","verdict":"accepted","status_color":"green",...}
// 3. Сообщить итог ордера
$ch = curl_init("https://chekvalidator.com/sandbox/v1/receipts/{$job_id}/feedback");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer sandbox-demo-key",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"order_outcome" => "approved",
"comment" => "деньги пришли",
], JSON_UNESCAPED_UNICODE),
CURLOPT_RETURNTRANSFER => true,
]);
$data = json_decode(curl_exec($ch), true);
import { readFile } from "node:fs/promises";
let res, data;
// 1. Подать чек (файлом)
const form = new FormData();
form.append("file", new Blob([await readFile("check.jpg")]), "check.jpg");
form.append("request_id", "ORDER-1001");
form.append("expected_amount", "1500.00");
form.append("requisite", "79001234567");
form.append("initiated_at", "2026-08-23T10:15:00");
res = await fetch("https://chekvalidator.com/sandbox/v1/receipts:validate", {
method: "POST",
headers: {"Authorization": "Bearer sandbox-demo-key"},
body: form,
});
data = await res.json();
console.log(res.status, data);
const jobId = data.job_id;
// Ответ: {"job_id":"...","status":"pending","eta_seconds":3.5,"status_url":"...",...}
// 2. Забрать результат (через пару секунд)
res = await fetch(`https://chekvalidator.com/sandbox/v1/receipts/${jobId}`, {
method: "GET",
headers: {"Authorization": "Bearer sandbox-demo-key"},
});
data = await res.json();
console.log(res.status, data);
// Ответ: {"status":"done","verdict":"accepted","status_color":"green",...}
// 3. Сообщить итог ордера
res = await fetch(`https://chekvalidator.com/sandbox/v1/receipts/${jobId}/feedback`, {
method: "POST",
headers: {"Authorization": "Bearer sandbox-demo-key", "Content-Type": "application/json"},
body: JSON.stringify({
"order_outcome": "approved",
"comment": "деньги пришли"
}),
});
data = await res.json();
console.log(res.status, data);
Доступ и ключи
Ключ передаётся любым из двух заголовков; если пришли оба, читается X-API-Key:
Authorization: Bearer ВАШ_КЛЮЧ
# или
X-API-Key: ВАШ_КЛЮЧ
- нет ключа или ключ неверен -
401; - за подключением закрепляется список разрешённых IP, запрос с чужого адреса -
403; - ключ перевыпускаем по вашему запросу, старый перестаёт работать сразу. Храните ключ в секретах и меняйте без выката кода;
- секрет подписи вебхуков меняется в кабинете партнёра самостоятельно.
Подача чека
Отвечает сразу 202: задача создана, проверка идёт в фоне. Тот же
202 приходит и при отказе приёма - тогда в теле "status": "done",
"verdict": "rejected" и код причины. Решение принимайте по телу, а не по коду HTTP.
Файлом (multipart/form-data)
Файл - в поле file, остальные поля - строками рядом.
curl -s https://chekvalidator.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}"
import requests
r = requests.post(
"https://chekvalidator.com/sandbox/v1/receipts:validate",
headers={
"Authorization": "Bearer sandbox-demo-key"
},
files={"file": open("check.jpg", "rb")},
data={
"request_id": "ORDER-1001",
"expected_amount": "1500.00",
"requisite": "79001234567",
"initiated_at": "2026-08-23T10:15:00",
"partner_meta": "{\"order\":1001}"
},
)
print(r.status_code, r.json())
<?php
$ch = curl_init("https://chekvalidator.com/sandbox/v1/receipts:validate");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer sandbox-demo-key",
],
CURLOPT_POSTFIELDS => [
"file" => new CURLFile("check.jpg"),
"request_id" => "ORDER-1001",
"expected_amount" => "1500.00",
"requisite" => "79001234567",
"initiated_at" => "2026-08-23T10:15:00",
"partner_meta" => "{\"order\":1001}",
],
CURLOPT_RETURNTRANSFER => true,
]);
$data = json_decode(curl_exec($ch), true);
import { readFile } from "node:fs/promises";
let res, data;
const form = new FormData();
form.append("file", new Blob([await readFile("check.jpg")]), "check.jpg");
form.append("request_id", "ORDER-1001");
form.append("expected_amount", "1500.00");
form.append("requisite", "79001234567");
form.append("initiated_at", "2026-08-23T10:15:00");
form.append("partner_meta", "{\"order\":1001}");
res = await fetch("https://chekvalidator.com/sandbox/v1/receipts:validate", {
method: "POST",
headers: {"Authorization": "Bearer sandbox-demo-key"},
body: form,
});
data = await res.json();
console.log(res.status, data);
Ссылкой (application/json)
Чек лежит в вашем хранилище - передайте временную ссылку, файл заберём сами.
curl -s https://chekvalidator.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"
}'
import requests
r = requests.post(
"https://chekvalidator.com/sandbox/v1/receipts:validate",
headers={
"Authorization": "Bearer sandbox-demo-key"
},
json={
"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"
},
)
print(r.status_code, r.json())
<?php
$ch = curl_init("https://chekvalidator.com/sandbox/v1/receipts:validate");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer sandbox-demo-key",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"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",
], JSON_UNESCAPED_UNICODE),
CURLOPT_RETURNTRANSFER => true,
]);
$data = json_decode(curl_exec($ch), true);
let res, data;
res = await fetch("https://chekvalidator.com/sandbox/v1/receipts:validate", {
method: "POST",
headers: {"Authorization": "Bearer sandbox-demo-key", "Content-Type": "application/json"},
body: JSON.stringify({
"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"
}),
});
data = await res.json();
console.log(res.status, data);
https, домен из списка, согласованного при подключении, внутренние сети и
редиректы запрещены. Пока домен не согласован, приём по ссылке выключен:
FETCH_DISABLED. Остальные отказы - в кодах приёма.
Песочница по ссылке файл не скачивает, а строит ответ по имени файла в ней.
Поля заявки
| Поле | Обязательное | Что это |
|---|---|---|
file / file_url | да | чек: PDF, JPEG, PNG, WebP, GIF или HEIC. Список можно сузить в кабинете, лишний формат получит FORMAT_NOT_ALLOWED |
expected_amount | да | ожидаемая сумма строкой: "1500.00" или "1500,00" |
requisite | да | реквизит получателя, выданный плательщику: карта, счёт или телефон, как есть |
initiated_at | да | когда создана заявка, ISO 8601; без смещения - время сервиса |
request_id | настоятельно | ID заявки у вас: едет во все ответы и вебхуки, включает защиту от дублей |
wait_seconds | нет | ждать вердикт в ответе подачи, 0-25 секунд. Не успели - обычный pending |
currency | нет | валюта заявки |
bank | нет | банк, если известен заранее |
file_name | нет | имя для журналов, до 80 символов |
subsystem | нет | какая ваша система подаёт чек, см. подсистемы |
partner_meta | нет | ваше сквозное поле до 4096 символов: вернётся как есть в ответах и вебхуках. Считайте его недоверенным вводом |
Без любого из трёх обязательных полей подача отклоняется кодом
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/<токен>"
}
eta_seconds - через сколько ждать вердикт, status_url - где его
забирать, receipt_url - файл чека по ключу, share_url -
карточка проверки без ключа. Отказы по самому файлу при подаче файлом
(UNSUPPORTED_FORMAT, FILE_TOO_LARGE, IMAGE_TOO_LARGE,
CONTENT_TYPE_MISMATCH) приходят не здесь, а вердиктом задачи.
Ссылки загрузки
Заявка, по которой чек прикладывает сам плательщик. Поля заявки те же и так же обязательны;
ttl_minutes - срок жизни ссылки, по умолчанию час, не больше суток.
curl -s https://chekvalidator.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"
}
import requests
r = requests.post(
"https://chekvalidator.com/sandbox/v1/upload-links",
headers={
"Authorization": "Bearer sandbox-demo-key"
},
json={
"request_id": "ORDER-1002",
"expected_amount": "900.00",
"requisite": "79007654321",
"initiated_at": "2026-08-23T12:00:00",
"ttl_minutes": 60
},
)
print(r.status_code, r.json())
# Ответ 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"
# }
<?php
$ch = curl_init("https://chekvalidator.com/sandbox/v1/upload-links");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer sandbox-demo-key",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"request_id" => "ORDER-1002",
"expected_amount" => "900.00",
"requisite" => "79007654321",
"initiated_at" => "2026-08-23T12:00:00",
"ttl_minutes" => 60,
], JSON_UNESCAPED_UNICODE),
CURLOPT_RETURNTRANSFER => true,
]);
$data = json_decode(curl_exec($ch), true);
// Ответ 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"
// }
let res, data;
res = await fetch("https://chekvalidator.com/sandbox/v1/upload-links", {
method: "POST",
headers: {"Authorization": "Bearer sandbox-demo-key", "Content-Type": "application/json"},
body: JSON.stringify({
"request_id": "ORDER-1002",
"expected_amount": "900.00",
"requisite": "79007654321",
"initiated_at": "2026-08-23T12:00:00",
"ttl_minutes": 60
}),
});
data = await res.json();
console.log(res.status, data);
// Ответ 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) - выпускайте новую ссылку.
Состояние заявки. status: pending - ссылка ждёт чек,
done - проверка закончена, expired - срок вышел без чека,
not_found - ссылки нет. Рядом: file_received, uploaded_at,
attempts, expires_at, job_id, verdict,
checking, eta_seconds, resend_deadline,
resend_left_seconds и адреса status_url, receipt_url,
share_url.
Чек по адресу заявки: 202 и waiting, пока чека нет, 202
и processing, пока идёт проверка, 410 - срок вышел без чека; дальше
как у файла чека.
Повторные подачи
Повтор той же подачи с тем же request_id не создаёт новую задачу: вернётся та
же job_id с "repeated": true и тем же 202 - ретраи по
обрыву сети безопасны. Память о заявке держится сутки. Без request_id защиты
от дублей нет.
{"request_id":"ORDER-1001","job_id":"3f8a1c2e-...","status":"done",
"repeated":true,"verdict":"accepted",...}
Результат проверки
Ответ всегда 200, разбирайте по полю status:
processing - ждите, done - готово, not_found - задачи
нет, она чужая или старше суток. Результат доступен сутки, файл - столько, сколько выбрано в
настройке хранения.
{
"job_id": "3f8a1c2e-...",
"request_id": "ORDER-1001",
"status": "done",
"status_key": "chist",
"status_text": "Чист",
"status_action": "Проводить платёж.",
"status_explain": "Чек прошёл проверку: подделки не нашли, данные сошлись с заявкой.",
"status_hex": "#1e8e5a",
"warning": null,
"file_delivery": "передаём",
"file_delivery_reason": "",
"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}"
}
| Поле | Что это |
|---|---|
status_key | Главное поле: один статус - одно действие, см. статусы |
status_text, status_action, status_explain | статус словом, что делать и что именно не так - готовые фразы для человека |
warning | предупреждение одной строкой; null только у чистого статуса |
status_hex | цвет статуса для интерфейса, показывайте вместе со словом |
file_delivery, file_delivery_reason | передаём ли файл чека: передаём | передаём обрезанный | не передаём, и почему |
verdict, status_color, reason_code | прежняя форма ответа, работает как раньше, см. таблицу соответствия |
mismatches | что разошлось с заявкой, словами; непуст при status_color: yellow |
comment, points | пояснение и пункты разбора для оператора; состав points не фиксирован |
survey | короткая анкета, только при not_confirmed; ответ - POST /v1/receipts/{job_id}/survey, необязателен |
file_available, file_cropped | отдадим ли файл по receipt_url; вырезан ли чек из кадра |
extracted | что распознали: сумма, время оплаты, банк, вид перевода. Приходит, если что-то извлечено |
partner_meta | ваше сквозное поле, как прислали |
Остальные служебные поля (например, authenticity) могут меняться - решение
принимайте по полям выше.
verdict: "rejected",
reason_code, stage: "intake" и detail, но
status_key равен prishlite_zanovo: чек мы не смотрели, беда в файле
или запросе. Показывать это плательщику как подделку нельзя.
Статусы и коды причин
Разбирайте ответ по status_key: он есть у каждой завершённой проверки, и у
каждого значения ровно одно действие.
| Статус | Что значит | Что делать |
|---|---|---|
chist «Чист» | чек настоящий, данные сошлись с заявкой | проводить платёж |
rashozhdenie «Расхождение» | чек выглядит настоящим, но разошёлся с заявкой; что именно - в status_explain и mismatches | не проводить автоматом, посмотреть руками |
povtor «Повтор» | файл уже предъявлялся по другой заявке | не проводить; если та заявка сорвалась - сообщите итогом ордера, блокировка снимется |
ne_podtverzhdyon «Не подтверждён» | подтвердить нечем: банк нам незнаком или проверка не завершилась по нашей причине. Признаков подделки нет, файл передаём | решать вам; при незнакомом банке в ответе анкета survey |
prishlite_zanovo «Пришлите заново» | файл не читается, чека в нём не видно, или он не прошёл приём | попросить новый чек; попытка не тратится |
otkaz «Отказ» | доказанная подделка или грубое нарушение | не проводить платёж |
Если заключения нет вовсе, приходит neyasno.
Прежняя форма: verdict и status_color
Код, написанный по старым полям, работает как раньше. В прежней форме пять вердиктов:
| Вердикт | Цвет | Что делать | Файл чека |
|---|---|---|---|
| accepted | green - сошлось; yellow - подделки нет, но данные разошлись с заявкой (mismatches) | green - проводить; yellow - решение за вами, обычно ручная проверка | есть |
| resend_requested | resend | попросить новый чек, comment можно показать плательщику как есть; это не отказ | нет, 409 |
| rejected | red | по вашему регламенту подозрительных операций; при отказе приёма код из таблицы приёма | нет, 409 |
| not_confirmed | unconfirmed | банк нам незнаком, подделки не нашли - решение за вами | есть |
| unverified | gray | заключения нет по нашей технической причине - сделка стоять не должна | обычно есть, см. file_available |
accepted принимают за зелёный и пропускают расхождение (у него
status_key = rashozhdenie). resend_requested показывают
плательщику как отказ. Ветку unverified не пишут вовсе - и сделка встаёт из-за
нашего сбоя.
Коды приёма файла
Приходят с verdict: rejected и status_key: prishlite_zanovo.
| 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 | нет file_url или в нём не разобрать хост | проверьте ссылку |
URL_SCHEME_NOT_ALLOWED | ссылка не по https | только https |
DOMAIN_NOT_ALLOWED | домен не из согласованного списка | согласуйте домен хранилища |
DNS_RESOLUTION_FAILED | домен ссылки не резолвится | проверьте доступность хранилища снаружи |
URL_RESOLVES_TO_INTERNAL_IP | ссылка ведёт во внутреннюю сеть | нужно публичное хранилище |
FETCH_DISABLED | приём по ссылке не включён | подавайте файлом или попросите включить |
FETCH_FAILED | хранилище не отдало файл | повторите, когда хранилище ответит |
Коды «нужен новый чек»
Приходят с verdict: resend_requested; comment готов для плательщика.
| reason_code | Что случилось |
|---|---|
NOT_A_RECEIPT | файл читается, но это не чек |
LOW_EXTRACTION_CONFIDENCE | чек не читается |
AMOUNT_NOT_FOUND | сумма в чеке не распозналась |
FOREIGN_BACKGROUND | в кадре обстановка вокруг чека, сам чек не выделить |
FOREIGN_INTERFACE_VISIBLE | в кадре постороннее приложение |
Коды наших технических обстоятельств
| reason_code | Что случилось | Что делать |
|---|---|---|
QUEUE_FULL | чек не взят, ответ 503 сразу | повторите после паузы из Retry-After |
PIPELINE_ERROR | проверка упала из-за нашего сбоя | подайте чек ещё раз |
CHECK_UNAVAILABLE | заключения нет, чек передан (unverified) | решайте по регламенту, файл доступен |
CHECK_STALLED | проверка затянулась и закрыта | то же |
CHECK_INTERRUPTED | перезапуск сервиса оборвал проверку | то же |
Коды признаков подделки при rejected приходят, но открыто не расшифровываются:
открытый список проверок был бы инструкцией для изготовителей подделок. Справочник входит в
пакет подключения.
Файл чека
Файл по ключу API. Пока проверка идёт - 202 и processing; если она
затянулась, файл может прийти раньше вердикта с заголовком X-Validator-Verdict: pending.
Файла не будет (нужен новый чек, отказ, снят с хранения) - 409 с вердиктом и
кодом; чужая задача - 404.
Снять чек с хранения досрочно. Ответ: {"job_id": "...", "stored": false, "deleted": true}.
Карточка без ключа - share_url вида /r/<токен>: в браузере
это статус, заключение и сам чек, программе по тому же адресу приходит файл. Удобно для карточки
операции в вашей админке. Ссылка равноценна документу: храните как секрет и не отдавайте
плательщику. Живёт, пока чек на хранении. По настройке файл отдаётся очищенным от служебных
полей (EXIF, GPS, метаданные PDF).
Вебхуки
Вердикт приходит сам на ваш адрес. Адрес задаётся в кабинете партнёра (только
https и публичный адрес), секрет подписи выдаётся при подключении и меняется
в кабинете.
| Событие | Когда |
|---|---|
receipt.accepted | файл принят, проверка пошла - переводите сделку в ожидание |
receipt.verdict | проверка закончена: status_key, status_text, status_action, warning, file_delivery и прежние verdict, status_color, reason_code, comment, points, file_available. Расхождения и распознанное - по status_url |
receipt.delayed | проверка затянулась; доступен ли уже файл - в file_available, вердикт придёт отдельно |
receipt.late | только для подсистем, которые не ждут: пригодный чек пришёл после финала |
# тело события: имя дублируется полем 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 от сырого тела, ключ - ваш
секрет, шестнадцатеричная строка в нижнем регистре. Считайте подпись до разбора JSON:
пересобранное тело даёт другие байты. Нет заголовка или не сошлась - событие чужое.
# Python: проверка подписи
import hmac, hashlib
def подпись_верна(тело: bytes, заголовок: str, секрет: str) -> bool:
ожидаемая = hmac.new(секрет.encode(), тело, hashlib.sha256).hexdigest()
return hmac.compare_digest(ожидаемая, заголовок)
<?php
// PHP: проверка подписи. $body - сырое тело: file_get_contents("php://input")
function signatureValid(string $body, string $header, string $secret): bool
{
$expected = hash_hmac("sha256", $body, $secret);
return hash_equals($expected, $header);
}
// Node.js: проверка подписи. body - Buffer сырого тела, до JSON.parse
import { createHmac, timingSafeEqual } from "node:crypto";
export function signatureValid(body, header, secret) {
const expected = Buffer.from(createHmac("sha256", secret).update(body).digest("hex"));
const received = Buffer.from(String(header || ""));
return expected.length === received.length && timingSafeEqual(expected, received);
}
Доставка
- «Как минимум один раз»: сразу три попытки, затем очередь досылки через полминуты, 2, 10 и 30 минут, 2, 6 и 12 часов - в пределах суток. Очередь переживает перезапуски с обеих сторон.
- Повторы идут с тем же
X-Validator-Event-Id: обрабатывайте идемпотентно и не откатывайте уже принятый вердикт. - Успех - только ответ
2xx: отвечайте сразу, тяжёлую работу уводите в фон. - Событий нет, если подача отклонена на приёме, получила
503или это повтор ("repeated": true). Приwait_secondsвердикт придёт и в ответе, и вебхуком. - Окно кончилось или вебхук не дошёл - вердикт всегда можно забрать по
status_url.
Приёмник по шагам
- Прочитать сырое тело байтами и проверить подпись.
X-Validator-Event-Idуже видели - ответить2xxи выйти.- Разобрать тело:
event,job_id,request_id,partner_meta. - Записать событие и сразу ответить
2xx. - В фоне применить вердикт к сделке.
Итог ордера
Чем закончился ордер у вас - после вердикта, по любому статусу. Нужно для разбора спорных случаев и снятия блокировки «Повтор».
{"order_outcome": "approved", "comment": "деньги поступили"}
# order_outcome: approved | declined; comment - до 1000 символов, лишнее обрезаем
# Ответ: {"job_id": "...", "feedback": "recorded", "order_outcome": "approved"}
# 422 - order_outcome не approved и не declined
# 404 - по этой задаче у нас нет записанного решения
Ошибки HTTP
| HTTP | Когда |
|---|---|
200 | статус задачи и состояние ссылки загрузки - разбирайте по status, включая not_found |
201 | ссылка загрузки создана |
202 | чек принят, проверка идёт; тот же код у отказа приёма - смотрите тело |
401 | нет ключа или ключ неверен |
403 | IP не в списке разрешённых (проверяется раньше ключа) |
404 | файл или решение не найдены, в том числе чужая задача |
409 | файла не будет: нужен новый чек, отказ или чек снят с хранения; в теле verdict и reason_code |
410 | срок ссылки загрузки вышел без чека |
413 | запрос заведомо больше лимита |
422 | тело не по контракту: неверный order_outcome или неразбираемое тело ссылки загрузки |
429 | слишком много обращений к несуществующим ссылкам с одного адреса |
503 | временно не принимаем: "status": "busy", "reason_code": "QUEUE_FULL" и заголовок Retry-After |
Формы тела ошибки: {"detail": "..."} у 401 и 403;
{"error": "..."} у 422 и 404 итога ордера; обычный ответ
подачи с verdict, reason_code, stage и detail
у отказов приёма.
Служебные адреса и схема
| Адрес | Что отвечает |
|---|---|
GET /health | {"status":"ok"} - сервис жив; ключ не нужен |
GET /readiness | {"status":"ready"} - принимает запросы; ключ не нужен. Не готов - 503 и причина в detail |
Машинная схема всех маршрутов - openapi.yaml: её понимают Postman, Insomnia и генераторы клиентов. Собирается из кода, поэтому не расходится с сервисом. Та же схема в виде страницы Swagger с запросами в песочницу.
Подсистемы
Если за одним ключом стоят разные ваши системы (касса, бот, ручной разбор) и они
по-разному умеют ждать, заведите их в кабинете («Интеграция» → «Подсистемы») и присылайте
идентификатор полем subsystem - при подаче и при создании ссылки загрузки.
- латиница, цифры, дефис, подчёркивание, до 32 знаков; поле необязательное;
- незнакомый идентификатор подачу не рушит: чек идёт по умолчанию, в ответе
subsystem_unknown: true; - подсистема, которая не умеет ждать, промежуточного «пришлите заново» не получает: придёт
один финальный вебхук - итог по новому чеку или
ne_podtverzhdyon, когда вышел её предел ожидания. Это не отказ; - в вебхуке оба статуса:
subsystem_statusи полныйfull_status_key; изменения настроек действуют на новые подачи.
Три срока - не перепутайте
| Срок | Про что | Кто задаёт |
|---|---|---|
| Срок оплаты ордера | деньги: оплата позже срока - «Расхождение», не отказ | вы, на подключении или подсистеме |
| Окно повторной загрузки | файл: сколько плательщик может слать новый снимок | мы, по умолчанию 10 минут |
| Предел ожидания финала | вебхук: сколько ваша система ждёт окончательного ответа | вы, на подсистеме |
Песочница
https://chekvalidator.com/sandbox - открытый двойник API для отладки: те же
адреса, поля и строгость приёма, но подлинность не проверяется - вердикт выбирается по имени
файла. Попробовать мышкой - в быстром старте.
| Имя файла содержит | Вердикт | reason_code |
|---|---|---|
fake | rejected | SANDBOX_MARKED_FAKE |
resend | resend_requested | SANDBOX_UNREADABLE |
| что угодно ещё | accepted | null |
- ключ - любая строка от 8 символов; ответы помечены заголовком
X-Sandbox: true; - вердикт созревает около 4 секунд: сначала
processing, потомdone; - вебхуки включаются полем
callback_urlпрямо в подаче - только здесь; - данные живут около часа в памяти; настоящие чеки сюда не загружайте.
# сценарий «подделка»: подаём файл, вердикт забираем по status_url через ~4 секунды
curl -s https://chekvalidator.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" \
-F "expected_amount=1500.00" \
-F "requisite=79001234567" \
-F "initiated_at=2026-09-01T10:15:00"
Переход в бой
В коде меняются две настройки - адрес и ключ. Приём одинаковый, различия осознанные:
| Что | Песочница | Бой |
|---|---|---|
| Адрес и ключ | https://chekvalidator.com/sandbox, любой ключ от 8 символов | выдаются при подключении; может быть закреплён список IP |
| Поля заявки | обязательны так же, как в бою: CONTEXT_FIELDS_MISSING, CONTEXT_FIELDS_INVALID | обязательны все три |
| Вердикт | по имени файла, коды SANDBOX_* | настоящая проверка |
wait_seconds | до 25 секунд, только для PDF | так же; фото сразу получает pending |
| Проверка файла | только размер | плюс пиксели, тип содержимого и форматы подключения |
| Подача ссылкой | файл не скачивается | скачивается, со всеми ограничениями ссылок |
| Вебхуки | callback_url в подаче, общий секрет - подпись ничего не доказывает | адрес в кабинете, ваш секрет, проверка обязательна |
| Ссылки загрузки | срок и попытки как в бою, письмом - нет | плюс приём письмом, если канал настроен |
| Хранение | около часа в памяти | файл шифруется, срок - ваша настройка |
sandbox. Заголовок
X-Sandbox на боевом ключе значит, что запрос ушёл не туда: такую задачу не
проводите.
Чек-лист перед выкатом
- В каждой подаче есть
expected_amount,requisite,initiated_atиrequest_id; повтор с"repeated": trueне плодит дублей. - На каждый
status_keyесть ветка; жёлтыйacceptedне считается зелёным; «Пришлите заново» плательщик видит как просьбу, а не отказ. - Вебхуки: адрес в кабинете, секрет в секретах, подпись по сырому телу, повтор события безопасен, ответ быстрый.
- Подача ссылкой - домен хранилища согласован; путь плательщика - решено, где показываете
upload_url. - IP согласованы, срок хранения выбран, ключ в секретах и меняется без выката.
- Наша недоступность не останавливает сделки: таймауты и повторы настроены.
- Итог ордера уходит автоматически. В день перехода - один настоящий чек, вердикт пришёл и
по
status_url, и вебхуком с верной подписью.
Безопасность и данные
- Каждый файл чека шифруется при записи (AES-256-GCM); ключ живёт отдельно от данных и в резервные копии не попадает.
- Срок хранения файла - ваша настройка, от «не хранить» до бессрочно; снять чек досрочно -
DELETE /v1/receipts/{job_id}/file. Рабочая запись задачи живёт около суток зашифрованной; запись о проверке без файла остаётся в журнале решений для разбора споров. - Чеки боевых подключений хранятся в закрытом зашифрованном хранилище для разбора спорных случаев и повышения качества; тестовые подключения туда не попадают.
- Распознавание - внутри сервиса: во внешние нейросети и чужие API чек не уходит.
- Чужая задача для вашего ключа неотличима от несуществующей;
share_url- как сам документ. - Загрузка по ссылкам ограждена от SSRF; вебхуки подписаны HMAC-SHA256.
Изменения API
Изменения обратно совместимы: поля и значения только добавляются, старые сохраняют смысл.
| Дата | Что изменилось |
|---|---|
| 01.09.2026 | Главный статус ответа: status_key, status_text, status_action, warning, file_delivery. Вердикт not_confirmed и анкета survey. Поле subsystem и событие receipt.late. Песочница требует поля заявки так же, как бой |
| 29.08.2026 | Выбор принимаемых форматов в кабинете и код FORMAT_NOT_ALLOWED; HEIC принимается и переводится в JPEG |
| 06.08.2026 | Событие receipt.delayed, заголовок X-Validator-Verdict: pending, вердикт unverified |