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
- In the dashboard open Merchant service → create an API key (starts with
pxk_live_). Set a default Webhook URL. - Create an invoice from your backend (below) and redirect the customer to
data.pay_link. - Receive the
invoice.paidwebhook, 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 · mainnet | create & read invoices, read balance and profile |
| pxk_test_ | merchant · testnet | same, against the test network (no real funds) |
| pxp_live_ | payout · mainnet | Payout 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/me | pxk_ | merchant profile, fee %, network mode |
| GET | /v1/balance | pxk_ | your wallet balances per coin/network |
| POST | /v1/invoices | pxk_ | create an invoice (USD or fixed coin) |
| GET | /v1/invoices | pxk_ | list invoices (filter + paginate) |
| GET | /v1/invoices/{track_id} | pxk_ | one invoice, includes txs[] |
| POST | /v1/payout | pxp_ | send a withdrawal to an address |
| GET | /v1/payouts | pxp_ | list payouts made via the API |
| GET | /v1/payout/{id} | pxp_ | one payout with status + txid |
| POST | /pay/{track_id}/simulate | public | sandbox only: complete a test invoice |
| GET | /v1/currencies | public | enabled coins, networks, contracts, minimums |
| GET | /v1/meta | public | site 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_usd | string | USD amount, e.g. "49.99" — customer chooses the coin. Either this OR amount+currency. |
| amount | string | coin amount, e.g. "25.50" (use with currency) |
| currency | string | USDT, TRX, USDC, … (required with amount) |
| network | string | TRON (default), BSC, ETH, POLYGON |
| order_id | string | your reference, echoed in every webhook; also the idempotency key |
| description | string | shown on the payment page |
| string | customer email (optional) | |
| payer_name | string | pre-fill the payer name field (optional) |
| payer_note | string | pre-fill a note from the payer (optional) |
| callback_url | url | webhook for this invoice (falls back to your merchant default) |
| return_url | url | where the customer returns after paying |
| cancel_url | url | where the customer goes if they cancel / abandon checkout |
| lifetime | int | minutes, 5–1440, default 60 — the USD→coin rate is locked until expiry |
| underpaid_cover | number | % shortfall still accepted as paid (0–60) |
| fee_paid_by_payer | bool | add the service fee on top of the customer total |
| mixed_payment | bool | allow several transfers / coins until the USD total is reached |
| metadata | object | up to 20 keys, echoed back in webhooks (great for a Telegram chat id) |
| sandbox | bool | create 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:
| status | string | filter by status: new, waiting, confirming, paid, underpaid, expired, failed |
| order_id | string | filter by your reference |
| page | int | page number, default 1 |
| per_page | int | items 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,
});Payment links
No code needed: create links from the dashboard. A link https://payxif.com/pay/l/12345678 takes fixed-amount payments; a donation link lets the payer choose the amount. Both support branding, customer fields (name / email / note), accepted-coin lists and a thanks message, and report every payment in Stats and the per-link payment list. Each payment still fires the same invoice.paid webhook, so you can automate delivery exactly as with API invoices.
Invoice statuses
| new | USD invoice waiting for the customer to pick a coin | |
| waiting | coin chosen, nothing received yet | |
| confirming | transfer seen on chain, waiting for confirmations (~1 min on Tron) | |
| paid | confirmed amount ≥ requested — wallet credited (event invoice.paid). FINAL. | |
| underpaid | below requested; the address stays open for top-up until expiry | |
| expired | deadline passed — a late payment still credits your wallet (invoice.paid_late). FINAL. | |
| failed | could 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.confirming | event | first transfer seen on chain, waiting for confirmations |
| invoice.paid | event | fully paid and credited — deliver the product |
| invoice.underpaid | event | received less than requested; address still open |
| invoice.expired | event | lifetime passed with no full payment |
| invoice.paid_late | event | a payment landed after expiry; still credited |
| test | event | sent 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_id | invoices | Resending 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_key | payouts | Send a unique idempotency_key on POST /v1/payout; a retry with the same key returns the same payout instead of sending twice. |
| track_id | webhooks | Deliveries 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: true | on create | marks the invoice as a test — no deposit address, no on-chain watching, no wallet credit |
| POST /pay/{`{track_id}`}/simulate | complete | public endpoint (sandbox invoices only) that marks it paid and delivers a signed invoice.paid webhook |
| Simulate button | checkout | the hosted checkout shows a Simulate button for sandbox invoices — same effect as the endpoint |
| "sandbox": true | webhook | every 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: truerequest 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.
- Create a merchant key in the dashboard (Merchant service). Copy the webhook secret too.
- When a user taps Buy, your bot calls
POST /v1/invoiceswithorder_id: "tg:<user id>"andmetadata.chat_idso you know who to deliver to. - Send the user an inline button linking to
data.pay_link. They pick a coin and pay on the hosted checkout. - PayXif sends
invoice.paidto your webhook. Verify the signature, then deliver the product tometadata.chat_idwith 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
2xxfast 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.
| address | string · required | destination address (validated for the network) |
| amount | string · required | amount to send, e.g. "25" |
| currency | string · required | USDT, TRX, … |
| network | string · required | TRON, BSC, ETH, POLYGON |
| description | string | your note, echoed back on the payout |
| idempotency_key | string | unique 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 urlPayout statuses
| pending | accepted, queued (may await approval depending on your limits) | |
| approved | cleared to send, waiting for the sender | |
| processing | being broadcast to the network | |
| sent | broadcast — txid available, waiting for confirmations | |
| confirmed | confirmed on-chain. FINAL — success. | |
| needs_review | flagged for manual review (e.g. broadcast uncertain) | |
| failed | could not be sent — funds returned to your balance. FINAL. | |
| rejected | rejected by policy/admin — funds returned. FINAL. | |
| cancelled | cancelled 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
| code | http | when & how to fix |
|---|---|---|
| unauthorized | 401 | Missing/invalid API key, or wrong key type for the endpoint. Check the X-API-Key header. |
| ip_not_allowed | 403 | Caller IP is not on the key's allowlist. Add your server IP in the key settings. |
| forbidden | 403 | The key is not permitted for this action/resource. |
| account_suspended | 403 | The merchant account is suspended. Contact the operator. |
| email_unverified | 403 | Verify the account email before using this endpoint. |
| rate_limited | 429 | Too many requests for this key. Back off and retry (see Rate limits). |
| validation_failed | 422 | A field is missing or malformed. Read error.fields for the exact field(s). |
| not_found | 404 | No such invoice / payout / resource for this key. |
| maintenance | 503 | Short maintenance window. Retry with backoff. |
| server_error | 500 | Unexpected server error. Safe to retry idempotent calls (order_id / idempotency_key). |
Invoices
| amount_required | 400 | Provide amount_usd, or amount + currency. |
| invalid_amount | 422 | Amount is not a valid positive number. |
| amount_too_small | 400 | Below the coin's minimum (or the merchant's minimum USD). See /v1/currencies. |
| amount_too_large | 400 | Above the allowed maximum for this coin/merchant. |
| unsupported_currency | 400 | Unknown coin code. Use a code from /v1/currencies. |
| unsupported_network | 400 | Unknown or disabled network for this coin. |
| currency_not_accepted | 400 | This coin/network is not enabled for your merchant or this invoice. |
| not_selectable | 400 | The coin can't be selected for this invoice at its current state. |
| no_rate | 400 | No USD price available for this coin right now. Retry shortly or pick another coin. |
| cannot_cancel | 400 | The invoice is already paid/expired and can't be cancelled. |
| expired | 400 | The invoice lifetime has passed. |
Payouts & wallet
| insufficient_balance | 400 | Balance can't cover amount + fee. Top up or lower the amount. |
| invalid_address | 400 | The destination address is invalid for the chosen network. |
| internal_address | 400 | Address belongs to PayXif — use an internal transfer instead of a payout. |
| not_internal | 400 | Internal transfer target is not a PayXif account. |
| self_transfer | 400 | Source and destination are the same account. |
| recipient_unavailable | 400 | The recipient account can't receive right now. |
| address_blocked | 400 | The address was blocked by screening (sanctions / risk). Use another address. |
| withdrawal_uneconomic | 400 | Network fee would exceed the amount. Send a larger amount. |
| not_refundable | 400 | This withdrawal is past the point where it can be refunded. |
| withdrawal_held | 403 | Withdrawal held for review (limits / risk). Wait or contact the operator. |
| withdraw_locked | 429 | Too many wrong withdrawal-password attempts — temporarily locked. |
| withdrawals_disabled | 503 | Withdrawals 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.
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.