API reference

Balances and payouts

Balances, withdrawals to an external address, exchange between your own balances, statistics and rewards.

Per-currency balances

POST/api/user/balances
API key200 OK

Conversion 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_iduidRequired

Must be your own uid. Another seller's is 403 not_permitted.

currencycomma-separated

A 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 code

Convert every balance into this currency. Wins over convert_currency_from_settings.

convert_currency_from_settingsboolean flag

Convert 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_currencystring

Present only when converting.

total_balancedecimal string

Sum of converted. Excludes credit. Present only when converting.

balancesmap
4 child attributes
actualdecimal string

Withdrawable money — exactly what withdraw and exchange accept.

creditdecimal string

Fee-only funds. Never withdrawable, never counted in total_balance.

convertedstring or null

actual in the target currency. Present only when converting; null for an unpriceable currency.

percentinteger

Share of total_balance, rounded to an integer. Present only when converting.

Errors

currency_not_found400

The currency code does not exist, or has no usable rate.

rate_unavailable503

A price feed is stale, so the request is refused rather than priced off an old rate. Retry shortly.

Request
curl https://anonpay.org/api/user/balances \
  -H "Authorization: Bearer $APA_KEY" \
  -d user_id=4b8e2d0f6a1c3e5b7d9f0a2c4e6b8d0f \
  -d currency=btc,xmr
import 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);
Response, filtered, unconverted200 OK
{
  "status": "success",
  "balances": {
    "btc": {
      "actual": "0.5",
      "credit": "0"
    },
    "xmr": {
      "actual": "0",
      "credit": "0"
    }
  }
}
Response, converted to usd200 OK
{
  "status": "success",
  "converted_currency": "usd",
  "total_balance": "10",
  "balances": {
    "usdt-erc20": {
      "actual": "10",
      "credit": "3",
      "converted": "10",
      "percent": 100
    }
  }
}

Withdraw to an external address

POST/api/user/withdraw
API keyIdempotent201 Created

Debits 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_iduidRequired

Must be your own uid. Another seller's is 403 not_permitted.

currencycurrency codeRequired

Must be a crypto currency.

amountdecimal stringRequired

For type=standard, the amount to debit. For type=payment, the amount the recipient must receive.

recipient_addressstringRequired

Checksum-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.
typestring

Who absorbs the fees. Defaults to standard.

standardpayment
excluded_input_addressstring

An address the node must not spend from. Validated like recipient_address.

Up to 128 characters.
required_transaction_iduid

Links the payout to one of your transactions. Must exist and belong to you, or the call is 404 uid_not_found.

webhookURL

Receives withdrawal.completed. Public hosts only.

Up to 2,048 characters.
idempotency_keystring

Makes 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_iduid
private_keyuid

Webhook signing secret for this withdrawal.

amount_chargeddecimal string
amount_sentdecimal string
feedecimal string

What you were charged, in the withdrawal currency — so amount_charged, amount_sent and fee still add up for a client that reads only those three.

fee_currencystring

Which balance the fee actually came out of. Your fee_payment_method can put it somewhere other than the withdrawal currency, so read this rather than assuming.

fee_charged_separatelyboolean

True when the fee came from a different balance. fee then covers the network fee alone, and the two keys around it say where the rest went.

fee_amount_in_fee_currencydecimal string

The fee, converted into fee_currency.

Errors

parameter_invalid400

A parameter is present but fails validation.

currency_not_found400

The currency code does not exist, or has no usable rate.

invalid_recipient_address400

The address fails the format or checksum of that currency. A cross-chain address is rejected.

uid_not_found404

No transaction or withdrawal with that id.

request_in_progress409

A request with the same idempotency_key is still running.

withdraw_amount_must_be_over_{limit}_{currency}422

The withdrawal is below the minimum. Match on the prefix.

amount_out_of_range422

The fees would consume the whole amount.

payouts_disabled_for_method422

Payouts in that currency are switched off for now. Your balance is unaffected.

user_balance_low422

Your withdrawable balance is too low.

idempotency_key_reused_with_different_parameters422

That idempotency_key was already used for a request with different parameters.

rate_unavailable503

A price feed is stale, so the request is refused rather than priced off an old rate. Retry shortly.

Request
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-4a6c
import 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);
Response201 Created
{
  "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

POST/api/user/exchange
API keyIdempotent200 OK

Both 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_iduidRequired

Must be your own uid. Another seller's is 403 not_permitted.

currency_fromcurrency codeRequired
currency_tocurrency codeRequired

Must 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.

simulatestring

Strictly the string 1 — price the exchange without executing it. Returns 200 and no exchange_id.

1
idempotency_keystring

Makes 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 string
currency_fromstring
currency_from_amountdecimal string
currency_tostring
currency_to_amountdecimal string
feedecimal string

Denominated in currency_to when currency_from_amount was fixed, in currency_from otherwise.

fee_currencystring

Which of the two currencies fee is denominated in.

Errors

parameter_invalid400

A parameter is present but fails validation.

currency_not_found400

The currency code does not exist, or has no usable rate.

request_in_progress409

A request with the same idempotency_key is still running.

exchange_amount_must_be_over_{limit}_{currency}422

The exchange is below the minimum. Match on the prefix.

amount_out_of_range422

The fees would consume the whole amount.

user_balance_low422

Your withdrawable balance is too low.

idempotency_key_reused_with_different_parameters422

That idempotency_key was already used for a request with different parameters.

rate_unavailable503

A price feed is stale, so the request is refused rather than priced off an old rate. Retry shortly.

Request
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-4a6c
import 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);
Response200 OK
{
  "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"
}
Response201 Created
{
  "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

POST/api/user/stats
API key200 OK

Current 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_iduidRequired

Must be your own uid. Another seller's is 403 not_permitted.

timeinteger string

Window in seconds. Default 86400. The comparison window is the preceding equal-length period.

Returns

200 OK. Window statistics versus the preceding window.

7 attributes
currencystring

The seller's settings currency — the denomination of revenue.

revenuedecimal string
revenue_changeinteger

Percentage change vs. the previous window. 0 when the previous value was zero.

transaction_countinteger
transaction_count_changeinteger
completion_ratenumber

Completed ÷ created, as a percentage, 2 decimals.

completion_rate_changeinteger

Errors

parameter_invalid400

A parameter is present but fails validation.

Request
curl https://anonpay.org/api/user/stats \
  -H "Authorization: Bearer $APA_KEY" \
  -d user_id=4b8e2d0f6a1c3e5b7d9f0a2c4e6b8d0f
import 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);
Response200 OK
{
  "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

POST/api/user/rewards/claim
API key200 OK

Payouts 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_iduidRequired

Must be your own uid. Another seller's is 403 not_permitted.

reward_idstringRequired
transactioncreatetransactioncompletedeposit100deposit500exchange100exchange500

Returns

200 OK. Success.

Errors

parameter_invalid400

A parameter is present but fails validation.

already_claimed409

That reward was already claimed.

expired422

The reward's claim window has closed.

requirements_not_met422

The reward's requirements are not met yet.

Request
curl https://anonpay.org/api/user/rewards/claim \
  -H "Authorization: Bearer $APA_KEY" \
  -d user_id=4b8e2d0f6a1c3e5b7d9f0a2c4e6b8d0f \
  -d reward_id=transactioncreate
import 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);
Response200 OK
{
  "status": "success"
}