PayXif
Home

PayXif API v1

Accept crypto payments from any app, website, game or Telegram bot. Base URL https://api.payxif.com. Every response is JSON:

{ "ok": true,  "data":  { ... } }
{ "ok": false, "error": { "code": "unsupported_currency", "message": "..." } }

You create an invoice for an amount in USD (or a specific coin). The customer opens the hosted checkout, picks any coin you accept, and pays. When the payment confirms on-chain, PayXif credits your wallet and sends a signed webhook so your app can deliver the product automatically. No KYC, no accounts for your customers.

Quickstart

  1. In the dashboard open Merchant service → create an API key (starts with pxk_live_). Set a default Webhook URL.
  2. Create an invoice from your backend (below) and redirect the customer to data.pay_link.
  3. Receive the invoice.paid webhook, verify its signature, and fulfil the order.
curl -X POST https://api.payxif.com/v1/invoices \
  -H "X-API-Key: pxk_live_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "amount_usd": "49.99",
    "order_id": "A-1042",
    "description": "Pro plan - 1 month",
    "callback_url": "https://shop.example/payxif/webhook",
    "return_url": "https://shop.example/thanks"
  }'

Response:

HTTP/1.1 201 Created
{
  "ok": true,
  "data": {
    "track_id":       "a1B2c3D4",
    "status":         "new",
    "source":         "api",
    "currency":       null,          // set once the customer picks a coin
    "network":        null,
    "amount":         null,          // coin amount, set after coin choice
    "amount_usd":     "49.99",
    "paid_usd":       0,
    "rate_usd":       null,          // USD->coin rate, locked at coin choice
    "mixed_payment":  false,
    "paid_amount":    "0",
    "pending_amount": "0",
    "remaining_amount": "0",
    "fee":            "0",
    "address":        null,          // a deposit address is created after coin choice
    "payment_uri":    null,          // e.g. tron:TR7N...?amount=... once a coin is picked
    "pay_link":       "https://payxif.com/pay/a1B2c3D4",
    "order_id":       "A-1042",
    "description":    "Pro plan - 1 month",
    "email":          null,
    "callback_url":   "https://shop.example/payxif/webhook",
    "return_url":     "https://shop.example/thanks",
    "cancel_url":     null,
    "underpaid_cover": 0,
    "metadata":       null,
    "expires_at":     "2026-08-29T12:34:56+00:00",
    "expires_in":     3600,
    "paid_at":        null,
    "created_at":     "2026-08-29T11:34:56+00:00"
  }
}

Authentication

Send the API key in any of these headers. Merchant keys start with pxk_live_ (mainnet) or pxk_test_ (testnet). Payout keys start with pxp_ and are used only for the Payout API. Keep keys server-side — never ship them in a browser or mobile app.

X-API-Key: pxk_live_xxxxxxxx
Authorization: Bearer pxk_live_xxxxxxxx
merchant_api_key: pxk_live_xxxxxxxx
pxk_live_merchant · mainnetcreate & read invoices, read balance and profile
pxk_test_merchant · testnetsame, against the test network (no real funds)
pxp_live_payout · mainnetPayout API only — created with password + 2FA, optional IP allowlist

A missing or wrong key returns 401 unauthorized. A payout key that hits a merchant endpoint (or vice-versa) is rejected. If the key has an IP allowlist and the caller IP is not on it, you get 403 ip_not_allowed.

Response format

Every endpoint returns JSON with a top-level ok boolean. Success carries data; list endpoints add meta for pagination. Failure carries error with a stable machine code plus a human message. Always branch on error.code, never on the message text.

// success
{ "ok": true, "data": { ... } }

// paginated list
{ "ok": true, "data": [ ... ], "meta": { "page": 1, "per_page": 25, "total": 134, "last_page": 6 } }

// validation error (HTTP 422)
{ "ok": false, "error": {
    "code": "validation_failed",
    "message": "The given data was invalid.",
    "fields": { "amount_usd": ["The amount usd field is required."] }
} }

// domain / auth error (HTTP status varies — see Error codes)
{ "ok": false, "error": { "code": "currency_not_accepted", "message": "USDT on TRON is not enabled for this merchant" } }

