API чеков НПД для ЮKassa и Telegram-ботов

Передавайте название услуги и данные оплаченного заказа, получайте статус и ссылку на чек через HTTP API v1. О готовом чеке можно узнать опросом API или через подписанное уведомление. Действующий магазин ЮKassa остаётся вашим.

Это API сервиса НПД Чек. Для регистрации дохода агент использует сессию веб-кабинета «Мой налог». Мы не используем официальный партнёрский API ФНС. Как устроено подключение.


Базовый адрес: https://npdcheck.ru/api/v1

Подключение


Это отдельный ключ с префиксом npdc_int_, не PAYMENT_PROVIDER_TOKEN, не токен Telegram-бота и не AGENT_TOKEN. Ключ показывается один раз.
После включения API неопубликованные чеки магазина ждут данные заказа от бота. Это относится к магазину целиком, а не только одному виду товара. Вебхуки payment.succeeded и refund.succeeded в ЮKassa оставьте включёнными. Отзыв ключа не снимает ожидание данных, иначе мог бы выдаться чек с неверным названием. Для продолжения выпустите новый ключ и обновите Secret.
Ключ имеет только права передачи заказа/чтения его чека. Не позволяет выпускать чек напрямую, менять магазин, вызывать агента или оформлять возврат. Включение API не включает сам агент и не продлевает подписку.

Авторизация


Все методы v1 требуют HTTP-заголовок:
Authorization: Bearer <NPDCHECK_INTEGRATION_KEY>
Content-Type: application/json

Отправка только с сервера/Worker. Не помещайте ключ в Mini App, URL, параметры Telegram invoice, клиентский JavaScript или логи.

Проверка подключения


GET /status
{"api_version":"1","provider":"yookassa","shop_connected":true,"subscription_active":true}

Передача оплаченного заказа


POST /orders
{
  "provider": "yookassa",
  "external_id": "order:42",
  "payment_id": "идентификатор-платежа-ЮKassa",
  "description": "Персональная тренировка",
  "amount": "1500.00",
  "currency": "RUB"
}

допустимы буквы, цифры и _.:-, первый символ буква или цифра. Не telegram_payment_charge_id и не payload счёта. строк/управляющих символов. Передавайте полное название из заказа, а не усечённый до32символов заголовок invoice. Более длинное название согласованно сократите в боте до отправки; API не обрезает его молча. с подтверждённым платежом ЮKassa. Валюта RUB. Комиссию не вычитать.
НПД Чек сам получает платёж через ключи подключённого магазина ЮKassa, проверяет ID, успешную оплату, валюту и сумму. Время оплаты берётся у ЮKassa. Данные покупателя/Telegram chat_id/email передавать не нужно.
Первый успешный запрос201, точный повтор200. Оба возвращают:
{
  "created": true,
  "order": {
    "external_id": "order:42",
    "provider": "yookassa",
    "payment_id": "идентификатор-платежа-ЮKassa",
    "description": "Персональная тренировка",
    "amount": "1500.00",
    "currency": "RUB",
    "status": "queued",
    "ready": false,
    "receipt_url": null,
    "error": null
  }
}

created означает создание привязки заказа, а не готовность чека. Заказы разрешено передавать при истёкшем доступе, но выдача будет ждать продления. Один платёж связан с одним external_id. Повтор с другими полями409; переименование принятого заказа/создание второго чека через новый ID запрещено.

Статус и ссылка на чек


GET /orders/{external_id} — предпочтительный метод, например /orders/order%3A42. Альтернатива: GET /receipts/{payment_id}. Ответ обоих: {"order":{...}} с теми же полями.
Опрос обычно раз в15–30секунд с увеличением интервала при длительном ожидании. Отправляйте покупателю receipt_url только при status=registered и ready=true. Используйте URL целиком. Ссылка должна вести по HTTPS ровно на lknpd.nalog.ru. Бот самостоятельно сопоставляет external_id с покупателем и отправляет сообщение через Telegram. НПД Чек сам сообщения в клиентский бот не отправляет.
Состояния:
statusЧто делать боту
awaiting_orderПлатёж получен через webhook; ждём сведения заказа
queuedЖдём агента/ФНС/активного доступа; продолжать опрос
processingАгент обрабатывает чек; продолжать опрос
registeredПри ready=true отправить ссылку и зафиксировать доставку
review, unknown, failedНужна проверка в кабинете; не создавать второй заказ/чек
refund_reviewПодтверждён полный возврат, ждём подтверждение аннулирования
partial_refundЧастичный возврат: подтверждение замены на остаток в кабинете
canceling, canceledАннулирование; не отправлять как новый действующий чек

