Developers · Documentation

Build with Paynexus.
Production in days.

Everything you need to onboard a merchant, accept your first payment, and go live. REST API, a spec-driven PHP SDK, and a deterministic sandbox — all covered in this guide.

01 · Introduction

What is Paynexus

Paynexus is a PSP orchestration platform. You integrate once with our REST API, and we route transactions across 100+ payment providers — crypto and fiat — with smart routing, automatic failover, idempotency, and verified callbacks.

What you get

  • REST API — the primary interface. Language-agnostic. Covers deposits, withdrawals, balances, payways, and webhooks.
  • PHP SDK — framework-agnostic (PHP 7.4+). Spec-driven provider pattern. Use it when you need direct PSP access or want to package a new provider.
  • Admin Cabinet — multi-tenant panel with RBAC for merchants, routing rules, API keys, and transaction monitoring.
  • Sandbox — deterministic test environment with the Debug module. Every scenario (success / pending / failed / declined / transport_error) is replayable.

Base URLs

EnvironmentBase URL
Productionhttps://api.paynexus.io
Sandboxhttps://api-sandbox.paynexus.io
New here?

Jump to the Quickstart — you'll have a working deposit in under 10 minutes.

02 · Quickstart

From zero to first deposit

This walkthrough connects a merchant, accepts a test payment, and verifies a webhook — end-to-end in five steps.

1. Sign up and create a merchant

Sign up in the Cabinet and create your first merchant. You'll choose a rate-limit tier (Standard / Premium / Unlimited) and one or more currencies.

2. Issue API keys

In Cabinet → Settings → API Keys, generate a key pair. Store both values in your environment:

.env
ENV
# Sandbox keys for development PN_KEY=pk_test_4f2a19c8a4b3... PN_SECRET=sk_test_7e3b28d9c5d6... PN_WEBHOOK_SECRET=whsec_3a2d19...

3. Create your first deposit

Send a POST /api/v1/deposits with the merchant's key pair. We return a transaction ID, status, and a redirect_url for the customer.

deposit.ts
TypeScript
const r = await fetch("https://api-sandbox.paynexus.io/api/v1/deposits", { method: "POST", headers: { "X-Api-Key": process.env.PN_KEY, "X-Api-Secret": process.env.PN_SECRET, "X-Idempotency-Key": "order-0001", "Content-Type": "application/json", }, body: JSON.stringify({ amount: "100.00", currency: "USD", payway: "CARD", bill_id: "order-0001", callback_url: "https://your-app.com/webhooks/paynexus", }), }); const { id, status, redirect_url } = await r.json(); // → { id: "txn_test_01HK...", status: "pending", // redirect_url: "https://api-sandbox.paynexus.io/redirect/..." }

4. Receive the webhook

Once the customer completes the payment (or the sandbox scenario resolves), we POST to your callback_url with the final transaction state. See Webhooks for verification.

5. Go live

  1. Swap base URL from api-sandbox.paynexus.io to api.paynexus.io.
  2. Replace pk_test_* keys with pk_live_*.
  3. Configure your production routing rules and PSP connections in the Cabinet.
  4. Whitelist Paynexus callback IPs on your webhook endpoint.
03 · Authentication

API keys & headers

Every request requires two headers:

headers
HTTP
X-Api-Key: pk_live_4f2a19c8a4b3... X-Api-Secret: sk_live_7e3b28d9c5d6...

Key pairs per environment

Sandbox keys are prefixed pk_test_* / sk_test_*. Production uses pk_live_* / sk_live_*. A merchant can hold multiple active key pairs — useful for zero-downtime rotation.

Rotation

Generate a new pair in Cabinet → Settings → API Keys. Both the old and new pair work for a 24-hour overlap window; after that, the old pair is revoked automatically.

Protect your secrets.

Never commit sk_* values to git. Use environment variables or a secret manager. Five failed auth attempts per five minutes will IP-block the source for one hour.

