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 prefixEnvironmentDescription
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
BehaviourDescription
Same key + same paramsReturns the cached original response. Safe to retry.
Same key + different paramsReturns 409 DUPLICATE_KEY error.
Key expiryKeys 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
  }
}
HTTP Code Meaning
200Success
400INVALID_AMOUNTAmount missing, zero, or not an integer
400INVALID_CURRENCYCurrency must be RUB
401UNAUTHORIZEDAPI key missing or invalid
404NOT_FOUNDPayment or refund does not exist (or belongs to another merchant)
409DUPLICATE_KEYIdempotency key reused with different parameters
422PAYMENT_NOT_REFUNDABLEPayment status does not allow refunds
422REFUND_EXCEEDS_AMOUNTRefund amount exceeds remaining refundable balance
429RATE_LIMITEDToo many requests. Respect Retry-After header (seconds).
500INTERNAL_ERRORServer error. Safe to retry with exponential backoff.

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
ParameterTypeDescription
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 paramTypeDescription
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
ParameterTypeDescription
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: PENDINGCOMPLETED 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 "", 200
PHP
$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.
FieldTypeDescription
idstringUnique NBPay payment ID. Prefix: pay_.
objectstringAlways "payment".
statusstringCurrent status. See Payment Statuses.
amountnumberPayment amount in RUB (decimal, 2 places).
currencystringAlways "RUB".
order_idstringYour order identifier provided at creation.
payment_methodstring · 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:
schemeMIR | VISA | MASTERCARD
typeDEBIT | CREDIT
last4 — last 4 digits of the card PAN
issuer_bank — name of the card-issuing bank
authorization_statusstring · nullAUTHORIZED | DECLINED | PENDING. Reflects the acquirer's authorization result. null while customer has not yet interacted with the payment page.
authorized_atstring · nullISO 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 RUB
currency — always "RUB"
null while the payment is still PENDING.
exchange_ratenumber · nullThe 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_currencystringYour contracted settlement currency (e.g. "USD", "EUR", "AED").
refunded_amountnumberTotal amount refunded to the customer across all refunds, in RUB. 0.00 if no refunds have been issued.
refundable_amountnumberAmount still available for refund: amount − refunded_amount. 0.00 when fully refunded.
refund_idsarrayArray of refund IDs (ref_xxx) associated with this payment. Empty array if no refunds have been made.
failure_reasonstring · nullMachine-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 name
sku — product article / SKU
unit_price — price per unit, in RUB
quantity — integer unit count
total_priceunit_price × quantity, calculated by NBPay
customerobject · nullCustomer details: email, phone.
metadataobjectYour key-value pairs, returned exactly as provided.
notification_urlstringThe webhook delivery URL for this specific payment.
success_urlstring · nullCustomer redirect URL on successful payment.
fail_urlstring · nullCustomer redirect URL on failed payment.
payment_urlstring · nullHosted payment page URL. Present only while status is PENDING.
expires_atstring · nullISO 8601. payment_url expiry time. Present only while PENDING.
created_atstringISO 8601. Payment creation timestamp.
completed_atstring · nullISO 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 Terminal? Meaning
PENDING No Payment created. Customer has been redirected to or is currently on the payment page.
COMPLETED Yes Authorised and funds confirmed. Safe to fulfil the customer's order.
FAILED Yes Declined by bank, 3DS failure, or payment page expired. No funds were moved. Create a new payment to retry.
REFUNDED Yes Fully refunded. refundable_amount is 0.00. No further refunds are possible.
PARTIALLY_REFUNDED No One or more partial refunds have been issued. Further refunds are allowed up to refundable_amount.
Status transitions
PENDINGCOMPLETEDREFUNDED
PENDINGFAILED
COMPLETEDPARTIALLY_REFUNDEDREFUNDED