Повтор POST не запускает заново unknown/failed/review и не аннулирует чек. При registered с ready=false нужна проверка поддержки: ссылка отсутствует.

Ошибки и повторы


Формат ошибки: {"detail":"..."}; при422 detail может быть массивом.
HTTPДействие
400Платёж/ключи магазина/валюта не подходят; разбор настройки
401Ключ интеграции отсутствует, отозван или заменён; обновить Secret
404Объект не найден в этом аккаунте/магазине; проверить идентификаторы
409Данные конфликтуют или настройка не готова; не менять ID ради повтора
422Неверный формат запроса; исправить данные
425ЮKassa ещё не подтвердила успешную оплату; повторить через≥5секунд
429Превышен лимит, повторить с учётом Retry-After
5xx, сетевой тайм-аутРезультат неизвестен; повторить точный POST с теми же ID/данными

Общий лимит300 запросов в минуту на аккаунт, проверок новых заказов60/мин. Постоянные ошибки сохранять для разбора, временные повторять с задержкой.

Подключение к присланному коду


Сохраните локальную задачу чека согласованно с переходом orders→paid, до fulfillPaidOrder. Для D1 используйте batch с уникальной задачей по платежу. Не делайте сеть НПД Чек обязательной частью выдачи покупки. Не привязывайте повтор доставки исключительно к res.meta.changes: при повторном событии paid-заказа задача могла ещё не дойти до сервиса.
Готовый транспортный модуль: https://npdcheck.ru/assets/npdcheck-client.mjs
import {orderPayload, submitOrder, getOrder, receiptLink}
  from './npdcheck-client.mjs';

// successful_payment: получить и сохранить снимок в локальную очередь.
const payload = orderPayload(order, msg.successful_payment);
// payload=null для demo/balance. Не вызывает сеть.

// В фоновом обработчике очереди:
const accepted = await submitOrder(C.env, savedPayload);
// Отметить отправку подтверждённой; это ещё не означает, что чек готов.

// Позже в отдельной задаче опроса:
const state = await getOrder(C.env, savedPayload.external_id);
const link = receiptLink(state);
// Только если link непустой: отправить покупателю и записать результат.

Модуль не заменяет очередь БД, cron, привязку покупателя и учёт доставки. Сетевой тайм-аут Telegram при отправке сообщения может дать неоднозначный результат; не обещайте exactly-once доставку сообщений.

Границы первой версии


ЮKassa/Telegram Payments, один заказ/одна услуга, RUB, продажи физлицам. Заказы с B2B-метаданными или признаками возврата направляются на проверку. Demo, внутренний balance и сумма0 не отправляются. При смешанной оплате передаётся сумма внешнего платежа ЮKassa, без прибавления credit_used. Lava.top, EUR/USD и API денежных возвратов пока не включены. Для полного возврата используйте действующий сценарий кабинета.

Ежемесячные списания

Каждый фактически завершённый платёж ЮKassa, включая рекуррентный, передаётся отдельным заказом. Используйте external_id: "charge:" + payment.id, а не постоянный ID подписки. Повтор одного списания отправляется с теми же данными и тем же ID. Круглосуточная выдача требует постоянно работающего агента и активного доступа. Действующий webhook ЮKassa можно оставить: ваш backend сохраняет событие в очередь и передаёт платёж через POST /orders. Возвраты также должны доходить до НПД Чек либо проверяться продавцом в кабинете.

Подписанные уведомления о чеках

Кабинет → «04. API для бота» → «Уведомления на ваш сервер». Сначала включите API. Укажите публичный HTTPS URL на порту 443. Секрет npdc_wh_… показывается один раз: сохраните его как NPDCHECK_WEBHOOK_SECRET на своём backend, отдельно от ключа API. Доступны сохранение/перевыпуск адреса и секрета, отключение и последние 20 доставок. Смена адреса/секрета и отключение отменяют старые ожидающие доставки. Отзыв ключа интеграции также отключает уведомления. Новое подключение не рассылает старые чеки.

