Начало работы
API принимает и возвращает JSON. Адрес: https://notpay.io/api/v1. Все суммы — целые числа в копейках: 150000 — это 1 500 ₽. Время — ISO 8601 в UTC.
- Войдите в кабинет и добавьте магазин. Ключ API покажется один раз — сохраните его в переменных окружения сервера.
- Создайте платёж и отправьте покупателя по ссылке
urlиз ответа. - Укажите адрес для уведомлений: когда платёж оплатят, мы отправим туда
payment.paid.
Авторизация
Передавайте ключ магазина в заголовке Authorization. Ключ даёт полный доступ к платежам магазина: храните его только на сервере и никогда не вставляйте в код страницы или мобильного приложения.
Authorization: Bearer np_3k9xq2mv_Xc1…Ключ можно перевыпустить в настройках магазина. Старый перестаёт работать сразу.
Создать платёж
POST/api/v1/payments
| Поле | Тип | Описание |
|---|---|---|
amount | integer | Обязательно. Сумма в копейках. |
order_id | string | Номер заказа в вашей системе, до 64 символов: буквы, цифры и - _ . : / #. Пока платёж по этому заказу ждёт оплаты, повторный запрос вернёт его же, а не создаст второй. |
description | string | Что покупают, до 250 символов. Покупатель увидит это на странице оплаты. |
method | string | Принять платёж только этим способом: sbp, card. Покупатель увидит только его. Без поля покупатель выберет сам. |
email | string | E-mail покупателя для чека. |
success_url | string | Куда вернуть покупателя после оплаты. По умолчанию — адрес из настроек магазина. |
fail_url | string | Куда вернуть после неуспешной оплаты. |
ttl_minutes | integer | Сколько живёт ссылка: от 5 до 1440. По умолчанию 30. |
metadata | object | Ваши данные: до 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 |
Объект платежа
{
"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 | Состояние платежа — см. статусы. |
test | true — тестовый платёж: деньги не списывались. |
fee, net | Комиссия и сумма, пришедшая на баланс. Заполнены у оплаченных платежей. |
refunded | Сколько уже вернули, в копейках. |
fail_reason | Почему платёж не прошёл: {"code", "message"} — см. ниже. |
card | Маска карты и банк, если банк их передал. Полный номер карты к нам не приходит. |
url | Страница оплаты для покупателя. |
Статусы и причины отказа
| status | Что значит |
|---|---|
pending | Ожидает оплаты: ссылка действует до expires_at. |
paid | Оплачен: деньги на балансе. Это финальный успешный статус. |
failed | Ошибка оплаты: банк или покупатель не завершили оплату, причина — в fail_reason. |
expired | Истёк: покупатель не оплатил вовремя. |
cancelled | Отменён: вы отменили платёж. |
refunded | Возвращён: деньги полностью вернули покупателю. |
| fail_reason.code | message |
|---|---|
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 минут — так старое уведомление нельзя прислать повторно.
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'));
}$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);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 Secure | failed, 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, а он сейчас не получен. Повторите позже. |