04 · Create a deposit

POST /api/v1/deposits

Creates a deposit transaction and routes it to the optimal PSP per your merchant's routing rules. Returns a redirect URL (for redirect flows) or host-to-host payment details (for crypto and some card flows).

Request body

FieldTypeDescription
amount requiredstringDecimal amount. Always a string to avoid float precision issues.
currency requiredstringISO 4217 code (USD, EUR, RUB...).
payway requiredstringPayment method code: CARD, SBP, CRYPTO_BTC, CRYPTO_USDT_TRC20, etc. Fetch available values from GET /api/v1/payways.
bill_id requiredstringYour internal order ID. Must be unique per merchant.
callback_url requiredstringWhere Paynexus POSTs the webhook when the status changes.
customer optionalobject{ id, email, ip, country } — used by routing and fraud rules.
metadata optionalobjectUp to 10 custom key-value pairs. Echoed back in webhooks.

Response

201 Created
JSON
{ "id": "txn_01HK4B8X...", "status": "pending", "redirect_url": "https://pay.piastrix.com/s/a1b2...", "external_id": "piastrix_729183", "provider": "piastrix", "amount": "100.00", "currency": "USD", "created_at": "2026-04-19T14:22:01Z" }

Flows

  • Redirect flow — we return a redirect_url. Redirect the customer; they pay on the PSP's page; PSP calls us; we call your webhook.
  • Host-to-host (H2H) — for crypto and some P2P card flows. Instead of redirect_url, the response includes a payment_details object with crypto address, QR code data, or bank details to render inline.
05 · Create a withdrawal

POST /api/v1/withdrawals

Creates a withdrawal request. The request enters pending status and our orchestration daemon processes it asynchronously, running it through a validation chain before selecting a PaymentAccount and calling the PSP.

Request body

FieldTypeDescription
amount requiredstringDecimal amount.
currency requiredstringISO 4217 code.
payway requiredstringPayment method code for the payout.
destination requiredobjectBeneficiary account — card number, crypto address, SBP phone, bank details.
bill_id requiredstringYour internal payout ID.
callback_url requiredstringWebhook target for status updates.
cascade_interrupt optionalbooleanWhen true, abort instead of trying fallback accounts on first failure. Default false.

Validation chain

Before dispatching to a PSP, the daemon validates the request against a configurable chain of rules: merchant balance, request limits (per-request / per-day), blacklists, country restrictions, payway availability, currency match, and KYC status. Any validator failure returns 422 Unprocessable Entity with a specific error code.

Cascade failover

If the primary PSP declines, we automatically try the next account in your routing chain. Each attempt is logged in transaction_attempts and visible in the Cabinet. Opt out per-withdrawal by setting cascade_interrupt: true.

Status check: GET /api/v1/withdrawals/{id}. Poll at most once every 30 seconds — webhooks are the recommended mechanism.

06 · Webhooks

Handle transaction events

When a transaction changes state, Paynexus POSTs a JSON event to your callback_url. Verify every webhook before processing it.

Event payload

webhook.json
JSON
{ "event": "deposit.completed", "transaction": { "id": "txn_01HK4B8X...", "external_id": "piastrix_729183", "bill_id": "order-0001", "status": "completed", "amount": "100.00", "currency": "USD", "payway": "CARD", "provider": "piastrix", "completed_at": "2026-04-19T14:24:17Z" }, "nonce": "a1b2c3d4e5f6...", "timestamp": 1713537857 }

Event types

  • deposit.pending · deposit.completed · deposit.failed
  • withdrawal.pending · withdrawal.completed · withdrawal.failed
  • refund.completed · chargeback.opened

Signature verification

Each webhook includes an X-Paynexus-Signature header. Compute HMAC-SHA256 over the raw request body using your webhook secret (Cabinet → Settings → Webhook Secrets).

