Приём платежей через QR и перевод на карту
Вы создаёте платёж одним запросом, отправляете покупателя на нашу форму оплаты и получаете вебхук, когда деньги пришли.
Статус review бывает только у оплаты на карту: покупатель загрузил чек, оператор проверяет поступление. QR-платёж переходит из pending сразу в success.
Подключение за 5 шагов
- Получите API-ключ и webhook secretИх выдаёт менеджер. Ключ выглядит как
pk_live_…, secret — какwhsec_…. Сообщите менеджеру адрес, на который присылать вебхуки. - Проверьте доступные методы
GET /api/v1/methods/покажет, какие способы оплаты включены для вашего ключа и в каких пределах суммы. - Создайте платёж
POST /api/v1/payments/с суммой, методом и вашим номером заказа. Сохранитеidиз ответа рядом с заказом. - Отправьте покупателя на формуПеренаправьте его по
payment_url. На форме он увидит QR или реквизиты карты и загрузит чек. - Примите вебхукКогда статус станет
success, на ваш адрес придётpayment.success. Проверьте подпись и выдайте товар.
Авторизация
Базовый адрес API:
Передавайте ключ в каждом запросе в заголовке Authorization. Запросы принимаются только по HTTPS.
Authorization: Bearer pk_live_9f3c…
Если ключ не передан, неверный или отключён, API отвечает 401. Ключ даёт полный доступ к платежам вашего аккаунта: храните его на сервере и не вставляйте в код сайта или мобильного приложения.
Доступные методы
Возвращает способы оплаты, доступные вашему ключу прямо сейчас, с минимальной и максимальной суммой. Список меняется: метод может быть временно выключен. Запрашивайте его перед тем, как показывать покупателю выбор, и не храните у себя.
[
{
"method": "qr",
"qr_method_id": "12",
"name": "СБП QR",
"description": "Оплата по QR в приложении банка",
"min_amount": "1000.00",
"max_amount": "200000.00"
},
{
"method": "card",
"qr_method_id": null,
"name": "Перевод на карту Узбекистана",
"description": null,
"min_amount": "1000.00",
"max_amount": "100000.00"
}
]max_amount: null означает, что верхнего предела нет. Пустой список — сейчас ни один метод недоступен.
Создать платёж
| Поле | Тип | Описание |
|---|---|---|
methodобязательно | string | qr или card |
amountобязательно | string / number | Сумма в RUB, от 1, до 2 знаков после точки: 5000, "1500.50" |
order_id | string | Ваш номер заказа, до 128 символов. Придёт обратно в ответах и вебхуках. Уникальность мы не проверяем |
customer_id | string | ID покупателя в вашей системе, до 128 символов. По нему работают лимиты на покупателя; для некоторых аккаунтов поле обязательно |
description | string | До 500 символов. Показывается покупателю на форме оплаты |
qr_method_id | string | Только для qr: ID метода из списка методов. Не передан — подберём подходящий по сумме |
success_url | URL | Куда вернуть покупателя после оплаты |
fail_url | URL | Куда вернуть покупателя, если оплата не прошла |
curl -X POST https://m-pay-provider.com/api/v1/payments/ \ -H "Authorization: Bearer pk_live_9f3c…" \ -H "Content-Type: application/json" \ -d '{ "method": "card", "amount": 5000, "order_id": "A-1042", "customer_id": "user-17", "success_url": "https://shop.example/thanks", "fail_url": "https://shop.example/fail" }'
import requests resp = requests.post( "https://m-pay-provider.com/api/v1/payments/", headers={"Authorization": "Bearer pk_live_9f3c…"}, json={ "method": "card", "amount": 5000, "order_id": "A-1042", "customer_id": "user-17", "success_url": "https://shop.example/thanks", "fail_url": "https://shop.example/fail", }, timeout=30, ) payment = resp.json() if resp.status_code == 201: redirect_to(payment["payment_url"]) else: print(payment["error"], payment["message"])
<?php $ch = curl_init('https://m-pay-provider.com/api/v1/payments/'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer pk_live_9f3c…', 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'method' => 'card', 'amount' => 5000, 'order_id' => 'A-1042', 'customer_id' => 'user-17', 'success_url' => 'https://shop.example/thanks', 'fail_url' => 'https://shop.example/fail', ]), ]); $payment = json_decode(curl_exec($ch), true); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); if ($code === 201) { header('Location: ' . $payment['payment_url']); }
const resp = await fetch("https://m-pay-provider.com/api/v1/payments/", { method: "POST", headers: { Authorization: "Bearer pk_live_9f3c…", "Content-Type": "application/json", }, body: JSON.stringify({ method: "card", amount: 5000, order_id: "A-1042", customer_id: "user-17", success_url: "https://shop.example/thanks", fail_url: "https://shop.example/fail", }), }); const payment = await resp.json(); if (resp.status === 201) res.redirect(payment.payment_url);
Ответ 201
{
"id": "83c00c41-ca6c-4d34-bb53-77de721d0b3d",
"status": "pending",
"method": "card",
"amount": "5000.00",
"paid_amount": null,
"currency": "RUB",
"order_id": "A-1042",
"customer_id": "user-17",
"description": null,
"payment_url": "https://m-pay-provider.com/pay/83c00c41-ca6c-4d34-bb53-77de721d0b3d/",
"comment": null,
"created_at": "2026-10-18T13:40:02+03:00",
"expires_at": "2026-10-18T14:10:02+03:00",
"paid_at": null,
"card": {
"number": "8600 1234 5678 9012",
"holder_name": "ALIYEV ALISHER",
"phone": "+998 90 111 22 33",
"bank": "Kapitalbank",
"instructions": "Откройте приложение банка → Переводы за границу…"
},
"receipts_count": 0
}| Поле | Описание |
|---|---|
id | ID платежа. Используйте его во всех остальных запросах и для сверки с вебхуками |
status | Текущий статус, см. Статусы |
payment_url | Наша форма оплаты для покупателя |
paid_amount | Зачисленная сумма. Заполняется при success |
expires_at | До какого момента ждём оплату |
comment | Причина при отказе, отклонении или отмене |
card | Реквизиты для перевода, только у method: card. У QR — null |
receipts_count | Сколько чеков загружено по платежу |
Если запрос оборвался по таймауту, не создавайте платёж повторно вслепую. Сначала найдите его по номеру заказа: GET /api/v1/payments/?order_id=A-1042.
Форма оплаты
По payment_url открывается готовая страница, адаптированная под телефон. Ничего встраивать не нужно — достаточно перенаправить покупателя.
| Метод | Что видит покупатель |
|---|---|
qr | Кнопку «Перейти к оплате» на страницу банка или QR-код. Через несколько секунд после создания платежа, пока поставщик готовит ссылку, — индикатор ожидания. По желанию можно приложить чек. |
card | Номер карты, получателя, банк, телефон и сумму с кнопками «Копировать», инструкцию по переводу, таймер и обязательную загрузку чека. |
Страница сама обновляет статус. После успешной оплаты покупатель через 3 секунды возвращается на success_url. При отказе на странице появляется кнопка возврата на fail_url.
Возврат на success_url — это только удобство для покупателя, а не подтверждение оплаты. Выдавайте товар по вебхуку payment.success или после проверки статуса через API.
Оплата на карту
Покупатель переводит деньги на карту узбекского банка и обязательно прикладывает чек — скриншот или PDF. Оператор сверяет поступление и подтверждает платёж, обычно за несколько минут.
- Платёж созданКарта выдана, статус
pending. На перевод и загрузку чека даётся время доexpires_at. - Чек загруженСтатус
review, приходит вебхукpayment.review. - Оператор проверил
success, если деньги пришли, илиrejected, если перевода нет. Причина отклонения — вcomment.
Если чек не загрузили вовремя, платёж переходит в expired. Покупатель, который всё же успел перевести, может загрузить чек и после этого — платёж снова уйдёт на проверку.
Хотите показывать реквизиты в своём интерфейсе — берите их из поля card, а чек отправляйте через загрузку чека.
Статусы
| Статус | Что значит | Финальный |
|---|---|---|
| created | Платёж создан, поставщик QR готовит реквизиты. Обычно длится несколько секунд | нет |
| pending | Ждём оплату от покупателя | нет |
| review | Только карта: чек загружен, оператор проверяет поступление | нет |
| success | Оплачено. Можно выдавать товар | да |
| failed | Оплата не прошла или поставщик не смог создать платёж. Причина в comment | да* |
| rejected | Только карта: оператор не нашёл перевод | да |
| expired | Время на оплату истекло | нет* |
| cancelled | Платёж отменён | да* |
* Банк может подтвердить оплату с опозданием. Поэтому QR-платёж из expired, failed или cancelled иногда переходит в success, а платёж по карте из expired — в review. Не удаляйте такие заказы сразу: держите их хотя бы сутки и обрабатывайте вебхук payment.success в любой момент.
Получить платёж
Возвращает платёж в том же формате, что и при создании. Удобно для проверки статуса, если вебхук не пришёл. Опрашивайте не чаще раза в 5–10 секунд. Чужой или несуществующий платёж — 404.
Список платежей
| Параметр | Описание |
|---|---|
status | Фильтр по статусу |
order_id | Найти платежи по вашему номеру заказа |
customer_id | Платежи одного покупателя |
limit | Сколько вернуть, по умолчанию 50, максимум 200 |
offset | Сколько пропустить — для постраничного вывода |
Платежи отсортированы от новых к старым. Ответ — массив объектов платежа.
Загрузить чек
Нужно, только если чек принимаете вы, а не наша форма. Формат запроса — multipart/form-data, поле file. Подходят JPG, PNG, WebP и PDF до 10 МБ, не больше 5 чеков на платёж.
curl -X POST https://m-pay-provider.com/api/v1/payments/83c00c41-…/receipt/ \ -H "Authorization: Bearer pk_live_9f3c…" \ -F "file=@check.jpg"
with open("check.jpg", "rb") as f: resp = requests.post( f"https://m-pay-provider.com/api/v1/payments/{payment_id}/receipt/", headers={"Authorization": "Bearer pk_live_9f3c…"}, files={"file": f}, timeout=60, )
$ch = curl_init("https://m-pay-provider.com/api/v1/payments/$paymentId/receipt/"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer pk_live_9f3c…'], CURLOPT_POSTFIELDS => ['file' => new CURLFile('check.jpg')], ]); $payment = json_decode(curl_exec($ch), true);
const form = new FormData(); form.append("file", new Blob([fs.readFileSync("check.jpg")]), "check.jpg"); const resp = await fetch(`https://m-pay-provider.com/api/v1/payments/${paymentId}/receipt/`, { method: "POST", headers: { Authorization: "Bearer pk_live_9f3c…" }, body: form, });
В ответ 201 приходит обновлённый платёж. Для card статус станет review. Для qr статус не меняется: чек передаётся поставщику, чтобы ускорить проверку, а итог придёт вебхуком.
Отменить платёж
Отменяет платёж по карте, пока чек не загружен — статус pending. Покупатель больше не сможет его оплатить. QR-платёж отменить нельзя: дождитесь итогового статуса от поставщика. В остальных случаях — 409 cannot_cancel.
Вебхуки
При каждой смене статуса мы отправляем POST с JSON на ваш адрес. Событие называется payment. + новый статус.
| Событие | Когда приходит |
|---|---|
payment.success | Оплата подтверждена — выдавайте товар |
payment.review | Покупатель загрузил чек по карте |
payment.rejected | Оператор отклонил чек |
payment.failed | Оплата не прошла |
payment.expired | Истекло время на оплату |
payment.cancelled | Платёж отменён |
payment.pending | Поставщик QR выдал реквизиты (приходит не всегда) |
Тело запроса
{
"id": "4821",
"event": "payment.success",
"created_at": "2026-10-18T13:52:10+03:00",
"data": {
"id": "83c00c41-ca6c-4d34-bb53-77de721d0b3d",
"status": "success",
"method": "card",
"amount": "5000.00",
"paid_amount": "5000.00",
"currency": "RUB",
"order_id": "A-1042",
"customer_id": "user-17",
"paid_at": "2026-10-18T13:52:10+03:00"
}
}В data — объект платежа целиком, как в ответе API, но без реквизитов карты.
Заголовки
| Заголовок | Значение |
|---|---|
X-Webhook-Id | ID доставки, совпадает с id в теле. Одинаковый при повторах |
X-Event | Имя события |
X-Timestamp | Время отправки, unix-секунды |
X-Signature | sha256= + HMAC-SHA256, см. ниже |
User-Agent | payments-webhook/1 |
Ответ и повторы
Ответьте любым кодом 2xx в течение 10 секунд — тогда вебхук считается доставленным. Тяжёлую обработку делайте после ответа. Если ответ другой или его нет, повторим до 8 раз с нарастающей паузой: 2, 4, 8, 16, 32, 64 и 128 минут.
Один и тот же вебхук может прийти дважды. Запоминайте X-Webhook-Id и не обрабатывайте повтор. Вебхуки могут прийти не по порядку — ориентируйтесь на data.status, а при сомнениях запросите платёж через API.
Проверка подписи
Ключ подписи — ваш webhook secret whsec_…. Подписывается строка X-Timestamp + "." + сырое тело запроса. Берите тело ровно в том виде, как оно пришло: после разбора и повторной сборки JSON байты изменятся, и подпись не сойдётся. Сравнивайте подписи безопасной функцией и отклоняйте запросы старше 5 минут.
import hashlib, hmac, json, time SECRET = "whsec_…" def handle_webhook(raw_body, headers): ts = headers.get("X-Timestamp", "") signature = headers.get("X-Signature", "") expected = "sha256=" + hmac.new( SECRET.encode(), ts.encode() + b"." + raw_body, hashlib.sha256 ).hexdigest() if not hmac.compare_digest(expected, signature): return 401 if abs(time.time() - int(ts)) > 300: return 401 event = json.loads(raw_body) if already_processed(event["id"]): return 200 if event["event"] == "payment.success": mark_order_paid(event["data"]["order_id"], event["data"]["paid_amount"]) remember(event["id"]) return 200
<?php $secret = 'whsec_…'; $raw = file_get_contents('php://input'); $ts = $_SERVER['HTTP_X_TIMESTAMP'] ?? ''; $signature = $_SERVER['HTTP_X_SIGNATURE'] ?? ''; $expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $raw, $secret); if (!hash_equals($expected, $signature) || abs(time() - (int)$ts) > 300) { http_response_code(401); exit; } $event = json_decode($raw, true); if ($event['event'] === 'payment.success') { markOrderPaid($event['data']['order_id'], $event['data']['paid_amount']); } http_response_code(200);
const crypto = require("node:crypto"); const express = require("express"); const SECRET = "whsec_…"; const app = express(); app.post("/payments/webhook", express.raw({ type: "application/json" }), (req, res) => { const ts = req.get("X-Timestamp") || ""; const received = Buffer.from(req.get("X-Signature") || ""); const expected = Buffer.from( "sha256=" + crypto.createHmac("sha256", SECRET).update(ts + "." + req.body).digest("hex") ); if (received.length !== expected.length || !crypto.timingSafeEqual(received, expected)) return res.sendStatus(401); if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(401); const event = JSON.parse(req.body); if (event.event === "payment.success") markOrderPaid(event.data.order_id, event.data.paid_amount); res.sendStatus(200); });
При перевыпуске webhook secret старый перестаёт действовать сразу — обновите его у себя в тот же момент.
Лимиты
Ограничения защищают от спама и неоплаченных заявок. Для вашего аккаунта их настраивает менеджер, отдельно для QR и для карт:
- минимальная и максимальная сумма платежа;
- сколько платежей можно создать в час и в сутки;
- сколько неоплаченных платежей может быть одновременно;
- те же ограничения на одного покупателя — по
customer_id.
Если лимит превышен, API отвечает 429 с кодом, по которому видно, какой именно. Отдельно действует ограничение частоты запросов: 120 в минуту на ключ.
Передавайте customer_id всегда, даже если для вас он не обязателен. Тогда лимит сработает на одного недобросовестного покупателя, а не на весь ваш магазин.
Ошибки
Ошибка всегда приходит в одном формате. У ошибок валидации дополнительно есть fields с описанием по каждому полю.
{
"error": "customer_pending_limit",
"message": "У покупателя слишком много неоплаченных платежей"
}| HTTP | error | Что делать |
|---|---|---|
| 400 | validation_error | Поле заполнено неверно — подробности в fields |
| 400 | invalid_amount | Сумма должна быть больше нуля |
| 400 | amount_too_small, amount_too_large, amount_out_of_range | Сумма вне лимита — сверьтесь с методами |
| 400 | customer_id_required | Для вашего аккаунта нужно передавать customer_id |
| 400 | qr_method_unavailable | Этот метод QR выключен — запросите список методов заново |
| 400 / 502 | provider_error | Поставщик QR не принял платёж, причина в message. При 502 повторите позже или выберите другой метод |
| 400 | bad_file | Нужен JPG, PNG, WebP или PDF |
| 401 | not_authenticated, authentication_failed | Ключ не передан, неверный или отключён |
| 403 | method_disabled, client_disabled | Метод или аккаунт отключён — обратитесь к менеджеру |
| 404 | not_found | Платёж не найден или принадлежит другому аккаунту |
| 409 | no_card | Сейчас нет карты для этой суммы — предложите QR или повторите позже |
| 409 | payment_closed, payment_not_ready, cannot_cancel | Действие недоступно в текущем статусе платежа |
| 413 | file_too_large | Файл больше 10 МБ |
| 429 | hour_limit, day_limit, pending_limit | Превышен лимит аккаунта или ключа |
| 429 | customer_hour_limit, customer_day_limit, customer_pending_limit | Превышен лимит на покупателя |
| 429 | too_many_receipts | По платежу уже загружено максимум чеков |
| 429 | throttled | Слишком частые запросы — подождите и повторите |
Перед запуском
- API-ключ хранится только на сервере.
- Товар выдаётся по вебхуку
payment.successс проверенной подписью, а не по возврату наsuccess_url. - Повторный вебхук с тем же
X-Webhook-Idне выдаёт товар второй раз. - Ваш адрес для вебхуков отвечает
2xxбыстрее чем за 10 секунд. - В каждом платеже передаются
order_idиcustomer_id. - Заказы в статусах
expiredиfailedне удаляются сразу — оплата может подтвердиться позже. - Ошибки
409 no_cardи429показывают покупателю понятное сообщение.
Попробовать запросы со своим ключом можно в Swagger. Вопросы по подключению — вашему менеджеру.