APIhttps://api.spqr-payments.com
API specification
SPQR Payments API

Move money with
one clear contract.

Create PayIns and PayOuts across enabled payment methods, and track every operation through a predictable lifecycle.

Nested HMAC-SHA256 Idempotent creates Signed webhooks
create-payin.jsProduction
Payment acceptedPENDING201

PayIns

Create incoming payments and return the customer action required by the selected payment method.

PayOuts

Send funds with operational balance reservation built into the processing flow.

Lifecycle events

Receive signed, retryable webhook events whenever an operation changes state.

01
First request

Quickstart

Use the credentials from your Integration page to create a signed PayIn request from your backend.

Keep both signing secrets server-side

You receive an API client key and secret plus a separate merchant signing secret. Never expose either secret in browser or mobile application code.

1

Collect your credentials

Copy your merchant code and API key from the merchant cabinet. Retrieve both 32-byte signing secrets issued during onboarding from your secret manager.

2

Build and sign the exact body

Serialize JSON once, hash those exact bytes, and use the nested HMAC construction below. Do not reformat the body after signing.

3

Send from your backend

Use a new idempotency key for each logical operation. Reuse that key only when retrying the same request body.

Node.js 18+
import crypto from "node:crypto";

const apiBase = "https://api.spqr-payments.com";
const apiKey = process.env.SPQR_API_KEY;
const apiClientSecret = Buffer.from(process.env.SPQR_API_CLIENT_SECRET, "hex");
const merchantSecret = Buffer.from(process.env.SPQR_MERCHANT_SECRET, "hex");

const body = JSON.stringify({
  merchant: "acme_my",
  amount: 12500,
  currency: "MYR",
  external_id: "order-8402",
  country: "MY",
  payment_method: "duitnowqr",
  return_url: "https://shop.example.com/orders/order-8402",
  callback_url: "https://api.shop.example.com/webhooks/spqr",
  payer: { customer_uid: "customer-184" }
});

const timestamp = Math.floor(Date.now() / 1000).toString();
const bodyHash = crypto.createHash("sha256").update(body).digest("hex");
const idempotencyKey = crypto.randomUUID();
const canonical = `POST\n/payments\n${bodyHash}\n${timestamp}\n${idempotencyKey}\n`;
const inner = crypto.createHmac("sha256", merchantSecret).update(canonical).digest();
const signature = crypto.createHmac("sha256", apiClientSecret).update(inner).digest("hex");

const response = await fetch(`${apiBase}/payments`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Api-Key": apiKey,
    "X-Timestamp": timestamp,
    "X-Signature": signature,
    "Idempotency-Key": idempotencyKey
  },
  body
});

console.log(response.status, await response.json());
201CreatedThe request has been accepted.
{
  "payment_id": "0198f0d4-15cb-7c98-ae52-4dd112dc14cc",
  "status": "PENDING",
  "created_at": "2026-08-17T11:42:16.921Z",
  "client_action": {
    "type": "redirect",
    "url": "https://spqr-payments.com/pay/0198f0d4-15cb-7c98-ae52-4dd112dc14cc#checkout_token=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
    "data": {
      "payment_page_id": "0198f0d4-15cb-7c98-ae52-4dd112dc14cc",
      "checkout_token": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
    }
  }
}

In the default redirect mode, client_action.url is an SPQR hosted checkout. Pass the complete client_action.url to the browser, including #checkout_token=…; do not reconstruct or strip its fragment. Keep client_action.data opaque and never render it as payment instructions.

02
Request security

Authentication

Every merchant API request is authenticated with a timestamped, nested HMAC-SHA256 signature.

Public

X-Api-Key

Identifies the API client and scopes it to a franchise. Safe to include as a request header, but not a substitute for either secret.

Secret 01

Merchant secret

Signs the canonical request first. It binds the request to the merchant code in the body or query string.

Secret 02

API client secret

Signs the raw 32-byte inner digest. It binds the request to the calling integration.

Canonical request

Merchant API uses one signing protocol; no version header is required. The canonical UTF-8 string has five values, each followed by LF, including the last value. Trim leading and trailing whitespace from Idempotency-Key before signing. For requests without a body, including normal GET requests, hash an empty body. Use an empty fifth line when the key is absent. Use the exact escaped path and raw query string without decoding or reordering. Old signatures are rejected.