verify.ts
TypeScript
import { createHmac, timingSafeEqual } from "crypto"; function verifyWebhook(rawBody: string, signatureHeader: string, secret: string) { const expected = "sha256=" + createHmac("sha256", secret) .update(rawBody) .digest("hex"); return timingSafeEqual( Buffer.from(expected), Buffer.from(signatureHeader), ); } // In your handler: if (!verifyWebhook(rawBody, req.headers["x-paynexus-signature"], process.env.PN_WEBHOOK_SECRET)) { return res.status(401).end(); }

Retry policy

If your endpoint doesn't return 2xx within 5 seconds, we retry with exponential backoff:

AttemptDelay
1immediate
2+2 seconds
3+8 seconds
4+32 seconds
5+2 minutes

After 5 failed attempts, delivery is marked failed. You can replay any failed webhook from the Cabinet.

07 · Idempotency

Safe retries by default

All mutating endpoints (POST /deposits, POST /withdrawals) accept an optional X-Idempotency-Key header. We cache the response for 24 hours keyed on that value.

Behavior

  • TTL — 24 hours.
  • Payload match — SHA-256 hash of the body must match the original request. Different body with same key → 409 Conflict.
  • Replay safety — network errors mid-flight can be retried with the same key without creating a duplicate transaction.
  • Recommended format — your internal order ID or a UUIDv4.
retry.ts
TypeScript
// Safe to retry on network errors — same key, same response. async function createDepositWithRetry(order: Order) { for (let attempt = 0; attempt < 3; attempt++) { try { return await fetch("/api/v1/deposits", { method: "POST", headers: { "X-Api-Key": key, "X-Api-Secret": secret, "X-Idempotency-Key": order.id, // ← your order ID }, body: JSON.stringify(order), }); } catch (e) { if (attempt === 2) throw e; await sleep(500 * (attempt + 1)); } } }
08 · Errors & rate limits

Predictable failures

All 4xx and 5xx responses share the same envelope:

error.json
JSON
{ "error": { "code": "insufficient_balance", "message": "Merchant balance too low for this payway", "details": { "required": "100.00", "available": "47.50" }, "request_id": "req_01HK4B8X..." } }

Common error codes

CodeHTTPMeaning
invalid_api_key401Key pair is unknown, revoked, or expired.
validation_failed422Request body failed field validation. See details.
idempotency_conflict409Same key, different payload.
insufficient_balance422Merchant balance too low.
routing_failed503No PSP available for this payway/currency.
upstream_error502PSP returned an unexpected response.
rate_limit_exceeded429Too many requests. See Retry-After header.

Rate limits

TierPer merchantConcurrent
Standard (Free)60 req/min5
Premium300 req/min20
Unlimited10,000 req/min100

Per-endpoint caps: deposits 30 req/min, withdrawals 20 req/min. Per-IP cap: 300 req/min. On a 429 response, respect the Retry-After header.

Circuit breaker

If a specific PSP returns 5+ failures within 60 seconds, our circuit breaker opens for that provider for 60 seconds and reroutes to the next account in your chain. The breaker transitions CLOSED → OPEN → HALF_OPEN → CLOSED. This is transparent — you'll see it reflected in attempt logs but your request flow is unaffected.

09 · PHP SDK

Spec-driven provider integration

The payment-core/sdk package is a framework-agnostic PHP library (PHP 7.4+). Use it when you need direct PSP access without going through the REST API, or when you want to package a new PSP as a reusable module.

Install

terminal
Shell
# Core SDK + the modules you need composer require paynexus/sdk-core composer require paynexus/module-piastrix composer require paynexus/module-ampay

Basic usage

deposit.php
PHP
use Paynexus\SDK\PaymentFacade; use Paynexus\SDK\Provider\ProviderRegistry; $sdk = new PaymentFacade($registry, $httpClient); $invoice = $sdk->createDepositInvoice('piastrix', $accountConfig, [ 'amount' => '100.00', 'currency' => 'USD', 'payway' => 'CARD', 'bill_id' => 'order-4729', 'callback_url' => 'https://merchant.app/cb', ]); echo $invoice->redirectUrl; // → https://pay.piastrix.com/s/a1b2c3...