HTTP status codes used: 200 OK, 201 created, 400 bad request, 401 unauthorized, 403 forbidden, 404 not found, 422 validation failed, 429 rate limited, 503 maintenance / feature disabled.

Endpoint reference

Every endpoint at a glance. Auth shows which key each needs.

GET/v1/mepxk_merchant profile, fee %, network mode
GET/v1/balancepxk_your wallet balances per coin/network
POST/v1/invoicespxk_create an invoice (USD or fixed coin)
GET/v1/invoicespxk_list invoices (filter + paginate)
GET/v1/invoices/{track_id}pxk_one invoice, includes txs[]
POST/v1/payoutpxp_send a withdrawal to an address
GET/v1/payoutspxp_list payouts made via the API
GET/v1/payout/{id}pxp_one payout with status + txid
POST/pay/{track_id}/simulatepublicsandbox only: complete a test invoice
GET/v1/currenciespublicenabled coins, networks, contracts, minimums
GET/v1/metapublicsite name & default theme (bootstrap)

Create invoice

POST /v1/invoices — price in USD (customer picks the coin at checkout) or in a specific coin with amount + currency. Returns 201 with the invoice object; if you resend the same order_id the existing open invoice is returned with 200 (see Idempotency).

amount_usdstringUSD amount, e.g. "49.99" — customer chooses the coin. Either this OR amount+currency.
amountstringcoin amount, e.g. "25.50" (use with currency)
currencystringUSDT, TRX, USDC, … (required with amount)
networkstringTRON (default), BSC, ETH, POLYGON
order_idstringyour reference, echoed in every webhook; also the idempotency key
descriptionstringshown on the payment page
emailstringcustomer email (optional)
payer_namestringpre-fill the payer name field (optional)
payer_notestringpre-fill a note from the payer (optional)
callback_urlurlwebhook for this invoice (falls back to your merchant default)
return_urlurlwhere the customer returns after paying
cancel_urlurlwhere the customer goes if they cancel / abandon checkout
lifetimeintminutes, 5–1440, default 60 — the USD→coin rate is locked until expiry
underpaid_covernumber% shortfall still accepted as paid (0–60)
fee_paid_by_payerbooladd the service fee on top of the customer total
mixed_paymentboolallow several transfers / coins until the USD total is reached
metadataobjectup to 20 keys, echoed back in webhooks (great for a Telegram chat id)
sandboxboolcreate a TEST invoice — no chain, no ledger; complete it with the Simulate button (see Sandbox & testing)
curl -X POST https://api.payxif.com/v1/invoices \
  -H "X-API-Key: pxk_live_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "amount_usd": "49.99",
    "order_id": "A-1042",
    "description": "Pro plan - 1 month",
    "callback_url": "https://shop.example/payxif/webhook",
    "return_url": "https://shop.example/thanks"
  }'

Full invoice object (the data field). Amount fields are strings to avoid float rounding; USD fields are numbers.

HTTP/1.1 201 Created
{
  "ok": true,
  "data": {
    "track_id":       "a1B2c3D4",
    "status":         "new",
    "source":         "api",
    "currency":       null,          // set once the customer picks a coin
    "network":        null,
    "amount":         null,          // coin amount, set after coin choice
    "amount_usd":     "49.99",
    "paid_usd":       0,
    "rate_usd":       null,          // USD->coin rate, locked at coin choice
    "mixed_payment":  false,
    "paid_amount":    "0",
    "pending_amount": "0",
    "remaining_amount": "0",
    "fee":            "0",
    "address":        null,          // a deposit address is created after coin choice
    "payment_uri":    null,          // e.g. tron:TR7N...?amount=... once a coin is picked
    "pay_link":       "https://payxif.com/pay/a1B2c3D4",
    "order_id":       "A-1042",
    "description":    "Pro plan - 1 month",
    "email":          null,
    "callback_url":   "https://shop.example/payxif/webhook",
    "return_url":     "https://shop.example/thanks",
    "cancel_url":     null,
    "underpaid_cover": 0,
    "metadata":       null,
    "expires_at":     "2026-08-29T12:34:56+00:00",
    "expires_in":     3600,
    "paid_at":        null,
    "created_at":     "2026-08-29T11:34:56+00:00"
  }
}

