API reference
Balances and payouts
Balances, withdrawals to an external address, exchange between your own balances, statistics and rewards.
Per-currency balances
/api/user/balancesConversion is opt-in. With neither convert_currency nor
convert_currency_from_settings, the keys converted, percent,
converted_currency and total_balance are absent entirely — conversion costs a
rate lookup and can fail on a stale rate, and a caller who only wants raw numbers
should pay for neither.
actual is withdrawable money only — exactly what withdraw and exchange
will accept. credit is fee-only money that can never be withdrawn. The two never
overlap, and total_balance excludes credit.
A stale rate on an individual holding yields "converted": null for that
currency. Only a stale target rate fails the request.
Without a currency filter, currencies where both numbers are zero are omitted.
Parameters
user_iduidRequiredMust be your own uid. Another seller's is
403 not_permitted.currencycomma-separatedA filter, not a conversion target: comma-separated codes naming which currencies to return. Named currencies the seller holds nothing of come back as zero. An unknown code is a 400, never a silent drop.
convert_currencycurrency codeConvert every balance into this currency. Wins over
convert_currency_from_settings.convert_currency_from_settingsboolean flagConvert into the seller's own settings currency.
01truefalseyesnoonoff
Returns
200 OK. Per-currency balances. Conversion keys appear only when conversion was requested.
3 attributes
converted_currencystringPresent only when converting.
total_balancedecimal stringSum of
converted. Excludes credit. Present only when converting.balancesmap-
4 child attributes
actualdecimal stringWithdrawable money — exactly what withdraw and exchange accept.
creditdecimal stringFee-only funds. Never withdrawable, never counted in
total_balance.convertedstring or nullactualin the target currency. Present only when converting; null for an unpriceable currency.percentintegerShare of
total_balance, rounded to an integer. Present only when converting.
Errors
currency_not_found400The currency code does not exist, or has no usable rate.
rate_unavailable503A price feed is stale, so the request is refused rather than priced off an old rate. Retry shortly.
curl https://anonpay.org/api/user/balances \
-H "Authorization: Bearer $APA_KEY" \
-d user_id=4b8e2d0f6a1c3e5b7d9f0a2c4e6b8d0f \
-d currency=btc,xmrimport os
import requests
KEY = os.environ["APA_KEY"]
response = requests.post(
"https://anonpay.org/api/user/balances",
headers={"Authorization": f"Bearer {KEY}"},
data={
"user_id": "4b8e2d0f6a1c3e5b7d9f0a2c4e6b8d0f",
"currency": "btc,xmr",
},
timeout=30,
)
print(response.json())const url = "https://anonpay.org/api/user/balances";
const response = await fetch(url, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.APA_KEY}` },
body: new URLSearchParams({
user_id: "4b8e2d0f6a1c3e5b7d9f0a2c4e6b8d0f",
currency: "btc,xmr",
}),
});
console.log(await response.json());$url = 'https://anonpay.org/api/user/balances';
$key = getenv("APA_KEY");
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Authorization: Bearer $key"],
CURLOPT_POSTFIELDS => http_build_query([
"user_id" => '4b8e2d0f6a1c3e5b7d9f0a2c4e6b8d0f',
"currency" => 'btc,xmr',
]),
CURLOPT_RETURNTRANSFER => true,
]);
$response = json_decode(curl_exec($ch), true);
{
"status": "success",
"balances": {
"btc": {
"actual": "0.5",
"credit": "0"
},
"xmr": {
"actual": "0",
"credit": "0"
}
}
}
{
"status": "success",
"converted_currency": "usd",
"total_balance": "10",
"balances": {
"usdt-erc20": {
"actual": "10",
"credit": "3",
"converted": "10",
"percent": 100
}
}
}
Withdraw to an external address
/api/user/withdrawDebits the balance and queues a payout. The debit, the ledger entry and the withdrawal record commit together or not at all, under a row lock on the balance.
type |
amount means |
Charged | Recipient receives |
|---|---|---|---|
standard |
the amount to debit | amount |
amount − withdrawal_fee |
payment |
what the recipient must receive | amount + network_fee + withdrawal_fee |
amount |
withdrawal_fee comes from the seller's plan. network_fee is a fixed absolute
amount converted into the withdrawal currency — 1 USD for xmr, 4 USD otherwise.
The minimum (0.1 USD) is checked against amount_charged, not amount.
private_key in the response is the webhook signing secret for this withdrawal.
Send an idempotency_key.
Parameters
user_iduidRequiredMust be your own uid. Another seller's is
403 not_permitted.currencycurrency codeRequiredMust be a crypto currency.
amountdecimal stringRequiredFor
type=standard, the amount to debit. Fortype=payment, the amount the recipient must receive.recipient_addressstringRequiredChecksum-validated for this exact currency — Base58Check, bech32/bech32m, CashAddr, EIP-55, Monero base58, Solana or Tron as appropriate. A cross-chain address is rejected.
Up to 128 characters.typestringWho absorbs the fees. Defaults to
standard.standardpaymentexcluded_input_addressstringAn address the node must not spend from. Validated like
Up to 128 characters.recipient_address.required_transaction_iduidLinks the payout to one of your transactions. Must exist and belong to you, or the call is
404 uid_not_found.webhookURLReceives
Up to 2,048 characters.withdrawal.completed. Public hosts only.idempotency_keystringMakes a retry safe. Scoped to (your key, method, path, sorted parameters, this value). Honoured on POST/PUT/PATCH/DELETE only; ignored on GET. Server errors and 429s are never cached.
Returns
201 Created. Withdrawal accepted and queued for payout.
8 attributes
withdrawal_iduidprivate_keyuidWebhook signing secret for this withdrawal.
amount_chargeddecimal stringamount_sentdecimal stringfeedecimal stringWhat you were charged, in the withdrawal currency — so
amount_charged,amount_sentandfeestill add up for a client that reads only those three.fee_currencystringWhich balance the fee actually came out of. Your
fee_payment_methodcan put it somewhere other than the withdrawal currency, so read this rather than assuming.fee_charged_separatelybooleanTrue when the fee came from a different balance.
feethen covers the network fee alone, and the two keys around it say where the rest went.fee_amount_in_fee_currencydecimal stringThe fee, converted into
fee_currency.
Errors
parameter_invalid400A parameter is present but fails validation.
currency_not_found400The currency code does not exist, or has no usable rate.
invalid_recipient_address400The address fails the format or checksum of that currency. A cross-chain address is rejected.
uid_not_found404No transaction or withdrawal with that id.
request_in_progress409A request with the same
idempotency_keyis still running.withdraw_amount_must_be_over_{limit}_{currency}422The withdrawal is below the minimum. Match on the prefix.
amount_out_of_range422The fees would consume the whole amount.
payouts_disabled_for_method422Payouts in that currency are switched off for now. Your balance is unaffected.
user_balance_low422Your withdrawable balance is too low.
idempotency_key_reused_with_different_parameters422That
idempotency_keywas already used for a request with different parameters.rate_unavailable503A price feed is stale, so the request is refused rather than priced off an old rate. Retry shortly.
curl https://anonpay.org/api/user/withdraw \
-H "Authorization: Bearer $APA_KEY" \
-d user_id=4b8e2d0f6a1c3e5b7d9f0a2c4e6b8d0f \
-d currency=btc \
-d amount=0.0153 \
-d recipient_address=bc1q9h7garjf3p8rk5tjz7w8a4v3gmn2zd7xkq8fy6 \
-d idempotency_key=2c9a4e7b-8d1f-4a6cimport os
import requests
KEY = os.environ["APA_KEY"]
response = requests.post(
"https://anonpay.org/api/user/withdraw",
headers={"Authorization": f"Bearer {KEY}"},
data={
"user_id": "4b8e2d0f6a1c3e5b7d9f0a2c4e6b8d0f",
"currency": "btc",
"amount": "0.0153",
"recipient_address": "bc1q9h7garjf3p8rk5tjz7w8a4v3gmn2zd7xkq8fy6",
"idempotency_key": "2c9a4e7b-8d1f-4a6c",
},
timeout=30,
)
print(response.json())const url = "https://anonpay.org/api/user/withdraw";
const response = await fetch(url, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.APA_KEY}` },
body: new URLSearchParams({
user_id: "4b8e2d0f6a1c3e5b7d9f0a2c4e6b8d0f",
currency: "btc",
amount: "0.0153",
recipient_address: "bc1q9h7garjf3p8rk5tjz7w8a4v3gmn2zd7xkq8fy6",
idempotency_key: "2c9a4e7b-8d1f-4a6c",
}),
});
console.log(await response.json());$url = 'https://anonpay.org/api/user/withdraw';
$key = getenv("APA_KEY");
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Authorization: Bearer $key"],
CURLOPT_POSTFIELDS => http_build_query([
"user_id" => '4b8e2d0f6a1c3e5b7d9f0a2c4e6b8d0f',
"currency" => 'btc',
"amount" => '0.0153',
"recipient_address" => 'bc1q9h7garjf3p8rk5tjz7w8a4v3gmn2zd7xkq8fy6',
"idempotency_key" => '2c9a4e7b-8d1f-4a6c',
]),
CURLOPT_RETURNTRANSFER => true,
]);
$response = json_decode(curl_exec($ch), true);
{
"status": "success",
"withdrawal_id": "7b9d0f2a4c6e8b0d2f4a6c8e0b2d4f6a",
"private_key": "e5d4c3b2a1908f7e6d5c4b3a29180f7e",
"amount_charged": "0.0153",
"amount_sent": "0.015258",
"fee": "0.000042",
"fee_currency": "btc",
"fee_charged_separately": false,
"fee_amount_in_fee_currency": "0.000042"
}
Exchange between your own balances
/api/user/exchangeBoth legs — debit and credit — commit in one locked transaction.
Supplying both amounts or neither is 400 parameter_invalid with
reason: supply_exactly_one_of_currency_from_amount_or_currency_to_amount.
The fee is exchange_fee_percent from the seller's plan, and it lands on whichever
side was not fixed: fixing currency_from_amount means you receive
amount × rate − fee (fee denominated in currency_to); fixing
currency_to_amount means you are charged amount ÷ rate + fee (fee denominated
in currency_from).
Exchange spends withdrawable balance only — credit funds are excluded.
Parameters
user_iduidRequiredMust be your own uid. Another seller's is
403 not_permitted.currency_fromcurrency codeRequiredcurrency_tocurrency codeRequiredMust differ from
currency_from.currency_from_amountdecimal string"Spend this much". Supply exactly one of this and
currency_to_amount.currency_to_amountdecimal string"Receive this much". Supply exactly one of this and
currency_from_amount.simulatestringStrictly the string
1— price the exchange without executing it. Returns 200 and noexchange_id.1idempotency_keystringMakes a retry safe. Scoped to (your key, method, path, sorted parameters, this value). Honoured on POST/PUT/PATCH/DELETE only; ignored on GET. Server errors and 429s are never cached.
Returns
200 OK. Priced but not executed (simulate=1). No exchange_id.
201 Created. Exchange executed. Both legs committed in one transaction.
7 attributes
ratedecimal stringcurrency_fromstringcurrency_from_amountdecimal stringcurrency_tostringcurrency_to_amountdecimal stringfeedecimal stringDenominated in
currency_towhencurrency_from_amountwas fixed, incurrency_fromotherwise.fee_currencystringWhich of the two currencies
feeis denominated in.
Errors
parameter_invalid400A parameter is present but fails validation.
currency_not_found400The currency code does not exist, or has no usable rate.
request_in_progress409A request with the same
idempotency_keyis still running.exchange_amount_must_be_over_{limit}_{currency}422The exchange is below the minimum. Match on the prefix.
amount_out_of_range422The fees would consume the whole amount.
user_balance_low422Your withdrawable balance is too low.
idempotency_key_reused_with_different_parameters422That
idempotency_keywas already used for a request with different parameters.rate_unavailable503A price feed is stale, so the request is refused rather than priced off an old rate. Retry shortly.
curl https://anonpay.org/api/user/exchange \
-H "Authorization: Bearer $APA_KEY" \
-d user_id=4b8e2d0f6a1c3e5b7d9f0a2c4e6b8d0f \
-d currency_from=btc \
-d currency_to=usdt-erc20 \
-d currency_from_amount=0.01 \
-d idempotency_key=2c9a4e7b-8d1f-4a6cimport os
import requests
KEY = os.environ["APA_KEY"]
response = requests.post(
"https://anonpay.org/api/user/exchange",
headers={"Authorization": f"Bearer {KEY}"},
data={
"user_id": "4b8e2d0f6a1c3e5b7d9f0a2c4e6b8d0f",
"currency_from": "btc",
"currency_to": "usdt-erc20",
"currency_from_amount": "0.01",
"idempotency_key": "2c9a4e7b-8d1f-4a6c",
},
timeout=30,
)
print(response.json())const url = "https://anonpay.org/api/user/exchange";
const response = await fetch(url, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.APA_KEY}` },
body: new URLSearchParams({
user_id: "4b8e2d0f6a1c3e5b7d9f0a2c4e6b8d0f",
currency_from: "btc",
currency_to: "usdt-erc20",
currency_from_amount: "0.01",
idempotency_key: "2c9a4e7b-8d1f-4a6c",
}),
});
console.log(await response.json());$url = 'https://anonpay.org/api/user/exchange';
$key = getenv("APA_KEY");
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Authorization: Bearer $key"],
CURLOPT_POSTFIELDS => http_build_query([
"user_id" => '4b8e2d0f6a1c3e5b7d9f0a2c4e6b8d0f',
"currency_from" => 'btc',
"currency_to" => 'usdt-erc20',
"currency_from_amount" => '0.01',
"idempotency_key" => '2c9a4e7b-8d1f-4a6c',
]),
CURLOPT_RETURNTRANSFER => true,
]);
$response = json_decode(curl_exec($ch), true);
{
"status": "success",
"rate": "95210.4",
"currency_from": "btc",
"currency_from_amount": "0.01",
"currency_to": "usdt-erc20",
"currency_to_amount": "942.58",
"fee": "9.52",
"fee_currency": "usdt-erc20"
}
{
"status": "success",
"exchange_id": "3b5d7f9a1c3e5b7d9f1a3c5e7b9d1f3a",
"rate": "95210.4",
"currency_from": "btc",
"currency_from_amount": "0.01",
"currency_to": "usdt-erc20",
"currency_to_amount": "942.58",
"fee": "9.52",
"fee_currency": "usdt-erc20"
}
Revenue and conversion statistics
/api/user/statsCurrent window versus the one immediately before it. revenue is denominated in
the seller's settings currency.
A currency with no usable rate is omitted from revenue but still counted in
transaction_count and completion_rate — this endpoint does not fail on a stale
rate.
Parameters
user_iduidRequiredMust be your own uid. Another seller's is
403 not_permitted.timeinteger stringWindow in seconds. Default 86400. The comparison window is the preceding equal-length period.
Returns
200 OK. Window statistics versus the preceding window.
7 attributes
currencystringThe seller's settings currency — the denomination of
revenue.revenuedecimal stringrevenue_changeintegerPercentage change vs. the previous window. 0 when the previous value was zero.
transaction_countintegertransaction_count_changeintegercompletion_ratenumberCompleted ÷ created, as a percentage, 2 decimals.
completion_rate_changeinteger
Errors
parameter_invalid400A parameter is present but fails validation.
curl https://anonpay.org/api/user/stats \
-H "Authorization: Bearer $APA_KEY" \
-d user_id=4b8e2d0f6a1c3e5b7d9f0a2c4e6b8d0fimport os
import requests
KEY = os.environ["APA_KEY"]
response = requests.post(
"https://anonpay.org/api/user/stats",
headers={"Authorization": f"Bearer {KEY}"},
data={
"user_id": "4b8e2d0f6a1c3e5b7d9f0a2c4e6b8d0f",
},
timeout=30,
)
print(response.json())const url = "https://anonpay.org/api/user/stats";
const response = await fetch(url, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.APA_KEY}` },
body: new URLSearchParams({
user_id: "4b8e2d0f6a1c3e5b7d9f0a2c4e6b8d0f",
}),
});
console.log(await response.json());$url = 'https://anonpay.org/api/user/stats';
$key = getenv("APA_KEY");
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Authorization: Bearer $key"],
CURLOPT_POSTFIELDS => http_build_query([
"user_id" => '4b8e2d0f6a1c3e5b7d9f0a2c4e6b8d0f',
]),
CURLOPT_RETURNTRANSFER => true,
]);
$response = json_decode(curl_exec($ch), true);
{
"status": "success",
"currency": "usd",
"revenue": "1240.5",
"revenue_change": 12,
"transaction_count": 48,
"transaction_count_change": -4,
"completion_rate": 81.25,
"completion_rate_change": 3
}
Claim a signup reward
/api/user/rewards/claimPayouts are credited as credit funds — fee-only, never withdrawable — and
recorded in rewards on the profile.
reward_id |
Pays | Requirement | Enabled |
|---|---|---|---|
transactioncreate |
1 USDT-ERC20 | ≥ 1 non-deposit transaction exists | yes |
transactioncomplete |
10 USDT-ERC20 | ≥ 50 USD completed volume since signup | yes |
deposit100 |
10 USDT-ERC20 | withdrawable balance ≥ 100 USD | yes |
deposit500 |
20 USDT-ERC20 | withdrawable balance ≥ 500 USD | yes |
exchange100 |
12 USDT-ERC20 | — | no |
exchange500 |
24 USDT-ERC20 | — | no |
Every reward expires 432000 s (5 days) after account creation. A disabled reward
is 400 parameter_invalid, not requirements_not_met.
The claim is one conditional UPDATE guarded on the reward key being absent, so it cannot be raced into a double payout.
Parameters
user_iduidRequiredMust be your own uid. Another seller's is
403 not_permitted.reward_idstringRequiredtransactioncreatetransactioncompletedeposit100deposit500exchange100exchange500
Returns
200 OK. Success.
Errors
parameter_invalid400A parameter is present but fails validation.
already_claimed409That reward was already claimed.
expired422The reward's claim window has closed.
requirements_not_met422The reward's requirements are not met yet.
curl https://anonpay.org/api/user/rewards/claim \
-H "Authorization: Bearer $APA_KEY" \
-d user_id=4b8e2d0f6a1c3e5b7d9f0a2c4e6b8d0f \
-d reward_id=transactioncreateimport os
import requests
KEY = os.environ["APA_KEY"]
response = requests.post(
"https://anonpay.org/api/user/rewards/claim",
headers={"Authorization": f"Bearer {KEY}"},
data={
"user_id": "4b8e2d0f6a1c3e5b7d9f0a2c4e6b8d0f",
"reward_id": "transactioncreate",
},
timeout=30,
)
print(response.json())const url = "https://anonpay.org/api/user/rewards/claim";
const response = await fetch(url, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.APA_KEY}` },
body: new URLSearchParams({
user_id: "4b8e2d0f6a1c3e5b7d9f0a2c4e6b8d0f",
reward_id: "transactioncreate",
}),
});
console.log(await response.json());$url = 'https://anonpay.org/api/user/rewards/claim';
$key = getenv("APA_KEY");
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Authorization: Bearer $key"],
CURLOPT_POSTFIELDS => http_build_query([
"user_id" => '4b8e2d0f6a1c3e5b7d9f0a2c4e6b8d0f',
"reward_id" => 'transactioncreate',
]),
CURLOPT_RETURNTRANSFER => true,
]);
$response = json_decode(curl_exec($ch), true);
{
"status": "success"
}