Security

Authentication

sk_test / sk_live API keys, request format, HMAC signing and idempotency. A Bearer key is mandatory; the signature is optional but recommended.

Every protected request needs an Authorization: Bearer <api_key> header. Keys are issued per shop in the Integration tab.

sk_test_…
test key. Hits the simulator; no real bank call. Always available.
sk_live_…
live key. Returns 403 until the merchant is activated for live mode.
The full secret is shown only once at creation. If lost, revoke the old key and issue a new one.

Request format

Request and response bodies are JSON in UTF-8. Dates and times use RFC 3339 in UTC. Money amounts are integers in the currency's minor units: kopeks for RUB, cents for USD. Identifiers are UUIDv4.

curl -X POST https://api.nerezpay.ru/v1/public/payments \
  -H 'Authorization: Bearer sk_test_x9k…' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: 7f2a-pay-1042'

Dashboard settings and HMAC signing

Everything below is configured in the dashboard: /cabinet/integration → shop card. These settings can\u2019t be changed via the public API: they\u2019re meant for a human, not a server.

Per shop

  • API keys. sk_test_… hits the simulator, sk_live_… hits the real bank. You can issue multiple keys per shop, each revocable in one click. The full secret is shown only once — save it immediately.
  • IP allow-list. List of IPv4, IPv6, CIDR that\u2019re allowed to use this shop\u2019s keys. Requests from other addresses get 403 ip_not_allowed. An empty list means no restriction.
  • Shop HMAC secret (thm_…). Used to sign your requests to our API. If the shop has "Require signature" enabled, requests without an X-PSP-Signature: sha256=<hex> header get 401. A second factor on top of the Bearer key: even if the key leaks via a log or proxy, without the secret no one can forge requests.

Request signing (optional)

When "Require signature" is enabled on the shop, every API request must be signed with the shop secret (thm_…) and placed in the header X-PSP-Signature: sha256=<hex>. Algorithm: HMAC-SHA256 over the raw body — the exact bytes that go over the wire, before any transforms. For GET requests with an empty body sign the empty string.

import crypto from 'node:crypto'

const body = JSON.stringify({
  amount: 150000, currency: 'RUB', method: 'sbp', order_id: 'ORDER-1042',
})
const sig = 'sha256=' + crypto
  .createHmac('sha256', process.env.PSP_TERMINAL_SECRET) // thm_…
  .update(body)
  .digest('hex')

await fetch('https://api.nerezpay.ru/v1/public/payments', {
  method: 'POST',
  headers: {
    'Authorization':       `Bearer ${process.env.PSP_API_KEY}`,
    'Content-Type':        'application/json',
    'Idempotency-Key':     '7f2a-pay-1042',
    'X-PSP-Signature': sig,
  },
  body, // IMPORTANT: exactly the same body you signed — JSON.stringify once
})
Sign the exact bytes that go over the wire. The most common mistake: serialize JSON to a string, compute the signature over it, then pass the object back — the HTTP client re-serializes it with different key order or whitespace, and the signature stops matching. Compute the signature over a ready byte body and send the same bytes.

Webhook endpoints

You can configure several endpoints per shop. Each has its own URL, event set and secret (whs_…). We use that secret to sign webhooks we send you. See Webhooks for details.

Don\u2019t mix up the two HMAC secrets. The shop secret (thm_…) signs your requests to us. The webhook endpoint secret (whs_…) signs the webhooks we send to you. They\u2019re independent and rotated separately.

Idempotency

On resource-creating POSTs (payment, payout) include an Idempotency-Key header of up to 64 characters. A repeated request with the same key returns the previously created object and does not create a duplicate. Keys are unique per merchant; use your own order_id or an application-side UUID.

Repeat with the same key and the same body — 200 OK with the existing object and idempotent: true. Repeat with the same key and a different body — 409 idempotent_conflict.