Paid.my Merchant API
Accept crypto payments with hosted checkout, invoices, static deposit addresses, automated payouts and balance swaps. All requests and responses are JSON. Monetary amounts are transmitted as strings to preserve decimal precision.
https://paid.my/api/v1Quick start
- Create an account and go to Merchant Service → Generate API Key. Pick the Merchant scope. The key is shown once.
- Call
POST /payment/invoicewith an amount — you get back apayment_url. - Redirect your customer there. They pick a coin and pay.
- Receive a signed
invoice.paidwebhook at yourcallback_url, or pollGET /payment/{track_id}.
curl -X POST https://paid.my/api/v1/payment/invoice \
-H "merchant-api-key: paidmy_mk_…" \
-H "Idempotency-Key: $(uuidgen)" \
-H "content-type: application/json" \
-d '{"amount":100,"currency":"USD","order_id":"order-1042","callback_url":"https://yoursite.com/webhooks/paidmy"}'Authentication
Every endpoint except the public common/* endpoints requires an API key sent in a scope-specific request header. Three equivalent forms are accepted: the hyphenated header shown below (recommended), the underscore variant (merchant_api_key — OxaPay-compatible, but many proxies silently strip underscore headers), or Authorization: Bearer <key>. A key only works for its own scope — using a payout key on a payment endpoint returns 401 with This key has scope PAYOUT; MERCHANT is required.
An account can hold up to five active keys across all scopes; revoke one from Merchant Service to make room for a rotation.
| Scope | Header | Live prefix | Sandbox prefix | Used for |
|---|---|---|---|---|
| Merchant | merchant-api-key | paidmy_mk_ | paidmy_smk_ | Creating/reading payments, static addresses |
| Payout | payout-api-key | paidmy_pk_ | paidmy_spk_ | Sending/reading payouts and swaps (anything that moves your balance) |
| General | general-api-key | paidmy_gk_ | paidmy_sgk_ | Account-level reads (balances) |
Response envelope
Every response — success or failure — uses the same envelope. On failure error is an object and data is empty. Amounts are decimal strings.
{
"data": {
"track_id": "9YfKp2hQ7Wm3"
},
"message": "Success",
"error": null,
"status": 200,
"version": "1.0"
}{
"data": {},
"message": "amount: Number must be greater than 0",
"error": {
"type": "validation_error",
"key": "amount",
"message": "amount: Number must be greater than 0"
},
"status": 400,
"version": "1.0"
}Every error.type and error.key is listed in the error reference.
Error reference
Every failure uses the envelope above with an error object. Branch on error.type first (it maps one-to-one to the HTTP status), then on error.key for validation errors — the key is stable and names either the offending field or the specific condition. error.message is for humans and may change; do not parse it.
Error types
| HTTP | error.type | Meaning | What to do |
|---|---|---|---|
400 | validation_error | The request was understood but cannot be fulfilled as sent. key says why (table below). | Fix the request; do not retry unchanged. |
401 | auth_error | Missing, revoked or wrong-scope API key. | Check the header and that the key's scope matches the endpoint (Authentication). |
403 | forbidden | The key is valid but this account may not perform the action. | Contact support if unexpected. |
404 | not_found | Unknown track_id, or it belongs to another account. | Check the identifier; sandbox and live records are separate. |
409 | conflict | Duplicate order_id on an open payment, or an Idempotency-Key reused with a different body / still in flight. | Look up the existing record; for idempotency, retry with the same body or a new key. |
429 | rate_limited | Too many requests for this key. | Wait for Retry-After seconds, then retry. |
500 | internal_error | Something failed on our side. Nothing was charged. | Retry with the same Idempotency-Key; if it persists, contact support. |
Validation keys
Present on validation_error. A key equal to a field name means that field is missing or malformed; the others name a business rule.
| error.key | Endpoints | Meaning |
|---|---|---|
body | any POST | Body is not valid JSON. |
amount | payout, swap | Insufficient available balance for the amount plus fee (the message says how much you have). On payment endpoints: missing or out of range. |
pay_currency | white-label | Not in your accepted currencies, not enabled, or its rate is temporarily unavailable. |
network | white-label, static-address, payout | Currency does not exist on that chain, the chain is disabled, or the currency exists on several networks and you must name one (the message lists them). |
from_network | swap | Not enough of the currency is held on the requested (or any single) network. Balances are tracked per network; move funds with a same-currency swap or pick another network. |
to_currency | payment, swap | On payments: only USDT is supported for auto-convert. On swaps: same currency without a network move, or an unsupported pair. |
below_minimum | payout, swap | Under the network's minimum payout, the swap USD minimum, or the provider's route minimum. The message includes the figure. |
not_allowlisted | payout | Address allowlisting is on for this account and the destination is not on the list. |
network_inventory_low | payout | The platform's hot wallet on that network cannot cover the amount right now. Retry later or use another network; nothing was held. |
temporarily_unavailable | swap | The swap provider or a hot wallet is unreachable. Retry in a minute; nothing was held. |
quote_failed | swap | The provider returned no usable quote for this pair and amount. |
Webhook delivery failures are not API errors: a callback that does not answer 2xx is retried with backoff (see Webhooks) and every delivery, with its attempts and responses, is listed under Settings in the dashboard.
Idempotency
POST endpoints with side effects (payment/invoice, payment/static-address, payout) accept an Idempotency-Key header. Send any unique string (a UUID) and retries are safe:
- Same key + same body → the stored response is replayed; no duplicate invoice or payout.
- Same key + different body →
409 conflict. - Same key while the first request is still processing →
409 conflict("still being processed") — retry shortly.
Always use it for payouts — a network timeout on a payout without a key can otherwise double-send.
Rate limits
Limits are applied per API key. Exceeding one returns 429 with a Retry-After header (seconds).
| Bucket | Limit | Applies to |
|---|---|---|
| General | 5 req/s sustained (burst 20) | All endpoints unless listed below |
| Invoice creation | 60 / minute | POST /payment/invoice |
| Payout creation | 10 / minute | POST /payout |
Payments
Invoices, hosted checkout and static deposit addresses.
Create an invoice
merchant-api-keyIdempotency-Key supportedRate limit: 60 / minuteCreates a payment and returns a hosted checkout URL. Redirect your customer to payment_url; they pick a coin and network there, and the exact pay amount and deposit address are allocated at that moment.
Request body
| Field | Type | Description |
|---|---|---|
amountrequired | number | Invoice amount in currency. Must be > 0. |
currency | string | Fiat (USD, EUR, GBP) or a crypto code. (default "USD") |
lifetime | integer | Minutes until expiry. (15–2880, default 60) |
fee_paid_by_payer | boolean | "0" | "1" | Charge the platform fee to the payer. Default: API key setting, then account setting. |
under_paid_coverage | number | Accepted shortfall in percent. Default: API key setting, then account setting. (0–60) |
callback_url | string (uri) | Webhook target for this invoice. Default: API key callback URL, then account default. |
return_url | string (uri) | Shown to the payer after payment. |
order_id | string | Your reference. Unique per merchant — reuse returns 409. (≤128 chars) |
email | string (email) | Payer email for your records. |
description | string | Shown on the payment page. (≤512 chars) |
thanks_message | string | Shown after payment completes. (≤512 chars) |
to_currency | "USDT" | Set to USDT to auto-convert the credited amount to USDT once the deposit is swept (no extra Paid.my fee; swap.my spread applies). Omit to use the API key's Auto-convert setting. |
sandbox | boolean | Force sandbox (implied when using a sandbox key). |
Example request
curl -X POST https://paid.my/api/v1/payment/invoice \
-H "merchant-api-key: paidmy_mk_…" \
-H "Idempotency-Key: $(uuidgen)" \
-H "content-type: application/json" \
-d '{"amount":100,"currency":"USD","order_id":"order-1042","callback_url":"https://yoursite.com/webhooks/paidmy"}'Response data
| Field | Type | Description |
|---|---|---|
track_id | string | Payment identifier. |
payment_url | string (uri) | Hosted checkout URL. |
expired_at | integer | Expiry. Unix timestamp (seconds). |
date | integer | Creation. Unix timestamp (seconds). |
{
"data": {
"track_id": "9YfKp2hQ7Wm3",
"payment_url": "https://paid.my/pay/9YfKp2hQ7Wm3",
"expired_at": 1755172800,
"date": 1755169200
},
"message": "Success",
"error": null,
"status": 200,
"version": "1.0"
}Errors
| Status | When |
|---|---|
400 | Validation error (error.type = validation_error, error.key names the field). |
401 | Missing/invalid API key, or wrong scope (auth_error). |
409 | order_id already used, or idempotency-key body mismatch. |
429 | Rate limited (rate_limited); honour the Retry-After header. |
Create a white-label payment
merchant-api-keyIdempotency-Key supportedRate limit: 60 / minuteCreates a payment and locks the pay currency in one call, returning the deposit address, exact pay_amount, memo, locked rate and a qr_code image, so you can render the payment screen inside your own checkout or app instead of redirecting to the hosted page. Everything downstream (detection, crediting, webhooks, GET /payment/{track_id}) is identical to a normal invoice. The address is reserved only for lifetime; funds sent after expiry are not credited automatically.
Request body
| Field | Type | Description |
|---|---|---|
amountrequired | number | Invoice amount in currency. Must be > 0. |
currency | string | Fiat (USD, EUR, GBP) or a crypto code. (default "USD") |
pay_currencyrequired | string | Crypto the payer will send. |
network | string | Chain id. Required when pay_currency is available on more than one network (the error lists them). |
lifetime | integer | Minutes the address stays valid. (15–2880, default 60) |
fee_paid_by_payer | boolean | "0" | "1" | Charge the platform fee to the payer. Default: API key setting, then account setting. |
under_paid_coverage | number | Accepted shortfall in percent. Default: API key setting, then account setting. (0–60) |
callback_url | string (uri) | Webhook target for this payment. |
order_id | string | Your reference. Unique per merchant; reuse returns 409. (≤128 chars) |
email | string (email) | Payer email for your records. |
description | string | Your note. (≤512 chars) |
to_currency | "USDT" | Set to USDT to auto-convert the credited amount to USDT once swept. Omit to use the API key's Auto-convert setting. |
sandbox | boolean | Force sandbox (implied when using a sandbox key). |
Example request
curl -X POST https://paid.my/api/v1/payment/white-label \
-H "merchant-api-key: paidmy_mk_…" \
-H "Idempotency-Key: $(uuidgen)" \
-H "content-type: application/json" \
-d '{"amount":100,"currency":"USD","pay_currency":"USDT","network":"TRON","order_id":"order-1043"}'Response data
| Field | Type | Description |
|---|---|---|
track_id | string | Payment identifier. |
status | "new" | "waiting" | "paying" | "paid" | "underpaid" | "expired" | "refunding" | "refunded" | "failed" | See Payment statuses. |
amount | string | Invoice amount in currency. Decimal string — never parse into a float for accounting. |
currency | string | Invoice currency (fiat or crypto code). |
pay_currency | string | null | Crypto the payer selected. |
network | string | null | Chain the payer selected. |
pay_amount | string | Exact crypto amount requested from the payer. Decimal string — never parse into a float for accounting. |
received_amount | string | Crypto received so far. Decimal string — never parse into a float for accounting. |
credited_amount | string | Amount credited to your balance after fees. Decimal string — never parse into a float for accounting. |
fee_amount | string | Platform fee. Decimal string — never parse into a float for accounting. |
rate | string | Locked crypto/fiat rate. Decimal string — never parse into a float for accounting. |
address | string | null | Deposit address allocated for this payment. |
memo | string | null | Memo/tag where the chain needs one. |
fee_paid_by_payer | "0" | "1" | 1 if the payer covered the fee. |
under_paid_coverage | number | Accepted shortfall, percent (0–60). |
lifetime | integer | Lifetime in minutes. |
order_id | string | null | Your order reference. |
email | string | null | Payer email. |
description | string | null | Description shown on the payment page. |
sandbox | boolean | True for sandbox payments. |
auto_convert | boolean | Whether the credited amount is auto-converted to USDT. |
conversion | object | null | Auto-convert progress; null when not enabled or not yet credited. Conversion starts after the deposit is swept to our hot wallet (typically within ~15 minutes of payment). |
expired_at | integer | Expiry. Unix timestamp (seconds). |
date | integer | Creation. Unix timestamp (seconds). |
paid_at | integer | null | Paid time. Unix timestamp (seconds). |
txs | object[] | On-chain deposits matched to this payment. |
payment_url | string (uri) | Hosted checkout URL, should you still want to offer it. |
qr_code | string (uri) | SVG QR image of the wallet-app payment URI for address / pay_amount. |
{
"data": {
"track_id": "9YfKp2hQ7Wm3",
"status": "waiting",
"amount": "100",
"currency": "USD",
"pay_currency": "USDT",
"network": "TRON",
"pay_amount": "101.01",
"received_amount": "0",
"credited_amount": null,
"fee_amount": "1.01",
"rate": "0.9999",
"address": "TQmZ1hVh7…",
"memo": null,
"fee_paid_by_payer": 1,
"under_paid_coverage": 0,
"lifetime": 60,
"order_id": "order-1043",
"email": null,
"description": null,
"sandbox": false,
"expired_at": 1755172800,
"date": 1755169200,
"paid_at": null,
"txs": [],
"payment_url": "https://paid.my/pay/9YfKp2hQ7Wm3",
"qr_code": "https://paid.my/api/pay/9YfKp2hQ7Wm3/qr"
},
"message": "Success",
"error": null,
"status": 200,
"version": "1.0"
}Errors
| Status | When |
|---|---|
400 | Validation error. error.key is pay_currency (not accepted / rate unavailable) or network (ambiguous or unavailable; the message lists valid networks). |
401 | Missing/invalid API key, or wrong scope (auth_error). |
409 | order_id already used, or idempotency-key body mismatch. |
429 | Rate limited (rate_limited); honour the Retry-After header. |
Get a payment
merchant-api-keyPath parameters
| Name | Type | Description |
|---|---|---|
track_id | string | Payment identifier. |
Example request
curl https://paid.my/api/v1/payment/9YfKp2hQ7Wm3 \
-H "merchant-api-key: paidmy_mk_…"Response data
| Field | Type | Description |
|---|---|---|
track_id | string | Payment identifier. |
status | "new" | "waiting" | "paying" | "paid" | "underpaid" | "expired" | "refunding" | "refunded" | "failed" | See Payment statuses. |
amount | string | Invoice amount in currency. Decimal string — never parse into a float for accounting. |
currency | string | Invoice currency (fiat or crypto code). |
pay_currency | string | null | Crypto the payer selected. |
network | string | null | Chain the payer selected. |
pay_amount | string | Exact crypto amount requested from the payer. Decimal string — never parse into a float for accounting. |
received_amount | string | Crypto received so far. Decimal string — never parse into a float for accounting. |
credited_amount | string | Amount credited to your balance after fees. Decimal string — never parse into a float for accounting. |
fee_amount | string | Platform fee. Decimal string — never parse into a float for accounting. |
rate | string | Locked crypto/fiat rate. Decimal string — never parse into a float for accounting. |
address | string | null | Deposit address allocated for this payment. |
memo | string | null | Memo/tag where the chain needs one. |
fee_paid_by_payer | "0" | "1" | 1 if the payer covered the fee. |
under_paid_coverage | number | Accepted shortfall, percent (0–60). |
lifetime | integer | Lifetime in minutes. |
order_id | string | null | Your order reference. |
email | string | null | Payer email. |
description | string | null | Description shown on the payment page. |
sandbox | boolean | True for sandbox payments. |
auto_convert | boolean | Whether the credited amount is auto-converted to USDT. |
conversion | object | null | Auto-convert progress; null when not enabled or not yet credited. Conversion starts after the deposit is swept to our hot wallet (typically within ~15 minutes of payment). |
expired_at | integer | Expiry. Unix timestamp (seconds). |
date | integer | Creation. Unix timestamp (seconds). |
paid_at | integer | null | Paid time. Unix timestamp (seconds). |
txs | object[] | On-chain deposits matched to this payment. |
{
"data": {
"track_id": "9YfKp2hQ7Wm3",
"status": "paid",
"amount": "100",
"currency": "USD",
"pay_currency": "USDT",
"network": "TRON",
"pay_amount": "100.5",
"received_amount": "100.5",
"credited_amount": "99.495",
"fee_amount": "1.005",
"rate": "0.999",
"address": "TQmZ1hVh7…",
"memo": null,
"fee_paid_by_payer": 1,
"under_paid_coverage": 0,
"lifetime": 60,
"order_id": "order-1042",
"email": "[email protected]",
"description": "Pro plan",
"sandbox": false,
"expired_at": 1755172800,
"date": 1755169200,
"paid_at": 1755169980,
"txs": [
{
"tx_hash": "6f9c2c6f6f0a…",
"amount": "100.5",
"confirmations": 21,
"status": "credited",
"date": 1755169900
}
]
},
"message": "Success",
"error": null,
"status": 200,
"version": "1.0"
}Errors
| Status | When |
|---|---|
400 | Validation error (error.type = validation_error, error.key names the field). |
401 | Missing/invalid API key, or wrong scope (auth_error). |
404 | Unknown track_id. |
429 | Rate limited (rate_limited); honour the Retry-After header. |
List payments
merchant-api-keyQuery parameters
| Name | Type | Description |
|---|---|---|
page | integer | Page number. (1–…, default 1) |
size | integer | Page size. (1–200, default 10) |
status | string | Filter by status, e.g. paid, waiting. |
order_id | string | Filter by your order reference. |
sort_type | "asc" | "desc" | Sort by creation date. (default "desc") |
Example request
curl https://paid.my/api/v1/payment?status=paid \
-H "merchant-api-key: paidmy_mk_…"Response data
| Field | Type | Description |
|---|---|---|
list | object[] | |
meta | object |
{
"data": {
"list": [
{
"track_id": "9YfKp2hQ7Wm3",
"status": "paid",
"amount": "100",
"currency": "USD"
}
],
"meta": {
"page": 1,
"last_page": 4,
"total": 38
}
},
"message": "Success",
"error": null,
"status": 200,
"version": "1.0"
}Errors
| Status | When |
|---|---|
400 | Validation error (error.type = validation_error, error.key names the field). |
401 | Missing/invalid API key, or wrong scope (auth_error). |
429 | Rate limited (rate_limited); honour the Retry-After header. |
Accepted currencies
merchant-api-keyCurrency codes your account accepts (all enabled currencies, intersected with your dashboard allowlist if set).
Example request
curl https://paid.my/api/v1/payment/accepted-currencies \
-H "merchant-api-key: paidmy_mk_…"Response data
| Field | Type | Description |
|---|---|---|
list | string[] |
{
"data": {
"list": [
"BTC",
"ETH",
"USDT",
"USDC",
"DAI",
"TRX",
"LTC",
"SOL",
"BNB",
"POL",
"GRAM",
"XMR"
]
},
"message": "Success",
"error": null,
"status": 200,
"version": "1.0"
}Errors
| Status | When |
|---|---|
400 | Validation error (error.type = validation_error, error.key names the field). |
401 | Missing/invalid API key, or wrong scope (auth_error). |
429 | Rate limited (rate_limited); honour the Retry-After header. |
Create a static address
merchant-api-keyIdempotency-Key supportedA permanent deposit address. Every confirmed deposit is credited individually (static-address fee applies) and fires a static_address.deposit webhook.
Request body
| Field | Type | Description |
|---|---|---|
networkrequired | string | Chain id, e.g. TRON, ETH, BSC, POLYGON, BTC, LTC, SOL, TON. |
currency | string | Restrict to one currency; omit to accept any enabled token on the chain. |
callback_url | string (uri) | Webhook target for deposits. |
order_id | string | Reporting only. (≤128 chars) |
email | string (email) | For your records. |
to_currency | "USDT" | Set to USDT to auto-convert every credited deposit to USDT. Omit to use the API key's Auto-convert setting. |
Example request
curl -X POST https://paid.my/api/v1/payment/static-address \
-H "merchant-api-key: paidmy_mk_…" \
-H "Idempotency-Key: $(uuidgen)" \
-H "content-type: application/json" \
-d '{"network":"TRON","currency":"USDT"}'Response data
| Field | Type | Description |
|---|---|---|
track_id | string | Identifier. |
network | string | Chain id. |
currency | string | null | Currency or null. |
address | string | Deposit address. |
memo | string | null | Memo/tag. |
date | integer | Creation. Unix timestamp (seconds). |
{
"data": {
"track_id": "Ab3dEfGh1jKl",
"network": "TRON",
"currency": "USDT",
"address": "TVxq8…",
"memo": null,
"date": 1755169200
},
"message": "Success",
"error": null,
"status": 200,
"version": "1.0"
}Errors
| Status | When |
|---|---|
400 | Validation error (error.type = validation_error, error.key names the field). |
401 | Missing/invalid API key, or wrong scope (auth_error). |
429 | Rate limited (rate_limited); honour the Retry-After header. |
List static addresses
merchant-api-keyQuery parameters
| Name | Type | Description |
|---|---|---|
page | integer | Page number. (1–…, default 1) |
size | integer | Page size. (1–200, default 10) |
network | string | Filter by chain id. |
Example request
curl https://paid.my/api/v1/payment/static-address?network=… \
-H "merchant-api-key: paidmy_mk_…"Response data
| Field | Type | Description |
|---|---|---|
list | object[] | |
meta | object |
{
"data": {
"list": [
{
"track_id": "Ab3dEfGh1jKl",
"network": "TRON",
"currency": "USDT",
"address": "TVxq8…",
"memo": null,
"have_tx": true,
"sandbox": false,
"date": 1755169200
}
],
"meta": {
"page": 1,
"last_page": 1,
"total": 3
}
},
"message": "Success",
"error": null,
"status": 200,
"version": "1.0"
}Errors
| Status | When |
|---|---|
400 | Validation error (error.type = validation_error, error.key names the field). |
401 | Missing/invalid API key, or wrong scope (auth_error). |
429 | Rate limited (rate_limited); honour the Retry-After header. |
Revoke a static address
merchant-api-keyDeposits arriving after revocation are not credited automatically.
Request body
| Field | Type | Description |
|---|---|---|
addressrequired | string | The address to revoke. |
Example request
curl -X POST https://paid.my/api/v1/payment/static-address/revoke \
-H "merchant-api-key: paidmy_mk_…" \
-H "content-type: application/json" \
-d '{"address":"TVxq8…"}'Response data
| Field | Type | Description |
|---|---|---|
revoked | boolean | Always true on success. |
{
"data": {
"revoked": true
},
"message": "Success",
"error": null,
"status": 200,
"version": "1.0"
}Errors
| Status | When |
|---|---|
400 | Validation error (error.type = validation_error, error.key names the field). |
401 | Missing/invalid API key, or wrong scope (auth_error). |
404 | Address not found. |
429 | Rate limited (rate_limited); honour the Retry-After header. |
Get a static address and its deposits
merchant-api-keyPoll a static address instead of (or as well as) relying on the static_address.deposit webhook. Every deposit is listed with its confirmations, and once credited, the amount that reached your balance and the fee taken. credited_amount and fee come from the ledger entry made at the time, so later fee changes never rewrite history.
Path parameters
| Name | Type | Description |
|---|---|---|
track_id | string | Static address identifier. |
Example request
curl https://paid.my/api/v1/payment/static-address/9YfKp2hQ7Wm3 \
-H "merchant-api-key: paidmy_mk_…"Response data
| Field | Type | Description |
|---|---|---|
track_id | string | Static address identifier. |
network | string | Chain id. |
currency | string | null | Restricted currency, or null for any enabled token on the chain. |
address | string | The permanent deposit address. |
memo | string | null | Memo/tag where the chain needs one. |
callback_url | string | null | Webhook target for this address. |
auto_convert | boolean | Whether deposits are auto-converted to USDT. |
revoked | boolean | True once revoked; later deposits are not credited automatically. |
sandbox | boolean | True for sandbox addresses. |
date | integer | Creation. Unix timestamp (seconds). |
deposits | object[] | Every deposit received, newest first. |
{
"data": {
"track_id": "Ab3dEfGh1jKl",
"network": "TRON",
"currency": "USDT",
"address": "TVxq8…",
"memo": null,
"callback_url": "https://example.com/hooks/paidmy",
"auto_convert": false,
"revoked": false,
"sandbox": false,
"date": 1755169200,
"deposits": [
{
"tx_hash": "6f9c2c6f6f0a…",
"currency": "USDT",
"amount": "50",
"credited_amount": "48.5",
"fee": "1.5",
"status": "credited",
"confirmations": 19,
"required_confirmations": 19,
"conversion": null,
"date": 1755169800,
"credited_at": 1755169900
},
{
"tx_hash": "a01b7d…",
"currency": "USDT",
"amount": "20",
"credited_amount": null,
"fee": null,
"status": "detected",
"confirmations": 3,
"required_confirmations": 19,
"conversion": null,
"date": 1755170400,
"credited_at": null
}
]
},
"message": "Success",
"error": null,
"status": 200,
"version": "1.0"
}Errors
| Status | When |
|---|---|
400 | Validation error (error.type = validation_error, error.key names the field). |
401 | Missing/invalid API key, or wrong scope (auth_error). |
404 | Unknown track_id. |
429 | Rate limited (rate_limited); honour the Retry-After header. |
Payouts
Send crypto from your balance to any external address.
Send a payout
payout-api-keyIdempotency-Key supportedRate limit: 10 / minuteSends crypto to an external address. amount + fee is held from your available balance immediately and settled on confirmation (or released on failure). Use an Idempotency-Key — retries are then safe.
Request body
| Field | Type | Description |
|---|---|---|
addressrequired | string | Destination address (the recipient's wallet). |
currencyrequired | string | Currency code. |
amountrequired | number | Amount to deliver — the fee is charged on top. |
network | string | Chain id. Required when the currency exists on more than one enabled network. |
memo | string | Memo/tag for chains that use one (e.g. TON exchanges). (≤256 chars) |
callback_url | string (uri) | Webhook target for this payout. |
description | string | Your note. (≤512 chars) |
Example request
curl -X POST https://paid.my/api/v1/payout \
-H "payout-api-key: paidmy_pk_…" \
-H "Idempotency-Key: $(uuidgen)" \
-H "content-type: application/json" \
-d '{"address":"TXYZ…","currency":"USDT","amount":25,"network":"TRON"}'Response data
| Field | Type | Description |
|---|---|---|
track_id | string | Payout identifier. |
status | string | Always processing on creation. |
{
"data": {
"track_id": "Zx9v8W7uQ6Tp",
"status": "processing"
},
"message": "Success",
"error": null,
"status": 200,
"version": "1.0"
}Errors
| Status | When |
|---|---|
400 | Validation error. error.key is amount (insufficient balance — or, when your balances are shown per network, not enough of that currency held on the requested network; the message says how much is available there), network (unavailable pair), network_inventory_low (the platform's hot wallet on that network can't cover the amount right now — retry later or use another network), below_minimum, or not_allowlisted. |
401 | Missing/invalid API key, or wrong scope (auth_error). |
409 | Idempotency-key body mismatch or request still processing. |
429 | Rate limited (rate_limited); honour the Retry-After header. |
List payouts
payout-api-keyQuery parameters
| Name | Type | Description |
|---|---|---|
page | integer | Page number. (1–…, default 1) |
size | integer | Page size. (1–200, default 10) |
status | string | Filter by status. |
Example request
curl https://paid.my/api/v1/payout?status=paid \
-H "payout-api-key: paidmy_pk_…"Response data
| Field | Type | Description |
|---|---|---|
list | object[] | |
meta | object |
Errors
| Status | When |
|---|---|
400 | Validation error (error.type = validation_error, error.key names the field). |
401 | Missing/invalid API key, or wrong scope (auth_error). |
429 | Rate limited (rate_limited); honour the Retry-After header. |
Estimate the fee
payout-api-keyRequest body
| Field | Type | Description |
|---|---|---|
currencyrequired | string | Currency code. |
networkrequired | string | Chain id. |
amountrequired | number | Amount to deliver. |
Example request
curl -X POST https://paid.my/api/v1/payout/estimate-fee \
-H "payout-api-key: paidmy_pk_…" \
-H "content-type: application/json" \
-d '{"currency":"USDT","network":"TRON","amount":25}'Response data
| Field | Type | Description |
|---|---|---|
currency | string | Currency. |
network | string | Chain. |
amount | number | Requested amount. |
fee | number | Flat network fee. |
total_debit | number | Amount + fee held from your balance. |
min_payout | number | Minimum payout on this network. |
{
"data": {
"currency": "USDT",
"network": "TRON",
"amount": 25,
"fee": 1,
"total_debit": 26,
"min_payout": 2
},
"message": "Success",
"error": null,
"status": 200,
"version": "1.0"
}Errors
| Status | When |
|---|---|
400 | Validation error (error.type = validation_error, error.key names the field). |
401 | Missing/invalid API key, or wrong scope (auth_error). |
429 | Rate limited (rate_limited); honour the Retry-After header. |
Get a payout
payout-api-keyPath parameters
| Name | Type | Description |
|---|---|---|
track_id | string | Payout identifier. |
Example request
curl https://paid.my/api/v1/payout/9YfKp2hQ7Wm3 \
-H "payout-api-key: paidmy_pk_…"Response data
| Field | Type | Description |
|---|---|---|
track_id | string | Payout identifier. |
status | "processing" | "pending" | "confirming" | "confirmed" | "canceled" | "rejected" | "failed" | See Payout statuses. |
amount | string | Amount delivered to the destination. Decimal string — never parse into a float for accounting. |
fee | string | Network fee charged on top. Decimal string — never parse into a float for accounting. |
currency | string | Currency code. |
network | string | Chain id. |
address | string | Destination address. |
memo | string | null | Memo/tag. |
tx_hash | string | null | On-chain transaction hash once broadcast. |
sandbox | boolean | True for sandbox payouts. |
date | integer | Creation. Unix timestamp (seconds). |
confirmed_at | integer | null | Confirmation time. Unix timestamp (seconds). |
{
"data": {
"track_id": "Zx9v8W7uQ6Tp",
"status": "confirmed",
"amount": "25",
"fee": "1",
"currency": "USDT",
"network": "TRON",
"address": "TXYZ…",
"memo": null,
"tx_hash": "b1946ac9…",
"sandbox": false,
"date": 1755169200,
"confirmed_at": 1755169500
},
"message": "Success",
"error": null,
"status": 200,
"version": "1.0"
}Errors
| Status | When |
|---|---|
400 | Validation error (error.type = validation_error, error.key names the field). |
401 | Missing/invalid API key, or wrong scope (auth_error). |
404 | Unknown track_id. |
429 | Rate limited (rate_limited); honour the Retry-After header. |
Swaps
Convert one balance into another, or move a currency between networks. Executed through swap.my; the result is credited to your balance.
Quote a swap
payout-api-keyIndicative quote for selling amount of from_currency into to_currency. Nothing moves. amount_out is what you would be credited (the provider's gross output minus the platform fee). Omit the networks to let Paid.my pick: the funding network is whichever of your balances can cover the amount, the delivery network is the platform default for that currency. To move the same currency between networks (e.g. USDT on BSC → USDT on ETH), set both from_network and to_network.
Request body
| Field | Type | Description |
|---|---|---|
from_currencyrequired | string | Currency to sell. |
to_currencyrequired | string | Currency to buy. |
amountrequired | number | Amount of from_currency to sell. |
from_network | string | Chain to fund from. Required only when your balance is split across networks and you want a specific one. |
to_network | string | Chain to receive on. |
Example request
curl -X POST https://paid.my/api/v1/swap/quote \
-H "payout-api-key: paidmy_pk_…" \
-H "content-type: application/json" \
-d '{"from_currency":"XMR","to_currency":"USDT","amount":0.5,"to_network":"TRON"}'Response data
| Field | Type | Description |
|---|---|---|
from_currency | string | Currency sold. |
from_network | string | Chain the balance would be taken from. |
to_currency | string | Currency bought. |
to_network | string | Chain it would be credited on. |
amount | string | Amount sold. Decimal string — never parse into a float for accounting. |
amount_out | string | Net amount you would be credited. Decimal string — never parse into a float for accounting. |
amount_out_gross | string | Provider output before the platform fee. Decimal string — never parse into a float for accounting. |
fee | string | Platform fee. Decimal string — never parse into a float for accounting. |
fee_bps | integer | Platform fee in basis points. |
rate | string | Net to units per from unit. Decimal string — never parse into a float for accounting. |
min_usd | number | Minimum swap size in USD. |
time_estimate | integer | null | Provider's estimate in minutes. |
{
"data": {
"from_currency": "XMR",
"from_network": "XMR",
"to_currency": "USDT",
"to_network": "TRON",
"amount": "0.5",
"amount_out": "149.688",
"amount_out_gross": "151.20",
"fee": "1.512",
"fee_bps": 100,
"rate": "299.376",
"min_usd": 10,
"time_estimate": 10
},
"message": "Success",
"error": null,
"status": 200,
"version": "1.0"
}Errors
| Status | When |
|---|---|
400 | Validation error. error.key is to_currency (same currency without a network move, or unsupported pair), from_network (not enough of the currency held on that network), below_minimum (under the route or USD minimum — the message includes the figure), temporarily_unavailable or quote_failed. |
401 | Missing/invalid API key, or wrong scope (auth_error). |
429 | Rate limited (rate_limited); honour the Retry-After header. |
Create a swap
payout-api-keyIdempotency-Key supportedRate limit: 10 / minuteQuotes the pair live, holds amount from your available balance and queues execution. The response carries expected_amount, the net credit at the quoted rate; the final credited_amount can differ slightly with market movement. Completion typically takes 5–20 minutes and arrives as a swap.completed webhook (or swap.failed, which releases the hold). Use an Idempotency-Key — retries are then safe.
Request body
| Field | Type | Description |
|---|---|---|
from_currencyrequired | string | Currency to sell. |
to_currencyrequired | string | Currency to buy. |
amountrequired | number | Amount of from_currency to sell. |
from_network | string | Chain to fund from (see quote). |
to_network | string | Chain to receive on. |
callback_url | string (uri) | Webhook target for this swap. Defaults to the key's or your account's callback URL. |
Example request
curl -X POST https://paid.my/api/v1/swap \
-H "payout-api-key: paidmy_pk_…" \
-H "Idempotency-Key: $(uuidgen)" \
-H "content-type: application/json" \
-d '{"from_currency":"XMR","to_currency":"USDT","amount":0.5,"to_network":"TRON"}'Response data
{
"data": {
"track_id": "Qw3rTy8uIo1p",
"status": "processing",
"from_currency": "XMR",
"from_network": "XMR",
"to_currency": "USDT",
"to_network": "TRON",
"amount": "0.5",
"quoted_amount": "151.20",
"fee": null,
"credited_amount": null,
"failure_reason": null,
"sandbox": false,
"date": 1755169200,
"completed_at": null,
"expected_amount": "149.688"
},
"message": "Success",
"error": null,
"status": 200,
"version": "1.0"
}Errors
| Status | When |
|---|---|
400 | Validation error. error.key is amount (insufficient balance), from_network (not enough on that network), below_minimum, to_currency (unsupported pair / same currency), temporarily_unavailable or quote_failed. |
401 | Missing/invalid API key, or wrong scope (auth_error). |
409 | Idempotency-key body mismatch or request still processing. |
429 | Rate limited (rate_limited); honour the Retry-After header. |
List swaps
payout-api-keyQuery parameters
| Name | Type | Description |
|---|---|---|
page | integer | Page number. (1–…, default 1) |
size | integer | Page size. (1–200, default 10) |
status | string | Filter by status. |
Example request
curl https://paid.my/api/v1/swap?status=paid \
-H "payout-api-key: paidmy_pk_…"Response data
| Field | Type | Description |
|---|---|---|
list | object[] | |
meta | object |
Errors
| Status | When |
|---|---|
400 | Validation error (error.type = validation_error, error.key names the field). |
401 | Missing/invalid API key, or wrong scope (auth_error). |
429 | Rate limited (rate_limited); honour the Retry-After header. |
Get a swap
payout-api-keyPoll until status is terminal (completed, failed or refunded).
Path parameters
| Name | Type | Description |
|---|---|---|
track_id | string | Swap identifier. |
Example request
curl https://paid.my/api/v1/swap/9YfKp2hQ7Wm3 \
-H "payout-api-key: paidmy_pk_…"Response data
| Field | Type | Description |
|---|---|---|
track_id | string | Swap identifier. |
status | "processing" | "sending" | "exchanging" | "completed" | "failed" | "refunded" | See Swap statuses. |
from_currency | string | Currency sold. |
from_network | string | Network the sold balance was taken from. |
to_currency | string | Currency bought. |
to_network | string | Network the bought currency is credited on. |
amount | string | Amount sold (held from your balance at creation). Decimal string — never parse into a float for accounting. |
quoted_amount | string | Gross provider quote at creation, before the platform fee. Decimal string — never parse into a float for accounting. |
fee | string | null | Platform fee taken from the output. Null until completed. Decimal string — never parse into a float for accounting. |
credited_amount | string | null | Net amount credited to your balance. Null until completed. Decimal string — never parse into a float for accounting. |
failure_reason | string | null | Why it failed or was refunded. |
sandbox | boolean | True for sandbox swaps. |
date | integer | Creation. Unix timestamp (seconds). |
completed_at | integer | null | Completion time. Unix timestamp (seconds). |
{
"data": {
"track_id": "Qw3rTy8uIo1p",
"status": "completed",
"from_currency": "XMR",
"from_network": "XMR",
"to_currency": "USDT",
"to_network": "TRON",
"amount": "0.5",
"quoted_amount": "151.20",
"fee": "1.505",
"credited_amount": "148.995",
"failure_reason": null,
"sandbox": false,
"date": 1755169200,
"completed_at": 1755169980
},
"message": "Success",
"error": null,
"status": 200,
"version": "1.0"
}Errors
| Status | When |
|---|---|
400 | Validation error (error.type = validation_error, error.key names the field). |
401 | Missing/invalid API key, or wrong scope (auth_error). |
404 | Unknown track_id. |
429 | Rate limited (rate_limited); honour the Retry-After header. |
General
Account-level reads — balances.
Account balances
general-api-keyEvery currency balance on your account. available is spendable; pending is held by in-flight payouts and swaps. usd is an indicative valuation at current display prices, not a settlement rate. Sandbox keys return sandbox balances.
Example request
curl https://paid.my/api/v1/general/account/balance \
-H "general-api-key: paidmy_gk_…"Response data
| Field | Type | Description |
|---|---|---|
balances | object | Keyed by currency symbol. Includes zero rows for every enabled currency. |
total_usd | string | Sum of all usd values. Decimal string — never parse into a float for accounting. |
sandbox | boolean | True when queried with a sandbox key. |
{
"data": {
"balances": {
"BTC": {
"available": "0.0125",
"pending": "0",
"usd": "802.88"
},
"USDT": {
"available": "120.5",
"pending": "25",
"usd": "120.38"
},
"GRAM": {
"available": "0",
"pending": "0",
"usd": "0.00"
}
},
"total_usd": "923.26",
"sandbox": false
},
"message": "Success",
"error": null,
"status": 200,
"version": "1.0"
}Errors
| Status | When |
|---|---|
400 | Validation error (error.type = validation_error, error.key names the field). |
401 | Missing/invalid API key, or wrong scope (auth_error). |
429 | Rate limited (rate_limited); honour the Retry-After header. |
Common
Public reference data — no API key required.
Supported currencies
Every enabled currency with its networks, minimums and payout fees. Public — no API key.
Example request
curl https://paid.my/api/v1/common/currenciesResponse data
{
"data": {
"USDT": {
"symbol": "USDT",
"name": "Tether",
"status": true,
"networks": {
"TRON": {
"network": "TRON",
"name": "TRON",
"required_confirmations": 1,
"contract_address": "TR7NHqje…",
"deposit_min": "0.5",
"deposit_fee": "0.5",
"withdraw_min": "2",
"withdraw_fee": "1"
}
}
}
},
"message": "Success",
"error": null,
"status": 200,
"version": "1.0"
}Prices
USD price per unit for every enabled currency, as strings. Public — no API key.
Example request
curl https://paid.my/api/v1/common/pricesResponse data
{
"data": {
"BTC": "64230.10",
"ETH": "3120.55",
"USDT": "1.00",
"GRAM": "1.40"
},
"message": "Success",
"error": null,
"status": 200,
"version": "1.0"
}Webhooks
When an invoice, static-address deposit, payout or swap changes state, Paid.my POSTs a JSON payload to the relevant callback_url (or your merchant default from Settings).
Signature
Every delivery is signed with HMAC-SHA512 over the raw request body, keyed with your webhook secret (Settings — a dedicated secret, not an API key). The lowercase hex digest is sent in two headers with the same value: HMAC (OxaPay-compatible) and paidmy-signature. Also sent: paidmy-event (event type) and paidmy-delivery-id (unique per delivery — use it to de-duplicate). Compute the HMAC over the raw bytes, never a re-serialized object.
const crypto = require("node:crypto");
function verify(rawBody, secret, signatureHeader) {
const expected = crypto
.createHmac("sha512", secret)
.update(rawBody, "utf8")
.digest("hex");
return (
expected.length === signatureHeader.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader))
);
}
// Express example — keep the raw body!
app.post("/webhooks/paidmy", express.raw({ type: "*/*" }), (req, res) => {
if (!verify(req.body, process.env.PAIDMY_WEBHOOK_SECRET, req.get("HMAC") ?? ""))
return res.status(401).end();
const event = JSON.parse(req.body);
// event.type: "invoice" | "static_address" | "payout"; event.status: "Paid" …
res.sendStatus(200);
});Delivery and retries
Respond with any 2xx within 10 seconds. On failure or timeout the delivery is retried:
| Attempt | Delay after previous failure |
|---|---|
| 2 | 1 minute |
| 3 | 3 minutes |
| 4 | 30 minutes |
| 5 | 3 hours |
| 6 | 9 hours |
After the ladder is exhausted the delivery is marked EXHAUSTED; re-send it from the dashboard (Payments → open a payment → Webhook deliveries → Resend). Deliveries are at-least-once.
Events
invoice.paid / invoice.paying / invoice.underpaid
POSTed to the invoice's callback_url (or your default) when a payment changes state. Signed with HMAC-SHA512 of the raw body in the HMAC and paidmy-signature headers; paidmy-event carries the event type, paidmy-delivery-id is unique per delivery. Respond 2xx within 10s. An underpaid invoice that you later accept from the dashboard produces a normal invoice.paid (with received_amount below pay_amount). A payment that lands after expiry carries late: true. If the customer sent a different token than they selected and an operator settles the invoice with it, the invoice is re-quoted in that asset and its webhooks carry wrong_asset: true plus asked_currency.
{
"type": "invoice",
"track_id": "9YfKp2hQ7Wm3",
"status": "Paid",
"amount": "100",
"currency": "USD",
"pay_currency": "USDT",
"pay_amount": "100.5",
"received_amount": "100.5",
"fee_paid_by_payer": 1,
"under_paid_coverage": 0,
"order_id": "order-1042",
"email": "[email protected]",
"description": "Pro plan",
"sandbox": false,
"date": 1755169900
}static_address.deposit
A deposit on a static address was credited.
{
"type": "static_address",
"track_id": "Ab3dEfGh1jKl",
"status": "Paid",
"address": "TVxq8…",
"amount": "50",
"credited_amount": "49",
"fee": "1",
"currency": "USDT",
"network": "TRON",
"tx_hash": "6f9c2c6f6f0a…",
"sandbox": false,
"date": 1755169900
}swap.completed / swap.failed
POSTed to the swap's callback_url when it reaches a terminal state. Completed means credited_amount is on your balance; Failed and Refunded mean the held amount was returned to your available balance. Same fields as GET /swap/{track_id} plus type: "swap".
{
"type": "swap",
"track_id": "Qw3rTy8uIo1p",
"status": "Completed",
"from_currency": "XMR",
"from_network": "XMR",
"to_currency": "USDT",
"to_network": "TRON",
"amount": "0.5",
"quoted_amount": "151.20",
"fee": "1.505",
"credited_amount": "148.995",
"failure_reason": null,
"sandbox": false,
"date": 1755169980,
"completed_at": 1755169980
}payout.confirmed / payout.failed
For payout.failed, status is Failed (or Rejected when a held payout was declined in review) and the held amount + fee is returned to your available balance.
{
"type": "payout",
"track_id": "Zx9v8W7uQ6Tp",
"status": "Confirmed",
"amount": "25",
"fee": "1",
"currency": "USDT",
"network": "TRON",
"address": "TXYZ…",
"memo": null,
"tx_hash": "b1946ac9…",
"description": null,
"sandbox": false,
"date": 1755169950
}invoice.paying is sent with status: "Paying" when a partial payment is confirmed — the hosted page then shows the payer exactly how much is still owed, and a top-up to the same address completes the invoice normally. invoice.underpaid (status: "Underpaid") is sent when the invoice expires with a partial amount; those funds are not credited automatically. You can accept the shortfall from Payments in the dashboard (the received amount is credited and a regular invoice.paid webhook follows) or decline it (the invoice becomes expired).
Sandbox mode
Integrate end-to-end with no real crypto. Enable sandbox in Settings → Sandbox mode, then:
- Create keys with the Sandbox toggle — they are prefixed
paidmy_smk_,paidmy_spk_,paidmy_sgk_. - Requests with a sandbox key operate on sandbox invoices, balances and payouts. A live merchant key can also pass
"sandbox": truewhen creating an invoice. - Sandbox invoices show a Simulate payment button on the hosted page that fabricates a confirmed deposit and runs the full crediting + webhook pipeline.
- Sandbox activity is flagged
"sandbox": truein every response and webhook; balances are segregated from live.
Fees
| Product | Default fee | Notes |
|---|---|---|
| Invoices | 1% (100 bps) | fee_paid_by_payer adds it to the payer's amount instead of deducting from your credit |
| Static addresses | 1% (100 bps) | Charged per credited deposit |
| Network fee on deposits | Flat per network | Taken from each credited payment on top of the percentage, sized to that chain's sweep cost; read deposit_fee from GET /common/currencies. With fee_paid_by_payer it is added to the payer's amount. |
| Payouts | Flat per network | Quote with POST /payout/estimate-fee, or read withdraw_fee from GET /common/currencies |
| Auto-convert to USDT | 0% | Enable per API key or pass to_currency: "USDT". Runs once the deposit is swept (typically within ~15 min); only swap.my's exchange spread applies. Progress is in the payment's conversion field. |
| Swaps | 1% (100 bps) | Taken from the output of POST /swap, on top of swap.my's exchange spread. POST /swap/quote shows the net figure before you commit. |
Status reference
Payment statuses
| Status | Meaning |
|---|---|
new | Invoice created; payer has not picked a currency yet |
waiting | Currency selected; waiting for funds at the deposit address |
paying | Partial/unconfirmed payment detected; waiting for the rest or confirmations |
paid | Fully paid and credited to your balance (terminal) |
underpaid | Expired with less than the required amount received. Accept it from the dashboard to credit what arrived (→ paid, with an invoice.paid webhook) or decline it (→ expired) |
expired | Lifetime elapsed with no payment. The address stays watched for a grace period (24h for BTC/LTC, 12h otherwise): a late payment still completes the invoice (→ paid, or → underpaid if short) and its webhook carries late: true |
refunding | Refund in progress |
refunded | Refund completed (terminal) |
failed | Processing failure (terminal) |
Webhook payloads capitalize the status (Paid, Paying, Underpaid).
Wrong asset: if your customer sends a different token than they selected (USDC to an ETH address, say), the coins are held and our operators can settle the invoice with them. The invoice is re-quoted in the asset that arrived at that day's rate and takes the normal path (paid, or underpaid if the value is short). Its webhooks carry wrong_asset: true and asked_currency (what the customer originally chose); pay_currency, pay_amount and received_amount are in the asset that arrived.
Payout statuses
| Status | Meaning |
|---|---|
processing | Accepted; funds held; queued for broadcast |
pending | Held for review; funds stay on hold until an operator approves (→ processing) or rejects it (→ rejected) |
confirming | Broadcast; waiting for confirmations |
confirmed | Confirmed on-chain; fee settled (terminal) |
canceled | Canceled before broadcast; hold released (terminal) |
rejected | Rejected by review; hold released (terminal) |
failed | Broadcast/processing failed; hold released (terminal) |
Swap statuses
| Status | Meaning |
|---|---|
processing | Accepted; the sold amount is held; queued for execution |
sending | Provider order created; our deposit to the provider is being broadcast |
exchanging | Deposit sent; waiting for the provider to settle |
completed | Output credited to your balance, net of the platform fee (terminal) |
failed | Could not execute; hold released (terminal) |
refunded | Provider refunded the deposit; hold released (terminal) |
Webhook payloads capitalize the status (Completed, Failed, Refunded).
Recipes
Three end-to-end flows that cover most integrations. Each one names the endpoints and the webhook you will handle.
1. Checkout with the hosted page
For shops and services: you never touch a wallet address; the payer picks the coin on our page.
- Your customer reaches checkout with a total, say
49.90 USD. - Call
POST /payment/invoicewithamount,currency: "USD"(or EUR / GBP, or a crypto), yourorder_id, acallback_urland areturn_url. Send anIdempotency-Keyso a double submit cannot create two invoices. - Store the returned
track_idagainst the order and redirect the customer topayment_url. - They choose a coin and network; we show an address, a QR code and a live status. The amount is locked at the rate when they choose.
- When funds confirm you receive
invoice.paidat yourcallback_url. Verify theHMACsignature, look the order up bytrack_id(ororder_id), mark it paid, and answer 2xx. - The customer is sent to your
return_url. Treat that page as informational only — the webhook is the source of truth. - If they pay too little, you get
invoice.payingwhile the page asks for the remainder, andinvoice.underpaidif it expires short. Accept or decline underpaid invoices from Payments in the dashboard.
No webhook infrastructure yet? Poll GET /payment/{track_id} until status is paid or expired.
2. Checkout in your own UI (white-label)
For apps that want the address and QR code inside their own screens.
- Let the customer pick the coin and network in your UI.
GET /payment/accepted-currencieslists what your account accepts;GET /common/currenciescarries networks, minimums and fees. - Call
POST /payment/white-labelwith the fiat or cryptoamount,pay_currencyandnetwork. You get the depositaddress, exactpay_amount, aqr_codeandexpired_at. - Render the address, amount and a countdown to
expired_at. Show the amount as a copyable string; never round it. - Show live progress by polling
GET /payment/{track_id}every few seconds —txslists detected transactions with confirmations, andstatusmoveswaiting → paying → paid. - Fulfil on the
invoice.paidwebhook, exactly as in recipe 1.
3. Wallet top-ups with static addresses
For platforms where users hold a balance: exchanges, games, marketplaces, investment sites.
- When a user first opens their deposit screen for a coin, call
POST /payment/static-addresswith thenetwork(and optionallycurrencyto restrict it) and acallback_url. Save thetrack_idandaddresson the user; it is permanent. - Show the address and QR code. The user can deposit any number of times, with no invoice and no expiry.
- Each confirmed deposit fires
static_address.depositwithamount,credited_amount,feeandtx_hash. Credit the user's balance withcredited_amount, keyed ontx_hashso a redelivered webhook cannot credit twice. - To reconcile, or to run without webhooks, poll
GET /payment/static-address/{track_id}: every deposit is listed with its confirmations and, once credited, the same amounts the webhook carried. - Want everything held in USDT regardless of what users send? Pass
to_currency: "USDT"when creating the address (or enable auto-convert on the key). The deposit'sconversionfield tracks the swap. - Withdrawals are payouts:
POST /payoutwith the user's address and amount using a Payout-scope key, then settle the user's balance onpayout.confirmed(or restore it onpayout.failed). Quote the fee first withPOST /payout/estimate-feeif you show it to the user. - If a user closes their account,
POST /payment/static-address/revokestops automatic crediting for that address.
Treasury: converting and moving balances
Balances are held per currency and per network. To turn XMR income into USDT, or move USDT from BSC to TRON before paying out there, quote with POST /swap/quote, commit with POST /swap and wait for swap.completed. Swaps and payouts both use the Payout-scope key. A rule of thumb: keep the stablecoin you pay out in on the network your users withdraw to most, so payouts never wait on a swap.