для разработчиков

Документация API

REST API для приёма платежей: создать платёж, проверить статус, вернуть деньги и получить уведомление об оплате.

Начало работы

API принимает и возвращает JSON. Адрес: https://notpay.io/api/v1. Все суммы — целые числа в копейках: 150000 — это 1 500 ₽. Время — ISO 8601 в UTC.

  1. Войдите в кабинет и добавьте магазин. Ключ API покажется один раз — сохраните его в переменных окружения сервера.
  2. Создайте платёж и отправьте покупателя по ссылке url из ответа.
  3. Укажите адрес для уведомлений: когда платёж оплатят, мы отправим туда payment.paid.
Пока магазин не прошёл проверку, все платежи тестовые: деньги не списываются, а на странице оплаты открывается тестовый банк. Интеграцию можно полностью проверить до проверки сайта.

Авторизация

Передавайте ключ магазина в заголовке Authorization. Ключ даёт полный доступ к платежам магазина: храните его только на сервере и никогда не вставляйте в код страницы или мобильного приложения.

Authorization: Bearer np_3k9xq2mv_Xc1…

Ключ можно перевыпустить в настройках магазина. Старый перестаёт работать сразу.

Создать платёж

POST/api/v1/payments

ПолеТипОписание
amountintegerОбязательно. Сумма в копейках.
order_idstringНомер заказа в вашей системе, до 64 символов: буквы, цифры и - _ . : / #. Пока платёж по этому заказу ждёт оплаты, повторный запрос вернёт его же, а не создаст второй.
descriptionstringЧто покупают, до 250 символов. Покупатель увидит это на странице оплаты.
methodstringПринять платёж только этим способом: sbp, card. Покупатель увидит только его. Без поля покупатель выберет сам.
emailstringE-mail покупателя для чека.
success_urlstringКуда вернуть покупателя после оплаты. По умолчанию — адрес из настроек магазина.
fail_urlstringКуда вернуть после неуспешной оплаты.
ttl_minutesintegerСколько живёт ссылка: от 5 до 1440. По умолчанию 30.
metadataobjectВаши данные: до 20 ключей, значения — строки, числа или true/false. Вернутся в ответах и вебхуках.
запрос
curl https://notpay.io/api/v1/payments \
  -H "Authorization: Bearer $NOTPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"amount": 490000, "order_id": "order-1042", "description": "Курс «Деловое письмо»"}'

Ответ — 201 Created и объект платежа со статусом pending. Повтор по тому же order_id — 200 OK и тот же платёж.

Получить платёж

GET/api/v1/payments/{id}

GET/api/v1/payments?order_id={order_id}

Второй вариант возвращает {"data": [...]} — все платежи магазина по этому заказу, новые первыми. Без order_id — последние 100 платежей; параметр limit — от 1 до 100.

Отменить платёж

POST/api/v1/payments/{id}/cancel

Только для платежей в статусе pending: ссылка перестанет работать, покупатель увидит, что платёж отменён. Придёт вебхук payment.cancelled.

Вернуть деньги

POST/api/v1/payments/{id}/refund

Тело — {"amount": 100000} для частичного возврата или пустое для полного. Деньги списываются с баланса магазина и уходят на ту же карту или счёт. Вернуть можно в течение 180 дней после оплаты, несколькими частями, но в сумме не больше платежа. Комиссия за исходный платёж не возвращается. Когда вернули всё, статус станет refunded.

Способы оплаты

GET/api/v1/methods

Без авторизации. Список способов со стандартной комиссией и лимитами — теми же, что в тарифах. Поля каждого способа: id — что передавать в method, name — название, fee_percent — стандартная комиссия в процентах (ваша может быть ниже, её видно в каждом платеже в поле fee), min и max — границы суммы в копейках.

idСпособКомиссияСумма, коп.
sbpСБПот 4 %1000 — 60000000
cardБанковская картаот 5 %10000 — 30000000

Объект платежа

payment
{
  "id": "fdba8c6e-2f69-46af-a134-86ffc0b0d6f3",
  "object": "payment",
  "status": "paid",
  "test": false,
  "order_id": "order-1042",
  "amount": 490000,
  "amount_rub": "4900.00",
  "currency": "RUB",
  "description": "Курс «Деловое письмо»",
  "method": "sbp",
  "fee": 19600,
  "net": 470400,
  "refunded": 0,
  "fail_reason": null,
  "card": null,
  "email": "anna@example.ru",
  "metadata": { "user_id": "18342" },
  "url": "https://notpay.io/pay/fdba8c6e-2f69-46af-a134-86ffc0b0d6f3",
  "created_at": "2026-10-26T11:32:11.000Z",
  "expires_at": "2026-10-26T12:02:11.000Z",
  "paid_at": "2026-10-26T11:33:02.000Z"
}
ПолеОписание
statusСостояние платежа — см. статусы.
testtrue — тестовый платёж: деньги не списывались.
fee, netКомиссия и сумма, пришедшая на баланс. Заполнены у оплаченных платежей.
refundedСколько уже вернули, в копейках.
fail_reasonПочему платёж не прошёл: {"code", "message"} — см. ниже.
cardМаска карты и банк, если банк их передал. Полный номер карты к нам не приходит.
urlСтраница оплаты для покупателя.

Статусы и причины отказа