Plain text
<UPPERCASE_METHOD>
<PATH_WITH_OPTIONAL_RAW_QUERY>
<LOWERCASE_HEX_SHA256_OF_BODY>
<X_TIMESTAMP>
<TRIMMED_IDEMPOTENCY_KEY_OR_EMPTY>
Exact request bytescanonical
HMAC-SHA256
merchant secret
Raw 32 bytesinner digest
HMAC-SHA256
API client secret
Lowercase hexX-Signature
signature = hex(HMAC-SHA256(api_client_secret, HMAC-SHA256(merchant_secret, canonical)))
Clock window

The default accepted clock skew is ±5 minutes. Synchronize servers with NTP and generate a fresh timestamp for every retry.

GET query order

Sign the same raw query order you send. For example, /payments/{id}?merchant=acme_my.

Constant-time compare

Use a timing-safe comparison when verifying SPQR webhook signatures on your server.

03
Incoming funds

PayIns

Create a customer payment, present the returned client action when required, and use webhooks or retrieval to observe the final state.

POST/payments

Create a PayIn.

Reference

Request fields

FieldTypeDescription
merchantrequiredstringYour SPQR merchant code.
amountrequiredint64Positive amount in minor units. MYR 125.00 is 12500; in SPQR, VND 50,000 is 5000000. These are example amounts, not limits.
currencyrequiredstringThree-letter ISO 4217 code, such as MYR.
external_idrequiredstringYour order ID, from 1 to 128 characters after trimming. It identifies the order in your system, not the SPQR operation ID.
countryrequiredstringTwo-letter ISO 3166-1 alpha-2 code, such as MY.
payment_methodrequiredstringAn enabled SPQR method code for the requested country and currency.
checkout_modestringredirect (default) or h2h. See H2H QR.
qr_formatstringH2H only: url (default) or base64.
return_urlURICustomer return destination. Defaults to the URL in merchant settings.
callback_urlURIHTTPS webhook endpoint. Defaults to the URL in merchant settings.
payerobjectOptional payer metadata required by the selected payment method.
Open the SPQR checkout before polling

In redirect mode, when client_action.url is present, redirect the customer to that SPQR hosted page. The values in client_action.data are opaque checkout credentials: store or forward them only when needed, and never display them or turn them into a QR code.

Retrieve a PayIn

GET/payments/{id}?merchant={merchant_code}

Return the latest merchant-facing state.

Reference

The full path and raw query are part of the HMAC signature. A pending operation may be refreshed before the response is returned.

QR
Your payment interface

H2H QR

Display a payment QR in your own interface using the existing PayIn API.

H2H is available by default for supported payment methods and enabled merchant country/currency combinations, unless access has been disabled. A QR method name alone does not guarantee H2H support. Unsupported or unavailable combinations are rejected. Omitting checkout_mode keeps the standard redirect flow; there is no automatic fallback between modes.

Create a QR payment

Send a signed POST /payments with a new Idempotency-Key. Amounts are integer minor units: 12500 MYR is MYR 125.00. Payer fields depend on the method.

{
  "merchant": "acme_my",
  "amount": 12500,
  "currency": "MYR",
  "external_id": "order-8402",
  "country": "MY",
  "payment_method": "duitnowqr",
  "checkout_mode": "h2h",
  "qr_format": "url",
  "payer": { "customer_uid": "customer-184" }
}

qr_format accepts url (default) or base64, and is valid only with checkout_mode=h2h. New payments return HTTP 201; identical retries return HTTP 200 and the original operation. Changing mode or format with the same idempotency key returns HTTP 409.

Display a ready instruction

JSON response · ready URL
{
  "payment_id": "11111111-1111-4111-8111-111111111111",
  "status": "PENDING",
  "created_at": "2026-10-07T10:00:00Z",
  "checkout_mode": "h2h",
  "instruction_status": "ready",
  "client_action": {
    "type": "qr",
    "url": "https://api.spqr-payments.com/payment-assets/qr/<token>.png",
    "data": { "mime_type": "image/png", "expires_at": "2026-10-07T10:15:00Z" }
  }
}

Load client_action.url as an image. It grants access to one PNG and does not require HMAC, cookies, Origin or Referer. Treat the URL as sensitive and keep it out of logs. The image endpoint returns image/png with Cache-Control: no-store and X-Content-Type-Options: nosniff. An unknown token returns HTTP 404; an expired instruction or terminal payment returns HTTP 410.

