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.

Base URL
https://paid.my/api/v1
OpenAPI spec
/api/openapi.json
Import into Postman, Insomnia or Swagger UI, or generate a client.

Quick start

  1. Create an account and go to Merchant Service → Generate API Key. Pick the Merchant scope. The key is shown once.
  2. Call POST /payment/invoice with an amount — you get back a payment_url.
  3. Redirect your customer there. They pick a coin and pay.
  4. Receive a signed invoice.paid webhook at your callback_url, or poll GET /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.

ScopeHeaderLive prefixSandbox prefixUsed for
Merchantmerchant-api-keypaidmy_mk_paidmy_smk_Creating/reading payments, static addresses
Payoutpayout-api-keypaidmy_pk_paidmy_spk_Sending/reading payouts and swaps (anything that moves your balance)
Generalgeneral-api-keypaidmy_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

HTTPerror.typeMeaningWhat to do
400validation_errorThe request was understood but cannot be fulfilled as sent. key says why (table below).Fix the request; do not retry unchanged.
401auth_errorMissing, revoked or wrong-scope API key.Check the header and that the key's scope matches the endpoint (Authentication).
403forbiddenThe key is valid but this account may not perform the action.Contact support if unexpected.
404not_foundUnknown track_id, or it belongs to another account.Check the identifier; sandbox and live records are separate.
409conflictDuplicate 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.
429rate_limitedToo many requests for this key.Wait for Retry-After seconds, then retry.
500internal_errorSomething 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.keyEndpointsMeaning
bodyany POSTBody is not valid JSON.
amountpayout, swapInsufficient available balance for the amount plus fee (the message says how much you have). On payment endpoints: missing or out of range.
pay_currencywhite-labelNot in your accepted currencies, not enabled, or its rate is temporarily unavailable.
networkwhite-label, static-address, payoutCurrency 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_networkswapNot 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_currencypayment, swapOn payments: only USDT is supported for auto-convert. On swaps: same currency without a network move, or an unsupported pair.
below_minimumpayout, swapUnder the network's minimum payout, the swap USD minimum, or the provider's route minimum. The message includes the figure.
not_allowlistedpayoutAddress allowlisting is on for this account and the destination is not on the list.
network_inventory_lowpayoutThe platform's hot wallet on that network cannot cover the amount right now. Retry later or use another network; nothing was held.
temporarily_unavailableswapThe swap provider or a hot wallet is unreachable. Retry in a minute; nothing was held.
quote_failedswapThe 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).

BucketLimitApplies to
General5 req/s sustained (burst 20)All endpoints unless listed below
Invoice creation60 / minutePOST /payment/invoice
Payout creation10 / minutePOST /payout

Payments

Invoices, hosted checkout and static deposit addresses.

Create an invoice

post/api/v1/payment/invoice
Auth: Merchant key · merchant-api-keyIdempotency-Key supportedRate limit: 60 / minute

Creates 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

FieldTypeDescription
amountrequirednumberInvoice amount in currency. Must be > 0.
currencystringFiat (USD, EUR, GBP) or a crypto code. (default "USD")
lifetimeintegerMinutes until expiry. (15–2880, default 60)
fee_paid_by_payerboolean | "0" | "1"Charge the platform fee to the payer. Default: API key setting, then account setting.
under_paid_coveragenumberAccepted shortfall in percent. Default: API key setting, then account setting. (0–60)
callback_urlstring (uri)Webhook target for this invoice. Default: API key callback URL, then account default.
return_urlstring (uri)Shown to the payer after payment.
order_idstringYour reference. Unique per merchant — reuse returns 409. (≤128 chars)
emailstring (email)Payer email for your records.
descriptionstringShown on the payment page. (≤512 chars)
thanks_messagestringShown 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.
sandboxbooleanForce 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

FieldTypeDescription
track_idstringPayment identifier.
payment_urlstring (uri)Hosted checkout URL.
expired_atintegerExpiry. Unix timestamp (seconds).
dateintegerCreation. 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