Facade methods

MethodReturns
createDepositInvoice()Invoice
createWithdraw()WithdrawResult
handleCallback()CallbackReceipt
checkStatus()StatusResult
getBalance()Money
capabilities()CapabilitySet
paywaySpec()PaywaySpec

Adding a new PSP module

Every integration is described declaratively in a PHP spec file — not in code. Core reads the spec and auto-generates HTTP requests, response mapping, and callback parsing.

newpsp.spec.php
PHP
return [ 'CARD' => [ 'flow' => 'redirect', 'capabilities' => ['deposit.create', 'deposit.redirect'], 'limits' => ['min_amount' => '1.00', 'max_amount' => '100000.00'], 'currencies' => ['USD', 'EUR'], 'request' => [ 'path' => '/deposit', 'method' => 'POST', 'json' => [ 'amount' => '{cmd.amount}', 'currency' => '{cmd.currency}', 'bill_id' => '{cmd.bill_id}', ], ], 'response' => [ 'external_id' => 'data.id', 'redirect_url' => 'data.url', ], ], ];

A full new-provider checklist:

  1. Create packages/modules/{new-psp}/config/{new-psp}.spec.php.
  2. Implement {NewPsp}Signer.php if the PSP uses a non-standard signature scheme.
  3. Write {NewPsp}Factory.php — returns a configured ConfigurableRestProvider.
  4. Register the module in your ProviderRegistry.
  5. Add unit tests for the signer and integration tests for the spec.
  6. Register in apps/api/config/orchestration.php → supported_providers.

Already-packaged modules

  • paynexus/module-debug — fake provider for sandbox (no HTTP, deterministic scenarios).
  • paynexus/module-piastrix — sorted-values HMAC-SHA256 signatures.
  • paynexus/module-ampay — sorted-values HMAC-SHA512, 22 deposit payways / 5 withdraw payways.
  • paynexus/module-betterbro — HMAC-SHA256 + Bearer token.
  • paynexus/module-unlimit — HMAC with validation chain.
10 · Sandbox & testing

Deterministic scenarios

Every endpoint has a sandbox counterpart at https://api-sandbox.paynexus.io. Use your pk_test_* keys. No real PSPs are contacted.

Debug scenarios

Pass _debug_scenario in any request body to force specific outcomes — perfect for wiring up error-handling code paths without hitting real providers.

ValueBehavior
success (default)Normal success flow. Deposit completes via webhook after ~2s.
pendingTransaction stays pending indefinitely. Good for testing timeouts.
failedReturns HTTP 422. Tests synchronous error handling.
declinedAccepts the request but webhook delivers deposit.failed.
transport_errorSimulates an upstream network error (HTTP 502). Tests retry logic.

Example: testing a declined deposit

test.ts
TypeScript
await fetch("https://api-sandbox.paynexus.io/api/v1/deposits", { method: "POST", headers: { ...headers, "X-Idempotency-Key": "test-decline-001" }, body: JSON.stringify({ amount: "100.00", currency: "USD", payway: "CARD", bill_id: "test-decline-001", callback_url: "https://your-tunnel.ngrok.io/cb", _debug_scenario: "declined", // ← force a decline }), }); // Webhook fires in ~2s with event: "deposit.failed"

Receiving webhooks locally

Use ngrok, webhook.site, or Cloudflare Tunnel to expose your local endpoint. Sandbox webhooks are signed with your sandbox webhook secret (Cabinet → Settings → Webhook Secrets → Sandbox).

Deterministic IDs.

Sandbox transaction IDs follow the pattern txn_test_<timestamp>_<nonce> so you can assert against them in automated tests.

Need help?

Open an issue on GitHub, or email arnon.hs.btc@gmail.com.