Base64 client_action · request qr_format=base64
{
  "type": "qr",
  "data": {
    "mime_type": "image/png",
    "expires_at": "2026-10-07T10:15:00Z",
    "image_base64": "<Base64-encoded PNG>"
  }
}

For Base64, use data:image/png;base64, followed by image_base64 as the image source. There is no url field. Both formats use the same saved PNG. Use the returned expires_at; deadlines vary by method and are not extended by retries or reads.

Wait for QR preparation

JSON response · pending
{
  "payment_id": "11111111-1111-4111-8111-111111111111",
  "status": "PENDING",
  "created_at": "2026-10-07T10:00:00Z",
  "checkout_mode": "h2h",
  "instruction_status": "pending"
}

The payment already exists. Poll the signed GET /payments/{id}?merchant={merchant_code} with the same merchant code used at creation and backoff to retrieve its instruction. Do not issue a second create while waiting. Stop polling when the instruction is ready, leaves pending, or the payment becomes terminal.

instruction_statusMeaning and action
pendingPreparation is in progress. Retrieve the existing payment again.
readyDisplay the PNG in client_action until expires_at or payment completion.
unavailablePreparation could not produce a usable image. Stop QR polling, track monetary status and contact support; do not assume another create is safe.
expiredThe instruction deadline has passed. Remove the image and track payment status.
closedThe payment is terminal. Remove the image and use its final monetary status.

Only ready contains an image action. QR readiness is independent of payment status: ready does not mean paid, and a QR preparation error does not change monetary status. Continue using the standard signed payment webhooks.

04
Outgoing funds

PayOuts

Create a disbursement with recipient details. SPQR reserves operational balance before processing begins.

POST/payouts

Create and reserve a PayOut.

Reference

PayOut-specific data

The common fields match a PayIn, while recipient replaces payer. For bank transfers, send all values as strings:

  • bank_code — recipient bank identifier, such as VCB for Vietcombank in Vietnam
  • account_name — account holder name
  • account_number — account number with leading zeroes preserved

Read GET /capabilities?merchant=acme_my&operation_type=PAY_OUT for the required fields of your method. Customer identifiers also belong inside recipient: send "customer_uid": "customer-184" when recipient.customer_uid is listed, or "player_username": "customer184" when recipient.player_username is listed. Both are strings; neither is required for every payout. Do not send a top-level party object.

{
  "merchant": "acme_my",
  "amount": 25000,
  "currency": "MYR",
  "external_id": "withdrawal-184",
  "country": "MY",
  "payment_method": "bank_transfer",
  "callback_url": "https://api.shop.example.com/webhooks/spqr",
  "recipient": {
    "bank_code": "MBBEMYKL",
    "account_name": "Alex Tan",
    "account_number": "5144229981"
  }
}
A PayOut depends on available operational balance

The create call can return insufficient_balance with HTTP 422 when funds cannot be reserved. Failed or cancelled processing releases its reservation.

Retrieve a PayOut

GET/payouts/{id}?merchant={merchant_code}

Return the latest state and reservation ID when available.

Reference
05
Merchant configuration

Capabilities

Discover the countries, currencies and payment methods currently configured for your merchant before you build a PayIn or PayOut request.

GET/capabilities?merchant={merchant_code}&operation_type=PAY_IN

Return the method catalog configured for your account.

Reference
Build choices from this endpoint

Call capabilities from your backend and use the returned country, currency, method and amount boundaries in your checkout. Do not hardcode the catalog: your enabled configuration can change independently of an API release.

What the response means

  • operation_enabled reports whether this direction is enabled for your merchant.
  • configured reports whether a published configuration exists for the direction.
  • Amounts are inclusive integer minor units; max_amount: 0 means no configured upper boundary.
  • required_fields lists extra request fields required by the method.
  • features is forward-compatible; ignore keys your integration does not recognize.

Sign the exact GET query

GET requests use the same nested HMAC construction as create and retrieve calls. Hash an empty body and sign the path with the exact query order you send.

Node.js 18+
const path = "/capabilities?merchant=acme_my&operation_type=PAY_IN";
const timestamp = Math.floor(Date.now() / 1000).toString();
const bodyHash = crypto.createHash("sha256").update("").digest("hex");
const canonical = `GET\n${path}\n${bodyHash}\n${timestamp}\n\n`;
const inner = crypto.createHmac("sha256", merchantSecret).update(canonical).digest();
const signature = crypto.createHmac("sha256", apiClientSecret).update(inner).digest("hex");