statusЧто значит
pendingОжидает оплаты: ссылка действует до expires_at.
paidОплачен: деньги на балансе. Это финальный успешный статус.
failedОшибка оплаты: банк или покупатель не завершили оплату, причина — в fail_reason.
expiredИстёк: покупатель не оплатил вовремя.
cancelledОтменён: вы отменили платёж.
refundedВозвращён: деньги полностью вернули покупателю.
fail_reason.codemessage
declinedБанк отклонил платёж
insufficientНедостаточно средств
3dsНе пройдена проверка 3-D Secure
limitПревышен лимит по карте или счёту
fraudПлатёж остановлен антифродом
expiredВремя на оплату истекло
cancelledПлатёж отменён
unavailableСпособ оплаты временно недоступен

Вебхуки

Когда статус платежа меняется, мы отправляем POST на адрес из настроек магазина. Адрес должен начинаться с https://.

typeКогда
payment.paidПлатёж оплачен — можно выдавать заказ.
payment.failedОплата не прошла. Покупатель может попробовать снова по новой ссылке.
payment.expiredВремя на оплату вышло.
payment.cancelledВы отменили платёж.
payment.refundedСделан возврат, полный или частичный: смотрите data.refunded.
pingПроверка адреса кнопкой «Отправить тестовое уведомление» в разделе «Проверка интеграции» на странице магазина. Ответьте 2xx, заказ не трогайте.
уведомление
POST /notpay/webhook HTTP/1.1
Content-Type: application/json; charset=utf-8
X-Notpay-Event: payment.paid
X-Notpay-Delivery: evt_5c2e0f4b8a91d3e7a6
X-Notpay-Signature: t=1792582382,v1=5f0c4b…e91a

{"id": "evt_5c2e0f4b8a91d3e7a6", "type": "payment.paid", "created_at": "…", "test": false, "data": { …объект платежа… }}

Проверка подписи

Подпись — HMAC-SHA256 от строки <t>.<тело запроса> с секретом вебхуков магазина (whsec_…, в настройках магазина). Считайте её от сырого тела, до разбора JSON, и сравнивайте за постоянное время. Отклоняйте уведомления старше 5 минут — так старое уведомление нельзя прислать повторно.

Node.js
import { createHmac, timingSafeEqual } from 'node:crypto';

// raw — тело запроса как строка, до JSON.parse
export function verify(raw, header, secret) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; // не старше 5 минут
  const expected = createHmac('sha256', secret).update(`${t}.${raw}`).digest('hex');
  const got = Buffer.from(parts.v1 ?? '', 'hex');
  return got.length === 32 && timingSafeEqual(got, Buffer.from(expected, 'hex'));
}
PHP
$raw = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_X_NOTPAY_SIGNATURE'] ?? ''), $sig);
$expected = hash_hmac('sha256', $sig['t'] . '.' . $raw, getenv('NOTPAY_WEBHOOK_SECRET'));
if (abs(time() - (int) $sig['t']) > 300 || !hash_equals($expected, $sig['v1'] ?? '')) {
  http_response_code(400);
  exit;
}
$event = json_decode($raw, true);
if ($event['type'] === 'payment.paid') {
  // отметьте заказ $event['data']['order_id'] оплаченным
}
http_response_code(200);
Python
import hmac, hashlib, time

def verify(raw: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t = int(parts.get("t", 0))
    if abs(time.time() - t) > 300:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

Ответ и повторы

Ответьте кодом 2xx в течение 10 секунд. Иначе мы повторим отправку 8 раз с паузами 1 мин, 5 мин, 15 мин, 30 мин, 1 ч, 2 ч, 4 ч, 6 ч. Одно и то же уведомление может прийти дважды: используйте id события или data.id, чтобы не выдать заказ два раза. Журнал отправок и кнопка «Отправить снова» — в карточке платежа в кабинете.

Тестовый режим

Платёж тестовый, если магазин ещё не прошёл проверку или в его настройках включён тестовый режим. У такого платежа "test": true, а на странице оплаты вместо банка открывается тестовый банк с кнопками:

КнопкаРезультат
Оплатитьpaid и вебхук payment.paid
Банк отклонилfailed, declined
Не хватает денегfailed, insufficient
Не пройден 3-D Securefailed, 3ds
Превышен лимитfailed, limit
Антифродfailed, fraud

Тестовые платежи видны в кабинете в режиме «Тест» и не влияют на баланс.

Не выдавайте заказ по тестовому платежу. Проверяйте поле test и в ответах API, и в вебхуках: если оно true, деньги не списывались. Это защищает, если тестовый режим включили по ошибке.

Ошибки

При ошибке ответ — JSON с кодом и понятным текстом на русском:

{ "error": { "code": "amount", "message": "amount — целое число копеек больше нуля: 150000 = 1 500 ₽" } }
HTTPКогда
400Неверные данные; поле — в error.code.
401Нет ключа или он неверный.
403Магазин заблокирован или на повторной проверке после смены адреса сайта: платежи временно не принимаются.
404Нет такого платежа у этого магазина.
409Действие невозможно в текущем статусе (например, вернуть неоплаченный платёж), заказ с этим order_id уже ждёт оплаты на другую сумму, или на балансе не хватает денег для возврата.
413Тело запроса больше 64 КБ.
429Слишком много запросов: больше 600 в минуту на магазин. Подождите минуту.
503Способ оплаты временно недоступен, или для возврата нужен курс Rapira, а он сейчас не получен. Повторите позже.