NBPay API Reference
Accept payments through Russian local payment instruments — Bank cards or Faster Payments System (FPS). Integrate once with NBPay; we handle the rest.
Base URL
https://api.nbpay.io/v1
Sandbox URL
https://sandbox.api.nbpay.io/v1
🔐
API Key Auth
Bearer token in
Authorization header on every request📋
REST · JSON
Standard HTTP verbs. Request and response bodies in JSON.
🔔
Webhooks
Signed real-time HTTP callbacks for every payment event
ℹ️
All amounts are in Russian Rubles (RUB), expressed as float, two characters after the separator. Your settlement is paid out in your contracted foreign currency (USD, EUR, AED, etc.) on the schedule defined in your agreement.
Authentication
All API requests must include your secret API key as a Bearer token in the
Authorization header. Manage your keys in the NBPay Merchant Portal → Settings → API Keys.HTTP
POST /v1/payments Authorization: Bearer nbpay_live_sk_xxxxxxxxxxxxxxxxxxx Content-Type: application/json Idempotency-Key: a4f2b1c8-7d3e-4a9b-b5c2-1e7f0d3a8c4b
| Key prefix | Environment | Description |
|---|---|---|
nbpay_live_sk_ |
Production | Real money. Use only on your server, never client-side. |
nbpay_test_sk_ |
Sandbox | No real transactions. Safe for development and QA. Use with the sandbox base URL. |
⚠️
Never expose your secret key in frontend code, public repos, or logs. If a key is compromised, rotate it immediately in the Merchant Portal. NBPay will never ask for your key over email.
Idempotency
All
POST requests support — and we strongly recommend using — idempotency keys to safely retry failed requests without creating duplicates.Add a Idempotency-Key header with a unique value (UUID v4 recommended) for each distinct operation. If the request fails due to a network error, resend with the same key — NBPay returns the original response.
Header
Idempotency-Key: a4f2b1c8-7d3e-4a9b-b5c2-1e7f0d3a8c4b
| Behaviour | Description |
|---|---|
| Same key + same params | Returns the cached original response. Safe to retry. |
| Same key + different params | Returns 409 DUPLICATE_KEY error. |
| Key expiry | Keys are retained for 24 hours after the original request. |
Errors & Rate Limits
NBPay uses standard HTTP status codes. Error responses always include a JSON body with a machine-readable
code field.Error Response Shape
{
"error": {
"code": "INVALID_AMOUNT",
"message": "Amount must be a positive integer in kopecks (min 100)",
"param": "amount" // present when error relates to a specific field
}
}Rate limits: 300 requests per minute per API key. On 429, wait the number of seconds specified in the Retry-After response header before retrying.
Create Payment
Initiates a new payment session and returns a
payment_url to redirect your customer to. The customer completes the payment on NBPay's hosted page — no PCI DSS scope for your integration.
POST
/v1/payments
🔐 Auth required
| Parameter | Type | Description |
|---|---|---|
| currencyrequired | string | Must be "RUB". |
| order_idrequired | string | Your unique order identifier. Max 64 characters. Used to look up payments from your system. |
| notification_urlrequired | string | HTTPS URL to recieve webhooks with payment status updates. |
| success_urloptional | string | HTTPS URL to redirect user after successfull payment. |
| fail_urloptional | string | HTTPS URL to redirect user after failed payment. |
| payment_methodrequired | string | Pre-select method: "CARD" or "FPS" |
| customeroptional | object | Customer details. Fields: email (string) and phone (string, E.164 format — required for FPS). |
| productsrequired | object | Order details. Fields: name (string), atricle (string,), quantity (integer), unitPrice (float). |
| metadataoptional | object | Up to 10 key-value string pairs for your own use. Returned as-is in all responses and webhooks. Max 500 chars per value. |
JSON · 200 OK
{
"id": "pay_2xKj8mNpQr4vBcDf",
"object": "payment",
"status": "PENDING",
"amount": 100000,
"currency": "RUB",
"fee_amount": 100000,
"currency": "RUB",
"order_id": "order_abc123",
"payment_url": "https://pay.nbpay.io/p/2xKj8mNpQr4vBcDf",
"notification_url": "https://yourstore.com/webhook",
"success_url": "https://yourstore.com/thank-you",
"fail_url": "https://yourstore.com/sorry",
"payment_method": "CARD",
"products": { "name": "Product Name", "article": "Product Article", "quantity": 1, "price": 100000 },
"expires_at": "2026-03-10T12:30:00Z",
"created_at": "2026-03-10T12:00:00Z",
"completed_at": null,
"customer": { "email": "user@example.com" },
"metadata": { "user_id": "usr_9912" }
}
ℹ️
Redirect your customer to
payment_url immediately. The URL is valid for 30 minutes. After expiry, create a new payment.
Get Payment
Returns the full current state of a payment, including card details, fee, exchange rate, and refund information.
GET
/v1/payments/{payment_id}
🔐 Auth required
JSON · 200 OK — COMPLETED payment with partial refund
{
"id": "pay_2xKj8mNpQr4vBcDf",
"object": "payment",
"status": "PARTIALLY_REFUNDED",
"amount": 1500.00,
"currency": "RUB",
"order_id": "order_abc123",
"payment_method": "BANK_CARD",
// ── Authorization ──────────────────────────────────────────
"authorization_status": "AUTHORIZED", // AUTHORIZED | DECLINED | PENDING
"authorized_at": "2026-03-10T12:04:22Z",
// ── Card details (only for BANK_CARD payments) ─────────────
"card": {
"scheme": "MIR", // MIR | VISA | MASTERCARD
"type": "DEBIT", // DEBIT | CREDIT
"last4": "7742", // last 4 digits of the card PAN
"issuer_bank":"Sberbank" // name of the card-issuing bank
},
// ── Commission charged by NBPay ────────────────────────────
"fee": {
"amount": 45.00,
"currency": "RUB"
},
// ── FX rate used to credit your virtual account ────────────
// Provided by NBPay settlement engine, not by the acquirer.
"exchange_rate": 88.42, // RUB per 1 unit of merchant_currency
"merchant_currency": "USD", // your contracted settlement currency
// ── Refund information ─────────────────────────────────────
"refunded_amount": 500.00, // total refunded so far (RUB)
"refundable_amount": 1000.00, // remaining amount available for refund
"refund_ids": ["ref_5mTp3nRqYs8wCxEg"],
// ── Products ───────────────────────────────────────────────
"products": [
{
"name": "Laptop Asus X554L",
"sku": "SKU-9864645",
"unit_price": 1250.00,
"quantity": 1,
"total_price": 1250.00 // unit_price × quantity, calculated by NBPay
},
{
"name": "Mouse Logitech M100",
"sku": "SKU-3452678",
"unit_price": 250.00,
"quantity": 1,
"total_price": 250.00
}
],
"customer": { "email": "buyer@example.com", "phone": "+79161234567" },
"metadata": { "user_id": "usr_9912" },
"notification_url": "https://yourstore.com/webhooks/nbpay",
"success_url": "https://yourstore.com/thank-you",
"fail_url": "https://yourstore.com/payment-failed",
"payment_url": null, // null once status is no longer PENDING
"expires_at": null,
"created_at": "2026-03-10T12:00:00Z",
"completed_at": "2026-03-10T12:04:22Z"
}
ℹ️
Use webhooks as your primary mechanism for status updates. Poll
GET /payments/{id} only as a fallback when a webhook is not received within your expected timeframe.
cURL
curl https://api.nbpay.io/v1/payments/pay_2xKj8mNpQr4vBcDf \
-H "Authorization: Bearer nbpay_live_sk_xxx"List Payments
Returns a cursor-paginated list of payments, ordered newest first.
GET
/v1/payments
🔐 Auth required
| Query param | Type | Description |
|---|---|---|
| limitoptional | integer | Results per page. Default 20, max 100. |
| starting_afteroptional | string | Payment ID. Returns results created after this payment (for forward pagination). |
| statusoptional | string | Filter by status. One of: PENDING COMPLETED FAILED REFUNDED CHARGEBACK. |
| created_fromoptional | string | ISO 8601 datetime. Lower bound on created_at. |
| created_tooptional | string | ISO 8601 datetime. Upper bound on created_at. |
Response shape
{
"object": "list",
"data": [ /* array of Payment objects */ ],
"has_more": true,
"next_cursor": "pay_xxxxxxxxxxxxxx"
}Create Refund
Issue a full or partial refund against a completed payment. Funds are returned to the customer's original payment instrument within 1–3 business days.
POST
/v1/payments/{payment_id}/refunds
🔐 Auth required
| Parameter | Type | Description |
|---|---|---|
| amountoptional | number |
Amount to refund in RUB, decimal. Full refund: omit this field — NBPay automatically refunds the entire refundable_amount.Partial refund: specify an amount less than refundable_amount. Multiple partial refunds are allowed until the full amount is exhausted.
|
| commentoptional | string | Any comment about the refund. Stored internally; not shown to the customer. |
| metadataoptional | object | Key-value pairs specific to this refund operation. |
⚠️
Refunds are only possible for payments with status
COMPLETED or PARTIALLY_REFUNDED. The amount must not exceed refundable_amount.
JSON · 201 Created — partial refund
{
"id": "ref_5mTp3nRqYs8wCxEg",
"object": "refund",
"payment_id": "pay_2xKj8mNpQr4vBcDf",
"status": "PENDING", // PENDING → COMPLETED | FAILED
"amount": 500.00,
"currency": "RUB",
"comment": "customer_request",
"created_at": "2026-03-10T15:22:00Z",
"completed_at": null,
"metadata": {}
}cURL — partial refund of ₽ 500
curl -X POST \ https://api.nbpay.io/v1/payments/pay_2xKj8mNpQr4vBcDf/refunds \ -H "Authorization: Bearer nbpay_live_sk_xxx" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "amount": 500.00, "comment": "customer_request" }' ## Full refund — omit amount entirely curl -X POST \ https://api.nbpay.io/v1/payments/pay_2xKj8mNpQr4vBcDf/refunds \ -H "Authorization: Bearer nbpay_live_sk_xxx" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "comment": "customer_request" }'
Get Refund
Retrieve a specific refund by its ID.
GET
/v1/payments/{payment_id}/refunds/{refund_id}
🔐 Auth required
Returns a Refund object. Structure is identical to the Create Refund 200 response above. Refund statuses: PENDING → COMPLETED or FAILED.
Webhook Event Types
NBPay sends a signed HTTP
POST to the notification_url specified on each individual payment for every payment event. There is no global webhook configuration — each payment carries its own delivery endpoint.
⚠️
Your endpoint must respond with a
2xx status within 10 seconds. On failure, NBPay retries 5 times with exponential backoff: 10s → 60s → 5m → 30m → 2h. All delivery attempts are logged in your portal.
ℹ️
Webhook delivery is at-least-once. Your handler must be idempotent — use
data.id as the deduplication key in your database before processing any event.
payment.completed
Fired when a payment is successfully authorised and funds are confirmed. This is your signal to fulfil the order. The full Payment object is embedded in
data.Payload
{
"event": "payment.completed",
"created_at": "2026-03-10T12:04:22Z",
"data": {
"id": "pay_2xKj8mNpQr4vBcDf",
"status": "COMPLETED",
"amount": 1500.00,
"currency": "RUB",
"order_id": "order_abc123",
"payment_method": "BANK_CARD",
"authorization_status": "AUTHORIZED",
"authorized_at": "2026-03-10T12:04:22Z",
"card": {
"scheme": "MIR",
"type": "DEBIT",
"last4": "7742",
"issuer_bank": "Sberbank"
},
"fee": { "amount": 45.00, "currency": "RUB" },
"exchange_rate": 88.42,
"merchant_currency": "USD",
"refunded_amount": 0.00,
"refundable_amount": 1500.00,
"refund_ids": [],
"products": [
{ "name": "Laptop Asus X554L", "sku": "SKU-9864645",
"unit_price": 1250.00, "quantity": 1, "total_price": 1250.00 },
{ "name": "Mouse Logitech M100", "sku": "SKU-3452678",
"unit_price": 250.00, "quantity": 1, "total_price": 250.00 }
],
"customer": { "email": "buyer@example.com", "phone": "+79161234567" },
"metadata": { "user_id": "usr_9912" },
"created_at": "2026-03-10T12:00:00Z",
"completed_at": "2026-03-10T12:04:22Z"
}
}payment.failed
Fired when a payment is declined, fails 3DS, or expires without completion. No funds were moved. The customer can retry — create a new payment.
Payload
{
"event": "payment.failed",
"created_at": "2026-03-10T12:10:00Z",
"data": {
"id": "pay_9nLpKx7mRs2qWjTy",
"status": "FAILED",
"amount": 1500.00,
"currency": "RUB",
"order_id": "order_abc124",
"payment_method": "FPS",
"authorization_status": "DECLINED",
"failure_reason": "BANK_DECLINED",
"card": null,
"fee": null,
"exchange_rate": null,
"products": [
{ "name": "Laptop Asus X554L", "sku": "SKU-9864645",
"unit_price": 1250.00, "quantity": 1, "total_price": 1250.00 }
],
"customer": { "email": "buyer@example.com" },
"metadata": {},
"created_at": "2026-03-10T12:05:00Z",
"completed_at": "2026-03-10T12:10:00Z"
}
}refund.completed
Fired when a refund (full or partial) is successfully processed and funds are returned to the customer. Includes the updated
refundable_amount remaining on the payment.Payload — partial refund
{
"event": "refund.completed",
"created_at": "2026-03-10T15:24:00Z",
"data": {
"id": "ref_5mTp3nRqYs8wCxEg",
"payment_id": "pay_2xKj8mNpQr4vBcDf",
"status": "COMPLETED",
"amount": 500.00,
"currency": "RUB",
"comment": "customer_request",
"payment_status_after": "PARTIALLY_REFUNDED",
"refundable_remaining": 1000.00, // remaining balance after this refund
"created_at": "2026-03-10T15:22:00Z",
"completed_at": "2026-03-10T15:24:00Z"
}
}refund.failed
Fired when a refund could not be processed. The payment's
refundable_amount is unchanged. Contact support with the refund ID if this occurs.Signature Verification
Every webhook request carries an
X-NBPay-Signature header. Always verify this signature before processing any event.The signature is HMAC-SHA256 of the raw request body bytes, keyed with your webhook secret. The secret is delivered in the first webhook sent to each notification_url and remains stable for all future deliveries to that URL.
Node.js · Express
import crypto from 'crypto'; function verifySignature(rawBody, signature, secret) { const expected = crypto .createHmac('sha256', secret) .update(rawBody) // rawBody must be a Buffer — NOT a parsed JSON object .digest('hex'); // timingSafeEqual prevents timing-attack leakage return crypto.timingSafeEqual( Buffer.from(expected, 'utf8'), Buffer.from(signature, 'utf8') ); } app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { const sig = req.headers['x-nbpay-signature']; if (!verifySignature(req.body, sig, process.env.NBPAY_WEBHOOK_SECRET)) { return res.status(401).send('Invalid signature'); } const event = JSON.parse(req.body); switch (event.event) { case 'payment.completed': await fulfil(event.data); break; case 'refund.completed': await handleRefund(event.data); break; } res.sendStatus(200); });
Python · Flask
import os, hmac, hashlib
from flask import Flask, request, abort
def verify_signature(raw_body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
@app.route("/webhook", methods=["POST"])
def webhook():
sig = request.headers.get("X-NBPay-Signature", "")
if not verify_signature(request.get_data(), sig, os.environ["NBPAY_WEBHOOK_SECRET"]):
abort(401)
event = request.get_json()
if event["event"] == "payment.completed":
fulfil(event["data"])
return "", 200PHP
$rawBody = file_get_contents('php://input'); $sig = $_SERVER['HTTP_X_NBPAY_SIGNATURE'] ?? ''; $secret = getenv('NBPAY_WEBHOOK_SECRET'); if (!hash_equals(hash_hmac('sha256', $rawBody, $secret), $sig)) { http_response_code(401); exit('Invalid signature'); } $event = json_decode($rawBody, true); if ($event['event'] === 'payment.completed') { fulfil($event['data']); } http_response_code(200);
⚠️
Always use a timing-safe comparison (
timingSafeEqual / compare_digest / hash_equals). Standard string equality is vulnerable to timing attacks and must not be used for signature verification.
Payment Object
Returned by Create Payment, Get Payment, and List Payments endpoints, and embedded in all payment webhook payloads.
| Field | Type | Description |
|---|---|---|
| id | string | Unique NBPay payment ID. Prefix: pay_. |
| object | string | Always "payment". |
| status | string | Current status. See Payment Statuses. |
| amount | number | Payment amount in RUB (decimal, 2 places). |
| currency | string | Always "RUB". |
| order_id | string | Your order identifier provided at creation. |
| payment_method | string · null | "BANK_CARD" or "FPS". Set after the customer selects a method on the payment page. null while PENDING. |
| card | object · null |
Card details — present only for BANK_CARD payments after authorization. Fields:scheme — MIR | VISA | MASTERCARDtype — DEBIT | CREDITlast4 — last 4 digits of the card PANissuer_bank — name of the card-issuing bank
|
| authorization_status | string · null | AUTHORIZED | DECLINED | PENDING. Reflects the acquirer's authorization result. null while customer has not yet interacted with the payment page. |
| authorized_at | string · null | ISO 8601 timestamp of successful authorization. null if not yet authorized or if declined. |
| fee | object · null | NBPay commission charged on this payment. Contains:amount — number, in RUBcurrency — always "RUB"null while the payment is still PENDING. |
| exchange_rate | number · null | The RUB/FCY exchange rate applied by NBPay's settlement engine when crediting your virtual account. Represents how many RUB equal 1 unit of merchant_currency. Note: this value is set by NBPay and is not returned by the acquirer. null while PENDING. |
| merchant_currency | string | Your contracted settlement currency (e.g. "USD", "EUR", "AED"). |
| refunded_amount | number | Total amount refunded to the customer across all refunds, in RUB. 0.00 if no refunds have been issued. |
| refundable_amount | number | Amount still available for refund: amount − refunded_amount. 0.00 when fully refunded. |
| refund_ids | array | Array of refund IDs (ref_xxx) associated with this payment. Empty array if no refunds have been made. |
| failure_reason | string · null | Machine-readable reason code. Present only when status is FAILED. E.g. BANK_DECLINED, 3DS_FAILED, EXPIRED. |
| products | array |
Line items in this payment. Each item contains:name — product namesku — product article / SKUunit_price — price per unit, in RUBquantity — integer unit counttotal_price — unit_price × quantity, calculated by NBPay
|
| customer | object · null | Customer details: email, phone. |
| metadata | object | Your key-value pairs, returned exactly as provided. |
| notification_url | string | The webhook delivery URL for this specific payment. |
| success_url | string · null | Customer redirect URL on successful payment. |
| fail_url | string · null | Customer redirect URL on failed payment. |
| payment_url | string · null | Hosted payment page URL. Present only while status is PENDING. |
| expires_at | string · null | ISO 8601. payment_url expiry time. Present only while PENDING. |
| created_at | string | ISO 8601. Payment creation timestamp. |
| completed_at | string · null | ISO 8601. Timestamp when the payment reached a terminal status. |
Payment Statuses
A payment moves through the following statuses. Only
COMPLETED means funds were successfully collected.Status transitions
PENDING → COMPLETED → REFUNDED
PENDING → FAILED
COMPLETED → PARTIALLY_REFUNDED → REFUNDED
PENDING → FAILED
COMPLETED → PARTIALLY_REFUNDED → REFUNDED