const response = await fetch(`${apiBase}${path}`, {
  headers: {
    "X-Api-Key": apiKey,
    "X-Timestamp": timestamp,
    "X-Signature": signature
  }
});
Empty is valid

A disabled or not-yet-configured direction returns HTTP 200 with an empty array and explanatory flags.

Configuration, not live health

The response describes configured eligibility and amount boundaries. It does not guarantee instantaneous processing availability.

Short private cache

Respect the response ETag and cache privately for at most 30 seconds. Revalidate with If-None-Match.

Empty response examples

Direction disabled · 200
{
  "merchant": "acme_my",
  "operation_type": "PAY_OUT",
  "operation_enabled": false,
  "configured": false,
  "capabilities": []
}
No published configuration · 200
{
  "merchant": "acme_my",
  "operation_type": "PAY_IN",
  "operation_enabled": true,
  "configured": false,
  "capabilities": []
}
Discovery is not a transaction allowlist

During the current rollout, a country/currency pair missing from this catalog is not rejected by a separate geography check. SPQR evaluates each submitted transaction independently; use its create response as the final answer.

06
Method catalog

Payment methods

Method codes are stable merchant-facing values. Use GET /capabilities as the authoritative catalog enabled for your account.

Method examples

Availability varies by merchant. Use the capabilities response and your onboarding terms for live requests.

FlowMethod codeCustomer experienceCountry · Currency
PayInduitnowqrDuitNow QRMY · MYR
PayInfpxFPX online bankingMY · MYR
PayInbank_transferHosted bank transferMY · MYR
PayIntngTouch 'n Go directMY · MYR
PayIntngqrTouch 'n Go QRMY · MYR
PayIngrabpayGrabPay directMY · MYR
PayIngrabpayqrGrabPay QRMY · MYR
PayInshopeeShopeePay directMY · MYR
PayIngxbankqrGXBank QRMY · MYR
PayInboostqrBoost QRMY · MYR
PayInmaybankqrMaybank QRPayMY · MYR
PayInrhbbankqrRHB Bank QRPayMY · MYR
PayInebankOnline bankingMY · MYR
PayOutbank_transferBank account transferMY · MYR
PayOuttng_payoutTouch 'n Go payoutMY · MYR
PayInmomopayMoMo Pay QRVN · VND
PayInzalopayZaloPay QRVN · VND
PayInvietqrVietQRVN · VND
PayInbank_transferHosted bank transfer QRVN · VND
PayOutbank_transferBank account transferVN · VND

Additional corridors can be enabled after commercial and operational approval. They are not listed as generally available until then.

07
Operation lifecycle

Statuses

PayIns and PayOuts share one merchant-facing state machine. Terminal states do not transition again.

NEWCreated locally
PENDINGProcessing
SUCCEEDEDFull or partial payment
FAILEDTerminal failure
CANCELLEDCancelled

NEW can also move directly to CANCELLED. The status_times object records the first UTC timestamp at which each state family was reached.

Resolution gives the reason

Status answers what happened; resolution explains why. Use both in operational dashboards and customer-support tooling.

okSuccessful terminal outcome method_unavailableNo eligible payment method processing_error_retryableRetryable processing fault processing_error_terminalPermanent processing decline limit_violationAmount or turnover limit attempt_limit_reachedAll attempts exhausted insufficient_balanceOperational funds unavailable cancelledOperation was cancelled
Pause automation when moderation is required

A retryable processing error can set moderation_required: true. Keep the operation in an operator-review flow instead of treating it as a safe final decline.

08
Asynchronous updates

Webhooks

SPQR posts a signed event to the request callback URL, or to the default callback URL in merchant settings, after each status transition.

Each operation keeps the X-Api-Key of the integration that created it. Verify its webhooks with that integration’s current client secret and your merchant secret. Retries keep the same key ID and event_id. Disabled, missing or foreign keys stop delivery; SPQR does not select another integration.

Rotating the secret under the same key ID affects the next delivery attempt. When replacing a key ID, keep the old client enabled until its operations and queued deliveries finish. Older operations and internal operations without an authenticated API client need an explicitly established binding before delivery.

Callback and return URLs are saved at first creation. Later merchant settings apply to new operations. Replaying an unchanged request with its original idempotency key returns the original operation; changing an explicitly supplied URL returns 409 idempotency_mismatch. An operation created without a callback keeps that absence.