Get & list invoices

GET /v1/invoices/{track_id} returns one invoice, including txs[] — every on-chain transfer received against it, with confirmations and status. This is the endpoint to poll if you cannot receive webhooks.

GET /v1/invoices/a1B2c3D4
{
  "ok": true,
  "data": {
    "track_id":       "a1B2c3D4",
    "status":         "paid",
    "currency":       "USDT",
    "network":        "TRON",
    "amount":         "49.99",
    "amount_usd":     "49.99",
    "paid_usd":       49.99,
    "rate_usd":       1.0,
    "paid_amount":    "49.99",
    "pending_amount": "0",
    "remaining_amount": "0",
    "fee":            "0.50",
    "address":        "TR7N...Lj6t",
    "order_id":       "A-1042",
    "metadata":       { "chat_id": 1122334455 },
    "txs": [
      { "txid": "7f3a...c91d", "currency": "USDT", "network": "TRON",
        "amount": "49.99", "usd_value": 49.99, "from": "TQ2s...9fA1",
        "confirmations": 41, "status": "credited",
        "block_time": "2026-08-29T11:41:00+00:00" }
    ],
    "paid_at":    "2026-08-29T11:41:03+00:00",
    "expires_at": "2026-08-29T12:34:56+00:00",
    "created_at": "2026-08-29T11:34:56+00:00"
  }
}

GET /v1/invoices lists your invoices, newest first. Query params:

statusstringfilter by status: new, waiting, confirming, paid, underpaid, expired, failed
order_idstringfilter by your reference
pageintpage number, default 1
per_pageintitems per page, default 25 (max 100)
GET /v1/invoices?status=paid&order_id=A-1042&page=1&per_page=25
{
  "ok": true,
  "data": [ { "track_id": "a1B2c3D4", "status": "paid", ... }, ... ],
  "meta": { "page": 1, "per_page": 25, "total": 134, "last_page": 6 }
}

USD pricing & payer coin choice

Invoices created with amount_usd open a checkout where the customer picks any accepted coin. The USD→coin rate (rate_usd) locks the moment they choose, until the invoice expires. With mixed_payment the invoice settles by accumulated USD value, allowing several transfers — even in different coins. Track progress with paid_usd vs amount_usd, and remaining_amount for the current coin.

Donations for creators (buy me a coffee)

Perfect for streamers, YouTubers, and developers who want tips / “buy me a coffee” in crypto — no store, no code required. In the dashboard open Donation service → create a donation link. The supporter opens it, chooses any amount and any coin you accept, and pays. You can brand it (logo, colour, thanks message) and embed a button anywhere.

Embed the donate button on your site, channel page or blog:

<a href="https://payxif.com/pay/d/coffee4dev" target="_blank" rel="noopener">
  ☕ Buy me a coffee (crypto)
</a>

Live donation alerts. Point the donation link’s webhook at your overlay/bot and every tip fires an invoice.paid event — show it on a stream overlay, in Discord, or as a Telegram message in seconds:

// on invoice.paid (see Webhooks), broadcast to your stream overlay
io.emit("tip", {
  amount: e.paid_amount + " " + e.currency,   // "5 USDT"
  from:   e.metadata?.name || "Anonymous",
  note:   e.description,
});

Invoice statuses

newUSD invoice waiting for the customer to pick a coin
waitingcoin chosen, nothing received yet
confirmingtransfer seen on chain, waiting for confirmations (~1 min on Tron)
paidconfirmed amount ≥ requested — wallet credited (event invoice.paid). FINAL.
underpaidbelow requested; the address stays open for top-up until expiry
expireddeadline passed — a late payment still credits your wallet (invoice.paid_late). FINAL.
failedcould not be completed / cancelled — no funds moved. FINAL.

Final states are paid, expired and failed. Once an invoice is final it will not change (except a late top-up after expiry, delivered as invoice.paid_late).

Webhooks

PayXif POSTs JSON to your callback_url with an HMAC-SHA512 signature of the raw body in the X-PayXif-Signature header (hex-encoded, keyed with your merchant webhook secret). It also sends X-PayXif-Event and X-PayXif-Delivery. Reply 2xx within 10s; failed deliveries retry with backoff over 8 attempts.