StatusWhen
400Validation error (error.type = validation_error, error.key names the field).
401Missing/invalid API key, or wrong scope (auth_error).
409order_id already used, or idempotency-key body mismatch.
429Rate limited (rate_limited); honour the Retry-After header.

Create a white-label payment

post/api/v1/payment/white-label
Auth: Merchant key · merchant-api-keyIdempotency-Key supportedRate limit: 60 / minute

Creates 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

FieldTypeDescription
amountrequirednumberInvoice amount in currency. Must be > 0.
currencystringFiat (USD, EUR, GBP) or a crypto code. (default "USD")
pay_currencyrequiredstringCrypto the payer will send.
networkstringChain id. Required when pay_currency is available on more than one network (the error lists them).
lifetimeintegerMinutes the address stays valid. (15–2880, default 60)
fee_paid_by_payerboolean | "0" | "1"Charge the platform fee to the payer. Default: API key setting, then account setting.
under_paid_coveragenumberAccepted shortfall in percent. Default: API key setting, then account setting. (0–60)
callback_urlstring (uri)Webhook target for this payment.
order_idstringYour reference. Unique per merchant; reuse returns 409. (≤128 chars)
emailstring (email)Payer email for your records.
descriptionstringYour 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.
sandboxbooleanForce 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

FieldTypeDescription
track_idstringPayment identifier.
status"new" | "waiting" | "paying" | "paid" | "underpaid" | "expired" | "refunding" | "refunded" | "failed"See Payment statuses.
amountstringInvoice amount in currency. Decimal string — never parse into a float for accounting.
currencystringInvoice currency (fiat or crypto code).
pay_currencystring | nullCrypto the payer selected.
networkstring | nullChain the payer selected.
pay_amountstringExact crypto amount requested from the payer. Decimal string — never parse into a float for accounting.
received_amountstringCrypto received so far. Decimal string — never parse into a float for accounting.
credited_amountstringAmount credited to your balance after fees. Decimal string — never parse into a float for accounting.
fee_amountstringPlatform fee. Decimal string — never parse into a float for accounting.
ratestringLocked crypto/fiat rate. Decimal string — never parse into a float for accounting.
addressstring | nullDeposit address allocated for this payment.
memostring | nullMemo/tag where the chain needs one.
fee_paid_by_payer"0" | "1"1 if the payer covered the fee.
under_paid_coveragenumberAccepted shortfall, percent (0–60).
lifetimeintegerLifetime in minutes.
order_idstring | nullYour order reference.
emailstring | nullPayer email.
descriptionstring | nullDescription shown on the payment page.
sandboxbooleanTrue for sandbox payments.
auto_convertbooleanWhether the credited amount is auto-converted to USDT.
conversionobject | nullAuto-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_atintegerExpiry. Unix timestamp (seconds).
dateintegerCreation. Unix timestamp (seconds).
paid_atinteger | nullPaid time. Unix timestamp (seconds).
txsobject[]On-chain deposits matched to this payment.
payment_urlstring (uri)Hosted checkout URL, should you still want to offer it.
qr_codestring (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

StatusWhen
400Validation error. error.key is pay_currency (not accepted / rate unavailable) or network (ambiguous or unavailable; the message lists valid networks).
401Missing/invalid API key, or wrong scope (auth_error).
409order_id already used, or idempotency-key body mismatch.
429Rate limited (rate_limited); honour the Retry-After header.

Get a payment

get/api/v1/payment/{track_id}
Auth: Merchant key · merchant-api-key

Path parameters

NameTypeDescription
track_idstringPayment identifier.

Example request

curl https://paid.my/api/v1/payment/9YfKp2hQ7Wm3 \
  -H "merchant-api-key: paidmy_mk_…"

Response data

FieldTypeDescription
track_idstringPayment identifier.
status"new" | "waiting" | "paying" | "paid" | "underpaid" | "expired" | "refunding" | "refunded" | "failed"See Payment statuses.
amountstringInvoice amount in currency. Decimal string — never parse into a float for accounting.
currencystringInvoice currency (fiat or crypto code).
pay_currencystring | nullCrypto the payer selected.
networkstring | nullChain the payer selected.
pay_amountstringExact crypto amount requested from the payer. Decimal string — never parse into a float for accounting.
received_amountstringCrypto received so far. Decimal string — never parse into a float for accounting.
credited_amountstringAmount credited to your balance after fees. Decimal string — never parse into a float for accounting.
fee_amountstringPlatform fee. Decimal string — never parse into a float for accounting.
ratestringLocked crypto/fiat rate. Decimal string — never parse into a float for accounting.
addressstring | nullDeposit address allocated for this payment.
memostring | nullMemo/tag where the chain needs one.
fee_paid_by_payer"0" | "1"1 if the payer covered the fee.
under_paid_coveragenumberAccepted shortfall, percent (0–60).
lifetimeintegerLifetime in minutes.
order_idstring | nullYour order reference.
emailstring | nullPayer email.
descriptionstring | nullDescription shown on the payment page.
sandboxbooleanTrue for sandbox payments.
auto_convertbooleanWhether the credited amount is auto-converted to USDT.
conversionobject | nullAuto-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_atintegerExpiry. Unix timestamp (seconds).
dateintegerCreation. Unix timestamp (seconds).
paid_atinteger | nullPaid time. Unix timestamp (seconds).
txsobject[]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

StatusWhen
400Validation error (error.type = validation_error, error.key names the field).
401Missing/invalid API key, or wrong scope (auth_error).
404Unknown track_id.
429Rate limited (rate_limited); honour the Retry-After header.

List payments

get/api/v1/payment
Auth: Merchant key · merchant-api-key

Query parameters

NameTypeDescription
pageintegerPage number. (1–…, default 1)
sizeintegerPage size. (1–200, default 10)
statusstringFilter by status, e.g. paid, waiting.
order_idstringFilter 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

FieldTypeDescription
listobject[]
metaobject
{
  "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

StatusWhen
400Validation error (error.type = validation_error, error.key names the field).
401Missing/invalid API key, or wrong scope (auth_error).
429Rate limited (rate_limited); honour the Retry-After header.

Accepted currencies

get/api/v1/payment/accepted-currencies
Auth: Merchant key · merchant-api-key

Currency 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

FieldTypeDescription
liststring[]
{
  "data": {
    "list": [
      "BTC",
      "ETH",
      "USDT",
      "USDC",
      "DAI",
      "TRX",
      "LTC",
      "SOL",
      "BNB",
      "POL",
      "GRAM",
      "XMR"
    ]
  },
  "message": "Success",
  "error": null,
  "status": 200,
  "version": "1.0"
}

Errors

StatusWhen
400Validation error (error.type = validation_error, error.key names the field).
401Missing/invalid API key, or wrong scope (auth_error).
429Rate limited (rate_limited); honour the Retry-After header.

Create a static address

post/api/v1/payment/static-address
Auth: Merchant key · merchant-api-keyIdempotency-Key supported

A permanent deposit address. Every confirmed deposit is credited individually (static-address fee applies) and fires a static_address.deposit webhook.

Request body

FieldTypeDescription
networkrequiredstringChain id, e.g. TRON, ETH, BSC, POLYGON, BTC, LTC, SOL, TON.
currencystringRestrict to one currency; omit to accept any enabled token on the chain.
callback_urlstring (uri)Webhook target for deposits.
order_idstringReporting only. (≤128 chars)
emailstring (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

FieldTypeDescription
track_idstringIdentifier.
networkstringChain id.
currencystring | nullCurrency or null.
addressstringDeposit address.
memostring | nullMemo/tag.
dateintegerCreation. 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

StatusWhen
400Validation error (error.type = validation_error, error.key names the field).
401Missing/invalid API key, or wrong scope (auth_error).
429Rate limited (rate_limited); honour the Retry-After header.

List static addresses

get/api/v1/payment/static-address
Auth: Merchant key · merchant-api-key

Query parameters

NameTypeDescription
pageintegerPage number. (1–…, default 1)
sizeintegerPage size. (1–200, default 10)
networkstringFilter by chain id.

Example request

curl https://paid.my/api/v1/payment/static-address?network=… \
  -H "merchant-api-key: paidmy_mk_…"

Response data

FieldTypeDescription
listobject[]
metaobject
{
  "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

StatusWhen
400Validation error (error.type = validation_error, error.key names the field).
401Missing/invalid API key, or wrong scope (auth_error).
429Rate limited (rate_limited); honour the Retry-After header.

Revoke a static address

post/api/v1/payment/static-address/revoke
Auth: Merchant key · merchant-api-key

Deposits arriving after revocation are not credited automatically.

Request body

FieldTypeDescription
addressrequiredstringThe 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

FieldTypeDescription
revokedbooleanAlways true on success.
{
  "data": {
    "revoked": true
  },
  "message": "Success",
  "error": null,
  "status": 200,
  "version": "1.0"
}

Errors

StatusWhen
400Validation error (error.type = validation_error, error.key names the field).
401Missing/invalid API key, or wrong scope (auth_error).
404Address not found.
429Rate limited (rate_limited); honour the Retry-After header.

Get a static address and its deposits

get/api/v1/payment/static-address/{track_id}
Auth: Merchant key · merchant-api-key

Poll 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

NameTypeDescription
track_idstringStatic address identifier.

Example request

curl https://paid.my/api/v1/payment/static-address/9YfKp2hQ7Wm3 \
  -H "merchant-api-key: paidmy_mk_…"

Response data

FieldTypeDescription
track_idstringStatic address identifier.
networkstringChain id.
currencystring | nullRestricted currency, or null for any enabled token on the chain.
addressstringThe permanent deposit address.
memostring | nullMemo/tag where the chain needs one.
callback_urlstring | nullWebhook target for this address.
auto_convertbooleanWhether deposits are auto-converted to USDT.
revokedbooleanTrue once revoked; later deposits are not credited automatically.
sandboxbooleanTrue for sandbox addresses.
dateintegerCreation. Unix timestamp (seconds).
depositsobject[]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

StatusWhen
400Validation error (error.type = validation_error, error.key names the field).
401Missing/invalid API key, or wrong scope (auth_error).
404Unknown track_id.
429Rate limited (rate_limited); honour the Retry-After header.

Payouts

Send crypto from your balance to any external address.

Send a payout

post/api/v1/payout
Auth: Payout key · payout-api-keyIdempotency-Key supportedRate limit: 10 / minute

Sends 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

FieldTypeDescription
addressrequiredstringDestination address (the recipient's wallet).
currencyrequiredstringCurrency code.
amountrequirednumberAmount to deliver — the fee is charged on top.
networkstringChain id. Required when the currency exists on more than one enabled network.
memostringMemo/tag for chains that use one (e.g. TON exchanges). (≤256 chars)
callback_urlstring (uri)Webhook target for this payout.
descriptionstringYour 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

FieldTypeDescription
track_idstringPayout identifier.
statusstringAlways processing on creation.
{
  "data": {
    "track_id": "Zx9v8W7uQ6Tp",
    "status": "processing"
  },
  "message": "Success",
  "error": null,
  "status": 200,
  "version": "1.0"
}

Errors

StatusWhen
400Validation 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.
401Missing/invalid API key, or wrong scope (auth_error).
409Idempotency-key body mismatch or request still processing.
429Rate limited (rate_limited); honour the Retry-After header.

List payouts

get/api/v1/payout
Auth: Payout key · payout-api-key

Query parameters

NameTypeDescription
pageintegerPage number. (1–…, default 1)
sizeintegerPage size. (1–200, default 10)
statusstringFilter by status.

Example request

curl https://paid.my/api/v1/payout?status=paid \
  -H "payout-api-key: paidmy_pk_…"

Response data

FieldTypeDescription
listobject[]
metaobject

Errors

StatusWhen
400Validation error (error.type = validation_error, error.key names the field).
401Missing/invalid API key, or wrong scope (auth_error).
429Rate limited (rate_limited); honour the Retry-After header.

Estimate the fee

post/api/v1/payout/estimate-fee
Auth: Payout key · payout-api-key

Request body

FieldTypeDescription
currencyrequiredstringCurrency code.
networkrequiredstringChain id.
amountrequirednumberAmount 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

FieldTypeDescription
currencystringCurrency.
networkstringChain.
amountnumberRequested amount.
feenumberFlat network fee.
total_debitnumberAmount + fee held from your balance.
min_payoutnumberMinimum 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

StatusWhen
400Validation error (error.type = validation_error, error.key names the field).
401Missing/invalid API key, or wrong scope (auth_error).
429Rate limited (rate_limited); honour the Retry-After header.

Get a payout

get/api/v1/payout/{track_id}
Auth: Payout key · payout-api-key

Path parameters

NameTypeDescription
track_idstringPayout identifier.

Example request

curl https://paid.my/api/v1/payout/9YfKp2hQ7Wm3 \
  -H "payout-api-key: paidmy_pk_…"

Response data

FieldTypeDescription
track_idstringPayout identifier.
status"processing" | "pending" | "confirming" | "confirmed" | "canceled" | "rejected" | "failed"See Payout statuses.
amountstringAmount delivered to the destination. Decimal string — never parse into a float for accounting.
feestringNetwork fee charged on top. Decimal string — never parse into a float for accounting.
currencystringCurrency code.
networkstringChain id.
addressstringDestination address.
memostring | nullMemo/tag.
tx_hashstring | nullOn-chain transaction hash once broadcast.
sandboxbooleanTrue for sandbox payouts.
dateintegerCreation. Unix timestamp (seconds).
confirmed_atinteger | nullConfirmation 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

StatusWhen
400Validation error (error.type = validation_error, error.key names the field).
401Missing/invalid API key, or wrong scope (auth_error).
404Unknown track_id.
429Rate 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

post/api/v1/swap/quote
Auth: Payout key · payout-api-key

Indicative 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

FieldTypeDescription
from_currencyrequiredstringCurrency to sell.
to_currencyrequiredstringCurrency to buy.
amountrequirednumberAmount of from_currency to sell.
from_networkstringChain to fund from. Required only when your balance is split across networks and you want a specific one.
to_networkstringChain 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

FieldTypeDescription
from_currencystringCurrency sold.
from_networkstringChain the balance would be taken from.
to_currencystringCurrency bought.
to_networkstringChain it would be credited on.
amountstringAmount sold. Decimal string — never parse into a float for accounting.
amount_outstringNet amount you would be credited. Decimal string — never parse into a float for accounting.
amount_out_grossstringProvider output before the platform fee. Decimal string — never parse into a float for accounting.
feestringPlatform fee. Decimal string — never parse into a float for accounting.
fee_bpsintegerPlatform fee in basis points.
ratestringNet to units per from unit. Decimal string — never parse into a float for accounting.
min_usdnumberMinimum swap size in USD.
time_estimateinteger | nullProvider'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

StatusWhen
400Validation 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.
401Missing/invalid API key, or wrong scope (auth_error).
429Rate limited (rate_limited); honour the Retry-After header.

Create a swap

post/api/v1/swap
Auth: Payout key · payout-api-keyIdempotency-Key supportedRate limit: 10 / minute

Quotes 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

FieldTypeDescription
from_currencyrequiredstringCurrency to sell.
to_currencyrequiredstringCurrency to buy.
amountrequirednumberAmount of from_currency to sell.
from_networkstringChain to fund from (see quote).
to_networkstringChain to receive on.
callback_urlstring (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

StatusWhen
400Validation 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.
401Missing/invalid API key, or wrong scope (auth_error).
409Idempotency-key body mismatch or request still processing.
429Rate limited (rate_limited); honour the Retry-After header.

List swaps

get/api/v1/swap
Auth: Payout key · payout-api-key

Query parameters

NameTypeDescription
pageintegerPage number. (1–…, default 1)
sizeintegerPage size. (1–200, default 10)
statusstringFilter by status.

Example request

curl https://paid.my/api/v1/swap?status=paid \
  -H "payout-api-key: paidmy_pk_…"

Response data

FieldTypeDescription
listobject[]
metaobject

Errors

StatusWhen
400Validation error (error.type = validation_error, error.key names the field).
401Missing/invalid API key, or wrong scope (auth_error).
429Rate limited (rate_limited); honour the Retry-After header.

Get a swap

get/api/v1/swap/{track_id}
Auth: Payout key · payout-api-key

Poll until status is terminal (completed, failed or refunded).

Path parameters

NameTypeDescription
track_idstringSwap identifier.

Example request

curl https://paid.my/api/v1/swap/9YfKp2hQ7Wm3 \
  -H "payout-api-key: paidmy_pk_…"

Response data

FieldTypeDescription
track_idstringSwap identifier.
status"processing" | "sending" | "exchanging" | "completed" | "failed" | "refunded"See Swap statuses.
from_currencystringCurrency sold.
from_networkstringNetwork the sold balance was taken from.
to_currencystringCurrency bought.
to_networkstringNetwork the bought currency is credited on.
amountstringAmount sold (held from your balance at creation). Decimal string — never parse into a float for accounting.
quoted_amountstringGross provider quote at creation, before the platform fee. Decimal string — never parse into a float for accounting.
feestring | nullPlatform fee taken from the output. Null until completed. Decimal string — never parse into a float for accounting.
credited_amountstring | nullNet amount credited to your balance. Null until completed. Decimal string — never parse into a float for accounting.
failure_reasonstring | nullWhy it failed or was refunded.
sandboxbooleanTrue for sandbox swaps.
dateintegerCreation. Unix timestamp (seconds).
completed_atinteger | nullCompletion 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

StatusWhen
400Validation error (error.type = validation_error, error.key names the field).
401Missing/invalid API key, or wrong scope (auth_error).
404Unknown track_id.
429Rate limited (rate_limited); honour the Retry-After header.

General

Account-level reads — balances.

Account balances

get/api/v1/general/account/balance
Auth: General key · general-api-key

Every 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

FieldTypeDescription
balancesobjectKeyed by currency symbol. Includes zero rows for every enabled currency.
total_usdstringSum of all usd values. Decimal string — never parse into a float for accounting.
sandboxbooleanTrue 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

StatusWhen
400Validation error (error.type = validation_error, error.key names the field).
401Missing/invalid API key, or wrong scope (auth_error).
429Rate limited (rate_limited); honour the Retry-After header.

Common

Public reference data — no API key required.

Supported currencies

get/api/v1/common/currencies
Public · no API key

Every enabled currency with its networks, minimums and payout fees. Public — no API key.

Example request

curl https://paid.my/api/v1/common/currencies

Response 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

get/api/v1/common/prices
Public · no API key

USD price per unit for every enabled currency, as strings. Public — no API key.

Example request

curl https://paid.my/api/v1/common/prices

Response 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:

AttemptDelay after previous failure
21 minute
33 minutes
430 minutes
53 hours
69 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": true when 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": true in every response and webhook; balances are segregated from live.

Fees

ProductDefault feeNotes
Invoices1% (100 bps)fee_paid_by_payer adds it to the payer's amount instead of deducting from your credit
Static addresses1% (100 bps)Charged per credited deposit
Network fee on depositsFlat per networkTaken 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.
PayoutsFlat per networkQuote with POST /payout/estimate-fee, or read withdraw_fee from GET /common/currencies
Auto-convert to USDT0%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.
Swaps1% (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

StatusMeaning
newInvoice created; payer has not picked a currency yet
waitingCurrency selected; waiting for funds at the deposit address
payingPartial/unconfirmed payment detected; waiting for the rest or confirmations
paidFully paid and credited to your balance (terminal)
underpaidExpired 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)
expiredLifetime 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
refundingRefund in progress
refundedRefund completed (terminal)
failedProcessing 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

StatusMeaning
processingAccepted; funds held; queued for broadcast
pendingHeld for review; funds stay on hold until an operator approves (→ processing) or rejects it (→ rejected)
confirmingBroadcast; waiting for confirmations
confirmedConfirmed on-chain; fee settled (terminal)
canceledCanceled before broadcast; hold released (terminal)
rejectedRejected by review; hold released (terminal)
failedBroadcast/processing failed; hold released (terminal)

Swap statuses

StatusMeaning
processingAccepted; the sold amount is held; queued for execution
sendingProvider order created; our deposit to the provider is being broadcast
exchangingDeposit sent; waiting for the provider to settle
completedOutput credited to your balance, net of the platform fee (terminal)
failedCould not execute; hold released (terminal)
refundedProvider 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.

  1. Your customer reaches checkout with a total, say 49.90 USD.
  2. Call POST /payment/invoice with amount, currency: "USD" (or EUR / GBP, or a crypto), your order_id, a callback_url and a return_url. Send an Idempotency-Key so a double submit cannot create two invoices.
  3. Store the returned track_id against the order and redirect the customer to payment_url.
  4. 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.
  5. When funds confirm you receive invoice.paid at your callback_url. Verify the HMAC signature, look the order up by track_id (or order_id), mark it paid, and answer 2xx.
  6. The customer is sent to your return_url. Treat that page as informational only — the webhook is the source of truth.
  7. If they pay too little, you get invoice.paying while the page asks for the remainder, and invoice.underpaid if 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.

  1. Let the customer pick the coin and network in your UI. GET /payment/accepted-currencies lists what your account accepts; GET /common/currencies carries networks, minimums and fees.
  2. Call POST /payment/white-label with the fiat or crypto amount, pay_currency and network. You get the deposit address, exact pay_amount, a qr_code and expired_at.
  3. Render the address, amount and a countdown to expired_at. Show the amount as a copyable string; never round it.
  4. Show live progress by polling GET /payment/{track_id} every few seconds — txs lists detected transactions with confirmations, and status moves waiting → paying → paid.
  5. Fulfil on the invoice.paid webhook, exactly as in recipe 1.

3. Wallet top-ups with static addresses

For platforms where users hold a balance: exchanges, games, marketplaces, investment sites.

  1. When a user first opens their deposit screen for a coin, call POST /payment/static-address with the network (and optionally currency to restrict it) and a callback_url. Save the track_id and address on the user; it is permanent.
  2. Show the address and QR code. The user can deposit any number of times, with no invoice and no expiry.
  3. Each confirmed deposit fires static_address.deposit with amount, credited_amount, fee and tx_hash. Credit the user's balance with credited_amount, keyed on tx_hash so a redelivered webhook cannot credit twice.
  4. 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.
  5. 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's conversion field tracks the swap.
  6. Withdrawals are payouts: POST /payout with the user's address and amount using a Payout-scope key, then settle the user's balance on payout.confirmed (or restore it on payout.failed). Quote the fee first with POST /payout/estimate-fee if you show it to the user.
  7. If a user closes their account, POST /payment/static-address/revoke stops 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.