Delivery contract

  • Respond with any 2xx statusNon-2xx responses and network failures are retried with bounded exponential backoff.
  • Deduplicate by event_idThe event ID remains stable across delivery attempts.
  • Verify before processingWebhooks retain the four-line nested HMAC protocol: method, escaped path with raw query, body hash, timestamp. They have no idempotency line.
  • Return quicklyPersist the event, return 2xx, and continue business work asynchronously.
payment.status_changed
{
  "event_id": "0198f0d9-d219-77e8-9419-c2dbfc54889d",
  "event_type": "payment.status_changed",
  "merchant": "acme_my",
  "operation_type": "payment",
  "operation_id": "0198f0d4-15cb-7c98-ae52-4dd112dc14cc",
  "external_id": "order-8402",
  "status": "SUCCEEDED",
  "resolution": "ok",
  "moderation_required": false,
  "status_times": {
    "new_at": "2026-08-17T11:42:16.921Z",
    "pending_at": "2026-08-17T11:42:17.244Z",
    "succeeded_at": "2026-08-17T11:42:31.808Z"
  },
  "amount": 12500,
  "paid_amount": 12500,
  "currency": "MYR"
}

SUCCEEDED covers full and partial payments. In the full example above, paid_amount equals amount. A partial payment can instead contain:

Partial payment amounts
{ "status": "SUCCEEDED", "amount": 12500, "paid_amount": 7500 }

Credit the customer using paid_amount; check that it equals amount before fulfilling a full order. SPQR records the actually paid gross amount; net and fees are separate. A success confirmation without a separately reported paid amount is currently treated as full payment. Always use the returned paid_amount.

Persist event_id under a unique constraint with the event before returning 2xx. Repeated delivery must not repeat the business effect. Delivery order is not guaranteed: reconcile through GET instead of applying an older event over newer state. Default delivery is limited to eight attempts with exponential backoff bounded between 200 ms and five minutes; deployment settings may differ. Each attempt has a fresh timestamp and signature. There is no guaranteed elapsed-time delivery deadline; after exhaustion automatic retries stop. Recover missed state through GET by operation ID or external_id.

Verify the callback

Webhook signatures use four LF-terminated lines: the uppercase method, exact escaped request path with its raw query, SHA-256 hex digest of the raw body, and timestamp. The last LF is required. Use the received originalUrl without decoding the path, rebuilding the query, sorting parameters, or changing repeated parameters. Read the raw body before JSON parsing and compare the signature in constant time. This is the current webhook contract; decoded-path signatures are not supported.

Express-style Node.js
function verifySPQRWebhook({ method, originalUrl, rawBody, headers, nowSeconds = Math.floor(Date.now() / 1000) }) {
  if (!headers || typeof method !== "string" || method.toUpperCase() !== "POST" ||
      typeof originalUrl !== "string" || !originalUrl.startsWith("/") ||
      !Buffer.isBuffer(rawBody) || !Number.isSafeInteger(nowSeconds)) return false;
  if (typeof apiKey !== "string" || !apiKey || headers["x-api-key"] !== apiKey) return false;
  const timestamp = headers["x-timestamp"];
  const received = headers["x-signature"];
  if (typeof timestamp !== "string" || !/^[0-9]+$/.test(timestamp) ||
      typeof received !== "string" || !/^[0-9a-f]{64}$/.test(received)) return false;
  const seconds = Number(timestamp);
  if (!Number.isSafeInteger(seconds) || Math.abs(nowSeconds - seconds) > 300) return false;
  const bodyHash = crypto.createHash("sha256").update(rawBody).digest("hex");
  const canonical = `${method.toUpperCase()}\n${originalUrl}\n${bodyHash}\n${timestamp}\n`;
  const inner = crypto.createHmac("sha256", merchantSecret).update(canonical).digest();
  const expected = crypto.createHmac("sha256", apiClientSecret).update(inner).digest();
  return crypto.timingSafeEqual(Buffer.from(received, "hex"), expected);
}

Persistent event inbox

After signature verification, store each event in PostgreSQL. A duplicate insert returns no row; acknowledge it without scheduling the business work again. Keep the inbox and a durable work item in the same transaction. The worker also needs an idempotent business effect. In-memory sets lose deduplication after restart.

PostgreSQL inbox
CREATE TABLE spqr_webhook_inbox (
  event_id uuid PRIMARY KEY,
  payload jsonb NOT NULL,
  received_at timestamptz NOT NULL DEFAULT now(),
  processed_at timestamptz
);