invoice.confirmingeventfirst transfer seen on chain, waiting for confirmations
invoice.paideventfully paid and credited — deliver the product
invoice.underpaideventreceived less than requested; address still open
invoice.expiredeventlifetime passed with no full payment
invoice.paid_lateeventa payment landed after expiry; still credited
testeventsent by the ‘Test webhook’ button so you can verify your endpoint

Example delivery:

POST https://shop.example/payxif/webhook
X-PayXif-Event:     invoice.paid
X-PayXif-Delivery:  1234
X-PayXif-Signature: <hex hmac_sha512(raw_body, webhook_secret)>

{
  "event":        "invoice.paid",
  "type":         "invoice",
  "track_id":     "a1B2c3D4",
  "order_id":     "A-1042",
  "status":       "paid",
  "currency":     "USDT",
  "network":      "TRON",
  "amount":       "49.99",
  "paid_amount":  "49.99",
  "pending_amount": "0",
  "fee":          "0.50",
  "address":      "TR7N...Lj6t",
  "email":        null,
  "description":  "Pro plan - 1 month",
  "metadata":     { "chat_id": 1122334455 },
  "txs": [{ "txid": "7f3a...c91d", "amount": "49.99", "confirmations": 41, "status": "credited" }],
  "paid_at":      "2026-08-29T11:41:03+00:00",
  "expires_at":   "2026-08-29T12:34:56+00:00",
  "sent_at":      "2026-08-29T11:41:04+00:00"
}

Verify the signature before trusting the body:

<?php
$secret = "whsec_your_merchant_webhook_secret";     // Merchant service -> Webhook secret
$body   = file_get_contents("php://input");          // the RAW body (do not re-encode)
$sig    = $_SERVER["HTTP_X_PAYXIF_SIGNATURE"] ?? "";

if (!hash_equals(hash_hmac("sha512", $body, $secret), $sig)) {
    http_response_code(401);
    exit;
}
$e = json_decode($body, true);
if ($e["event"] === "invoice.paid") {
    // fulfil the order: $e["order_id"], $e["paid_amount"], $e["metadata"]
}
http_response_code(200);

Test your endpoint

In Merchant service → Webhooks press Send test to fire a signed test event at your webhook_url. You can also resend any past delivery from the same screen. Use these to confirm your signature check and 2xx response before going live.

Idempotency & retries

Networks fail; retries are normal. PayXif makes the money paths safe to retry so you never double-charge or double-pay.

order_idinvoicesResending POST /v1/invoices with the same order_id returns the existing open invoice (HTTP 200) instead of creating a second one. Use your internal order id.
idempotency_keypayoutsSend a unique idempotency_key on POST /v1/payout; a retry with the same key returns the same payout instead of sending twice.
track_idwebhooksDeliveries can repeat. Key your handler off track_id + event and ignore anything you already processed.
// invoices: same order_id -> same invoice (HTTP 200, not 201)
POST /v1/invoices { "amount_usd": "5", "order_id": "A-1042" }   // 201 created
POST /v1/invoices { "amount_usd": "5", "order_id": "A-1042" }   // 200, returns the same invoice

// payouts: same idempotency_key -> same payout, sent once
POST /v1/payout { "address": "...", "amount": "25", "currency": "USDT",
                  "network": "TRON", "idempotency_key": "payout-A17" }

Sandbox & testing

Build and test your integration with no real crypto and no blockchain. Add sandbox: true when you create an invoice: it behaves like a normal invoice (same object, same webhook format) but never touches the chain or your ledger. You complete it yourself with a Simulate button on the checkout, or with the simulate endpoint — that fires a real, signed invoice.paid webhook so you can verify your whole flow end-to-end.

// 1) create a sandbox invoice (your normal live key + sandbox:true)
curl -X POST https://api.payxif.com/v1/invoices \
  -H "X-API-Key: pxk_live_xxxxxxxx" -H "Content-Type: application/json" \
  -d '{ "amount_usd": "5.00", "order_id": "TEST-1", "sandbox": true,
        "callback_url": "https://shop.example/payxif/webhook" }'
