Paid.my Merchant API
Accept crypto payments with hosted checkout, invoices, static deposit addresses and automated payouts. 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.
| 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 |
| 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"
}| error.type | HTTP | Meaning |
|---|---|---|
validation_error | 400 | Bad input; key names the field |
auth_error | 401 | Missing/invalid key or wrong scope |
forbidden | 403 | Key or feature not permitted |
not_found | 404 | Unknown track_id / address |
conflict | 409 | Duplicate order_id or idempotency mismatch |
rate_limited | 429 | Too many requests — see Retry-After |
internal_error | 500 | Something went wrong on our side |
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": "payer@example.com",
"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",
"TRX",
"LTC",
"SOL",
"BNB",
"POL",
"GRAM"
]
},
"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. |
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), network (unavailable pair), 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. |
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",
"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, or payout 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.
{
"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": "payer@example.com",
"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
}payout.confirmed / payout.failed
For payout.failed, status is Failed 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; invoice.underpaid (status: "Underpaid") when the invoice expires with a partial amount — those funds are not credited automatically.
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 | 2% (200 bps) | Charged per credited deposit |
| 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. |
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 (terminal) |
expired | Lifetime elapsed with no payment (terminal) |
refunding | Refund in progress |
refunded | Refund completed (terminal) |
failed | Processing failure (terminal) |
Webhook payloads capitalize the status (Paid, Paying, Underpaid).
Payout statuses
| Status | Meaning |
|---|---|
processing | Accepted; funds held; queued for broadcast |
pending | Transaction being prepared/broadcast |
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) |