POST содержит JSON в UTF-8:

{
  "id": "evt_0123456789abcdef0123456789abcdef",
  "type": "receipt.registered",
  "created_at": 1793690000,
  "data": {
    "external_id": "charge:payment-12345678",
    "provider": "yookassa",
    "payment_id": "payment-12345678",
    "provider_receipt_id": "fns-document-id",
    "description": "Подписка MUVAI за ноябрь",
    "amount": "1000.00",
    "original_amount": "1500.00",
    "refunded_amount": "500.00",
    "currency": "RUB",
    "status": "registered",
    "ready": true,
    "receipt_url": "https://lknpd.nalog.ru/api/v1/receipt/ИНН/ID/print",
    "error": null
  }
}

Типы: receipt.registered, receipt.canceled, receipt.partial_refund, receipt.refund_review, receipt.unknown, receipt.review, receipt.failed. external_id может быть null, если заказ ещё не передавался через API.

Заголовки:

Проверяйте подпись до разбора/использования данных, через constant-time compare или криптографическую verify. Допускайте разницу часов не более 5 минут и синхронизируйте часы сервера. Повторная попытка получает новую подпись и timestamp, сохраняя ID и тело. В модуле есть готовая verifyWebhook(rawBody, request.headers, env.NPDCHECK_WEBHOOK_SECRET).

import {verifyWebhook} from './npdcheck-client.mjs';

// В маршруте POST /webhooks/npdcheck вашего backend:
const rawBody = await request.text();
const event = await verifyWebhook(rawBody, request.headers, env.NPDCHECK_WEBHOOK_SECRET);
// Здесь: транзакционный INSERT OR IGNORE в свою очередь с UNIQUE(event.id).
// Ответ 2xx только ПОСЛЕ успешной записи; при ошибке БД верните 5xx.
// Затем отдельный worker обрабатывает свою очередь.
return new Response(null, {status: 204});

Успех доставки: любой HTTP 2xx в пределах 5 секунд ожидания сети на соединение/чтение. Редиректы не выполняются. При ошибках до 10 попыток, интервалы после неудач: 30 с, 2 мин, 10 мин, 30 мин, 1 ч, 2 ч, 4 ч, 6 ч, 10 ч (около суток). После исчерпания попыток в кабинете доступен ручной повтор с тем же ID. Событие хранится в БД вместе с изменением чека; перезапуск не теряет очередь.

Доставка допускает повторы и изменение порядка. Храните UNIQUE(event.id), а отправку покупателю учитывайте по ID документа ФНС, поскольку после корректировки у того же платежа появится новый чек. Перед отправкой ссылки получите актуальный заказ через GET /orders/{external_id}: снимок старого события мог уже устареть. Не отправляйте ссылку из события registered, если действующий документ уже другой или аннулирован. Опрос API остаётся доступен как резерв и для сверки пропущенных/устаревших событий.

Частичный возврат и замена чека

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

  1. Сохраняется история исходного документа, суммы и ссылки.
  2. Аннулируется исходный чек. До подтверждённого успеха новый не выпускается.
  3. В очередь ставится новый чек на оставшуюся сумму с исходным названием и датой расчёта.
  4. Новый документ доступен через тот же GET заказа и событие receipt.registered.

Повторный частичный возврат допускает следующую замену. Проверяется общая сумма возвратов относительно первоначального платежа, а не сумма последнего чека. Если возврат изменился перед выдачей или результат ФНС неизвестен, операция останавливается для сверки. Автоматически повторять неопределённый POST ФНС нельзя. При полном возврате используется «Аннулировать» без нового чека.

API интеграции по-прежнему не даёт прав возвращать деньги или подтверждать замену: это действие владельца в кабинете. GET заказа дополнительно возвращает original_amount, refunded_amount, provider_receipt_id и replacements.

Основание сохранения даты расчёта: разъяснение ФНС.

Нужна помощь с подключением? Напишите в поддержку НПД Чек.