// -> { "ok": true, "data": { "track_id": "a1B2c3D4", "sandbox": true,
//        "pay_link": "https://payxif.com/pay/a1B2c3D4", ... } }

// 2) complete it (same as pressing "Simulate payment" on the checkout page)
curl -X POST https://api.payxif.com/pay/a1B2c3D4/simulate
// -> the invoice becomes "paid" and your webhook fires with "sandbox": true
sandbox: trueon createmarks the invoice as a test — no deposit address, no on-chain watching, no wallet credit
POST /pay/{`{track_id}`}/simulatecompletepublic endpoint (sandbox invoices only) that marks it paid and delivers a signed invoice.paid webhook
Simulate buttoncheckoutthe hosted checkout shows a Simulate button for sandbox invoices — same effect as the endpoint
"sandbox": truewebhookevery sandbox webhook carries this flag so your handler can tell test traffic apart from live

Good to know

  • Use your normal merchant key — sandbox is a per-invoice flag, not a separate key.
  • Sandbox invoices never move money: no address is created, nothing is credited, the ledger is untouched.
  • The webhook is signed exactly like a live one (X-PayXif-Signature), so it exercises your signature check for real.
  • Sandbox can be turned off site-wide by the operator (Admin → Settings → Invoices); a sandbox: true request then creates a normal live invoice.

Telegram bots — accept crypto, 0 → 100

Want a Telegram bot that sells a file, a subscription, VIP access or a service and gets paid in crypto automatically? Here is the full flow. You do not touch private keys or wallets — PayXif handles the chain and credits your wallet.

  1. Create a merchant key in the dashboard (Merchant service). Copy the webhook secret too.
  2. When a user taps Buy, your bot calls POST /v1/invoices with order_id: "tg:<user id>" and metadata.chat_id so you know who to deliver to.
  3. Send the user an inline button linking to data.pay_link. They pick a coin and pay on the hosted checkout.
  4. PayXif sends invoice.paid to your webhook. Verify the signature, then deliver the product to metadata.chat_id with the Bot API.

Complete example — bot + webhook in one file:

import { Telegraf, Markup } from "telegraf";
import express from "express";
import crypto from "crypto";

const bot     = new Telegraf(process.env.BOT_TOKEN);
const API_KEY = "pxk_live_xxxxxxxx";                 // PayXif merchant key
const SECRET  = process.env.PAYXIF_SECRET;           // PayXif webhook secret

// 1) the customer starts a purchase inside your bot
bot.command("buy", async (ctx) => {
  const res = await fetch("https://api.payxif.com/v1/invoices", {
    method: "POST",
    headers: { "X-API-Key": API_KEY, "Content-Type": "application/json" },
    body: JSON.stringify({
      amount_usd:   "5.00",
      order_id:     "tg:" + ctx.from.id,             // who to deliver to
      description:  "VIP access - 1 month",
      metadata:     { chat_id: ctx.chat.id },        // comes back in the webhook
      callback_url: "https://yourbot.example/payxif/webhook",
    }),
  });
  const { data } = await res.json();

  // 2) send them a one-tap Pay button (opens the hosted crypto checkout)
  await ctx.reply("Pay with crypto to unlock VIP 👇",
    Markup.inlineKeyboard([Markup.button.url("💳 Pay $5", data.pay_link)]));
});

// 3) PayXif calls you the moment it is paid -> deliver automatically
const app = express();
app.post("/payxif/webhook", express.raw({ type: "*/*" }), async (req, res) => {
  const sig = req.get("X-PayXif-Signature") || "";
  const mac = crypto.createHmac("sha512", SECRET).update(req.body).digest("hex");
  if (mac.length !== sig.length || !crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(sig)))
    return res.sendStatus(401);

  const e = JSON.parse(req.body.toString());
  if (e.event === "invoice.paid") {
    await bot.telegram.sendMessage(e.metadata.chat_id,
      "✅ Payment received! Here is your VIP link: https://t.me/+abc123");
  }
  res.sendStatus(200);   // reply 2xx within 10s or PayXif retries
});

bot.launch();
app.listen(3000);