-- Bind event_id and the verified JSON body as parameters.
INSERT INTO spqr_webhook_inbox (event_id, payload)
VALUES ($1, $2)
ON CONFLICT (event_id) DO NOTHING
RETURNING event_id;

PayOut events use payout.status_changed, operation_type: "payout", and also include the original recipient object.

Recover an operation

The merchant assigns external_id (1–128 characters after trimming). It is unique for that merchant separately for PayIn and PayOut, regardless of currency or method, including FAILED and CANCELLED operations. Matching is case-sensitive; leading and trailing whitespace is trimmed. A new key with an existing external ID returns HTTP 409 external_id_conflict, even when the body is identical.

Use signed GET /payments?merchant=acme_my&external_id=order-8402 or GET /payouts?merchant=acme_my&external_id=withdrawal-184 if you lost the SPQR ID. URL-encode query values before signing the exact path and query. The response is the same as GET by operation ID; unknown IDs return 404. GET also restores a saved SPQR redirect action for an unfinished PayIn without creating another operation.

An error after acceptance includes error.operation with operation_type, operation_id, external_id and the current status (null when state cannot be read after the failure). The original key is already claimed. An uncertain result must be recovered with that key; after a terminal rejection, a new attempt requires both a new external ID and a new key. An identical replay can return HTTP 200 with the saved CANCELLED operation after the initial rejection.

Accepted payout rejection
{
  "error": {
    "code": "merchant_balance_insufficient",
    "message": "merchant balance is insufficient",
    "operation": {
      "operation_type": "payout",
      "operation_id": "0198f0d4-15cb-7c98-ae52-4dd112dc14cc",
      "external_id": "withdrawal-184",
      "status": "CANCELLED"
    }
  }
}
09
Failure handling

Errors

Signed merchant API errors use a consistent JSON envelope. PNG assets return an image only on success; their 404, 410 and 503 responses have no image body. Log request_id and share it with the integration team when troubleshooting.

409Idempotency mismatch

The key was already used for a different logical request.

application/json
{
  "error": {
    "code": "idempotency_mismatch",
    "message": "idempotency key reused with different request body",
    "request_id": "7c6bfb2944246c12",
    "details": []
  }
}
HTTPCodeRecommended handling
400validation_errorCorrect the request. Use details for field-level feedback.
401unauthorizedCheck the API key, merchant scope, timestamp, and credential status.
401invalid_signatureCompare exact body bytes, path/query, trailing newlines, and secret decoding.
403ip_not_allowedAdd your backend IP to the merchant API IP whitelist. An empty list blocks access. QR images remain accessible to customers.
403merchant_disabledContact SPQR operations; the merchant or requested flow is disabled.
404not_foundConfirm the merchant code and operation ID are in the authenticated scope.
409idempotency_mismatchDo not retry with this key. Investigate the conflicting request.
409conflictRetrieve the operation and reconcile its current state.
409external_id_conflictRead the existing operation from error.operation. Reuse the original key to recover it.
422merchant_balance_insufficientMerchant funds are insufficient. Check the accepted operation before starting a new attempt.
422method_unavailableNo compatible method meets the request conditions. Check the method, country, currency and amount.
422limit_exceededThe amount is outside permitted limits. Change the amount or check applicable merchant limits.
429rate_limitedWait at least Retry-After seconds; retain the original body and key, with a fresh timestamp and signature.
503processing_capacity_unavailableProcessing capacity is unavailable; adding merchant funds may not resolve it. Check error.operation.
503processing_configuration_unavailableProcessing configuration is unavailable. Contact SPQR support and check error.operation.
500internal_errorRecover using the same key and identical body with a fresh signature; inspect error.operation when present.
10
Endpoint details

API reference

The public contract contains seven signed merchant endpoints and one QR image endpoint. Status mutation endpoints are private to SPQR services.

Required headersX-Api-Key · X-Timestamp · X-Signature · Idempotency-Key
Success201 on create; 200 on an identical idempotent replay.
Request schemamerchant, amount, currency, external_id, country, and payment_method are required.
Response schemapayment_id, status, created_at, with client_action for redirect checkout or a ready H2H QR. H2H also returns checkout_mode and instruction_status.
Machine-readable contract

Build from the OpenAPI specification

Download the merchant-only YAML contract for code generation, API clients, or contract tests.

Download YAML
Copied to clipboard