Tips

  • Put the buyer’s Telegram chat id in metadata — it comes straight back in the webhook, so you always know whom to deliver to.
  • Reply 2xx fast and deliver asynchronously; if you crash, PayXif retries the webhook.
  • Use a pxk_test_ key + Tron Nile testnet to try the whole flow with no real funds.
  • Prefer the hosted pay_link — your bot never has to display addresses or handle coins itself.

Payout API

Programmatic withdrawals with a payout-scoped key (pxp_…, created in Payout API with password + 2FA and an optional IP allowlist). Key possession is the auth — no OTP per call. Always send an idempotency_key so a network retry never sends twice.

addressstring · requireddestination address (validated for the network)
amountstring · requiredamount to send, e.g. "25"
currencystring · requiredUSDT, TRX, …
networkstring · requiredTRON, BSC, ETH, POLYGON
descriptionstringyour note, echoed back on the payout
idempotency_keystringunique per payout — a retry with the same key is a no-op
curl -X POST https://api.payxif.com/v1/payout \
  -H "X-API-Key: pxp_live_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "address": "TR7N...Lj6t",
    "amount": "25",
    "currency": "USDT",
    "network": "TRON",
    "description": "affiliate payout",
    "idempotency_key": "payout-2026-08-29-A17"
  }'

Response — poll GET /v1/payout/{id} until it reaches a final status:

HTTP/1.1 201 Created
{
  "ok": true,
  "data": {
    "id":           "b7d2...uuid",   // use this to poll GET /v1/payout/{id}
    "status":       "pending",       // pending -> approved -> processing -> sent -> confirmed
    "currency":     "USDT",
    "network":      "TRON",
    "address":      "TR7N...Lj6t",
    "amount":       "25.000000",
    "fee":          "1.000000",
    "txid":         null,            // set once broadcast
    "explorer_url": null,
    "description":  "affiliate payout",
    "created_at":   "2026-08-29T11:34:56+00:00",
    "processed_at": null
  }
}
GET /v1/payouts        # list payouts made through the API (paginated)
GET /v1/payout/{id}    # single payout with status, txid + explorer url

Payout statuses

pendingaccepted, queued (may await approval depending on your limits)
approvedcleared to send, waiting for the sender
processingbeing broadcast to the network
sentbroadcast — txid available, waiting for confirmations
confirmedconfirmed on-chain. FINAL — success.
needs_reviewflagged for manual review (e.g. broadcast uncertain)
failedcould not be sent — funds returned to your balance. FINAL.
rejectedrejected by policy/admin — funds returned. FINAL.
cancelledcancelled before sending — funds returned. FINAL.

On any of failed / rejected / cancelled the amount + fee are returned to your balance — no funds are lost. Very small withdrawals may be refused up-front with withdrawal_uneconomic when the network fee would exceed the amount.

Wallet & balance

Every confirmed payment lands in your central wallet. Read balances with the API, or manage funds (deposit, send, internal transfer) from the dashboard wallet.

GET /v1/balance
{
  "ok": true,
  "data": [
    { "currency": "USDT", "network": "TRON", "balance": "1240.500000" },
    { "currency": "TRX",  "network": "TRON", "balance": "310.204100" }
  ]
}

GET /v1/me returns your merchant profile and effective fee:

GET /v1/me
{
  "ok": true,
  "data": {
    "id":           "9c1f...uuid",
    "name":         "My Shop",
    "email":        "me@shop.example",
    "status":       "active",
    "webhook_url":  "https://shop.example/payxif/webhook",
    "fee_pct":      1.0,
    "network_mode": "live"          // "live" (mainnet) or "test" (testnet)
  }
}

Currencies & networks

GET /v1/currencies (public) lists every enabled coin with its network, contract, decimals, required confirmations and minimum amount. Read it at startup to build your coin picker and to know each coin’s minimum before creating an invoice.

GET /v1/currencies
{
  "ok": true,
  "data": [
    {
      "code": "USDT", "name": "Tether", "network": "TRON",
      "contract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
      "decimals": 6, "min_confirmations": 19, "min_amount": "1"
    },
    { "code": "TRX", "name": "TRON", "network": "TRON",
      "contract": null, "decimals": 6, "min_confirmations": 19, "min_amount": "5" }
  ]
}

Networks are identified by code: TRON, BSC, ETH, POLYGON. A coin is always a (code, network) pair — the same symbol on two networks is two entries (e.g. USDT on TRON vs USDT on BSC).

Error codes

On failure ok is false and error.code is one of the stable codes below. Branch on the code; show error.message to yourself, not the buyer. Validation failures (422) additionally include error.fields mapping each bad field to its messages.

Auth & transport

codehttpwhen & how to fix
unauthorized401Missing/invalid API key, or wrong key type for the endpoint. Check the X-API-Key header.
ip_not_allowed403Caller IP is not on the key's allowlist. Add your server IP in the key settings.
forbidden403The key is not permitted for this action/resource.
account_suspended403The merchant account is suspended. Contact the operator.
email_unverified403Verify the account email before using this endpoint.
rate_limited429Too many requests for this key. Back off and retry (see Rate limits).
validation_failed422A field is missing or malformed. Read error.fields for the exact field(s).
not_found404No such invoice / payout / resource for this key.
maintenance503Short maintenance window. Retry with backoff.
server_error500Unexpected server error. Safe to retry idempotent calls (order_id / idempotency_key).

Invoices

amount_required400Provide amount_usd, or amount + currency.
invalid_amount422Amount is not a valid positive number.
amount_too_small400Below the coin's minimum (or the merchant's minimum USD). See /v1/currencies.
amount_too_large400Above the allowed maximum for this coin/merchant.
unsupported_currency400Unknown coin code. Use a code from /v1/currencies.
unsupported_network400Unknown or disabled network for this coin.
currency_not_accepted400This coin/network is not enabled for your merchant or this invoice.
not_selectable400The coin can't be selected for this invoice at its current state.
no_rate400No USD price available for this coin right now. Retry shortly or pick another coin.
cannot_cancel400The invoice is already paid/expired and can't be cancelled.
expired400The invoice lifetime has passed.

Payouts & wallet

insufficient_balance400Balance can't cover amount + fee. Top up or lower the amount.
invalid_address400The destination address is invalid for the chosen network.
internal_address400Address belongs to PayXif — use an internal transfer instead of a payout.
not_internal400Internal transfer target is not a PayXif account.
self_transfer400Source and destination are the same account.
recipient_unavailable400The recipient account can't receive right now.
address_blocked400The address was blocked by screening (sanctions / risk). Use another address.
withdrawal_uneconomic400Network fee would exceed the amount. Send a larger amount.
not_refundable400This withdrawal is past the point where it can be refunded.
withdrawal_held403Withdrawal held for review (limits / risk). Wait or contact the operator.
withdraw_locked429Too many wrong withdrawal-password attempts — temporarily locked.
withdrawals_disabled503Withdrawals are temporarily turned off site-wide. Retry later.

Troubleshooting

Webhook signature never matches

You are almost certainly hashing a re-serialized body. Compute the HMAC over the raw request bytes, before any JSON parse/re-stringify. In Express use express.raw(); in Flask use request.get_data(); in PHP use file_get_contents("php://input"). Verify against the webhook secret (not the API key) with SHA-512, hex output, and a constant-time compare.

No webhook arrives

Your endpoint must be publicly reachable over HTTPS and reply 2xx within 10s. Use Send test in the dashboard to fire a signed test event, and Resend to replay a real one. Deliveries retry 8 times with backoff — check the Webhooks log for the last response code.

I created two invoices for one order

Send your internal order_id on every create. Resending the same order_id returns the existing open invoice (HTTP 200) instead of a new one.

A payout might have been sent twice

Always include a unique idempotency_key. A retry with the same key returns the original payout and never sends again. Poll GET /v1/payout/{id} to confirm the final status.

amount_too_small on a valid-looking amount

Each coin has a minimum (and there may be a merchant minimum in USD). Read min_amount from /v1/currencies and check against it before creating the invoice.

Testing safely

Use a pxk_test_ key and the Tron Nile testnet — the whole invoice → pay → webhook flow works with no real funds. Switch to pxk_live_ only when you go to production.

Rate limits. API keys are throttled per key. If you exceed the limit you get 429 rate_limited — back off and retry with exponential delay. Withdrawal/payout calls have a tighter limit than read calls. Batch reads and cache /v1/currencies.