JavaScript is turned off. Use AnonChange Lite, the full exchange without JavaScript.

API documentation

Offer private swaps between Bitcoin, Ethereum, Monero, Litecoin, Solana and stablecoins in your wallet, aggregator or site. Same rates, limits and guarantees as AnonChange itself, no KYC for your customers.

Download for LLMs (.md)OpenAPIPostmanThe whole guide as one Markdown file to give ChatGPT, Claude or your coding assistant.

Overview

Base URL: https://anonchange.com/api/partner/v1

  • Every endpoint is a POST with a JSON body and Content-Type: application/json. Send {} when an endpoint has no fields. Unknown fields are rejected.
  • Every request is signed with your API secret (see Authentication).
  • Success responses are plain JSON. Errors are { "error": "message" } with an HTTP status code.
  • Amounts are decimal strings ("0.01"), never numbers, in whole coins. Percentages and counts are numbers. Times are ISO 8601 in UTC.
  • Coins are identified by their code from currencies, for example BTC, XMR or USDT_SOL (USDT on Solana).
StatusMeaning
400Invalid input: unknown or missing field, bad amount or address, pair not available, body not JSON.
401Missing or invalid authentication headers, expired timestamp, reused nonce or wrong signature.
403Your partner account is disabled.
404Order not found (or not one of yours), or no such endpoint (every endpoint is a POST).
409The order is not in a state that allows this action, or it has to wait a moment.
413Body larger than 16 KB.
415Content-Type is not application/json.
429Rate limit reached. Wait for the Retry-After header's seconds and retry.
500Our error. Retry later; for create, retry with the same Idempotency-Key.
503Temporarily unavailable: no fresh rates, not enough reserve, exchange paused or too many unpaid orders.

Retry 429 and 503 later with a new nonce and timestamp. Rarely, a 429 or 503 comes from our proxy as plain text instead of JSON.

Getting started

  • Create a partner account (or log in to an existing one). It is self-service and needs only a password: your account ID is your username. Store both safely, a lost password cannot be reset.
  • You get an API key and an API secret. The secret is shown once; you can replace it with a new one in your dashboard at any time (the old one then stops working).
  • Call currencies and price to test your signature, then create orders and poll their status.

Authentication

Every request carries four headers:

HeaderValue
X-API-KEYYour API key.
X-API-TIMESTAMPCurrent unix time in seconds. Must be within 60 seconds of our clock.
X-API-NONCEA random string of 16-64 characters (A-Z, a-z, 0-9, - and _). Each nonce works once within 120 seconds.
X-API-SIGNLowercase hex HMAC-SHA256 of the signed string, keyed with your API secret.
Signed string
${timestamp}\n${nonce}\nPOST\n${path}\n${idempotencyKey}\n${sha256hex(body)}
  • \n is a single newline character, with no newline at the end.
  • nonce is the exact X-API-NONCE header. idempotencyKey is the exact Idempotency-Key header, or an empty string when absent. Keep that empty line in the signature. Generate a new nonce and signature for retries, keeping the same idempotency key.
  • path is the request path including /api/partner/v1, for example /api/partner/v1/price: exactly the path you request. Send no query string and no trailing slash. The method is always POST.
  • sha256hex(body) is the lowercase hex SHA-256 of the exact body bytes you send. Serialize the JSON once and send that same string: re-serializing can change spacing or key order and break the signature.

A complete signed request (the Node.js and Python versions define a call helper used below):

API_KEY="ak_your_api_key"
API_SECRET="your_api_secret"
REQ_PATH="/api/partner/v1/price"
BODY='{"from":"BTC","to":"XMR","rateType":"float","amount":"0.01"}'

TS=$(date +%s)
NONCE=$(openssl rand -hex 16)
IDEMPOTENCY_KEY=""
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | sed 's/^.* //')
SIGN=$(printf '%s\n%s\nPOST\n%s\n%s\n%s' "$TS" "$NONCE" "$REQ_PATH" "$IDEMPOTENCY_KEY" "$BODY_HASH" \
  | openssl dgst -sha256 -hmac "$API_SECRET" -hex | sed 's/^.* //')

curl -sS -X POST "https://anonchange.com$REQ_PATH" \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: $API_KEY" \
  -H "X-API-TIMESTAMP: $TS" \
  -H "X-API-NONCE: $NONCE" \
  -H "X-API-SIGN: $SIGN" \
  --data-binary "$BODY"
Keep the secret on your server. Never put it in a browser, an app or a public repository; if it leaks, replace it in your dashboard.

OpenAPI and Postman

The whole API as an OpenAPI 3.1 spec (for code generators and API tools) and as a Postman collection.

  • Postman: import the collection, then set the collection variables apiKey and apiSecret. Its pre-request script signs every request, so they work as they are.
  • Generated clients from the OpenAPI spec still need to sign each request as described in Authentication.
Postman pre-request script (included in the collection)
const body = pm.request.body && pm.request.body.raw ? pm.variables.replaceIn(pm.request.body.raw) : "";
const timestamp = String(Math.floor(Date.now() / 1000));
const path = "/" + pm.request.url.path.join("/");
const nonce = CryptoJS.lib.WordArray.random(16).toString();
const idempotencyKey = pm.variables.replaceIn(pm.request.headers.get("Idempotency-Key") || "");
const signed = timestamp + "\n" + nonce + "\nPOST\n" + path + "\n" + idempotencyKey + "\n" + CryptoJS.SHA256(body).toString();
pm.request.body.update(body);
pm.request.headers.upsert({ key: "X-API-KEY", value: pm.variables.get("apiKey") });
pm.request.headers.upsert({ key: "X-API-TIMESTAMP", value: timestamp });
pm.request.headers.upsert({ key: "X-API-NONCE", value: nonce });
pm.request.headers.upsert({ key: "X-API-SIGN", value: CryptoJS.HmacSHA256(signed, pm.variables.get("apiSecret")).toString() });

Currencies

POST/api/partner/v1/currencies120/min

All coins with their limits. Body: {}. Only coins with enabled: true can be exchanged right now.

FieldTypeDescription
coin, namestringCoin code (used everywhere else) and display name.
decimalsnumberDecimals the coin's amounts can have.
enabledbooleanAvailable for new orders.
minDepositstringSmallest deposit we exchange. Smaller deposits are refunded.
confirmationsnumberConfirmations a deposit needs, as an order's requiredConfirmations shows them (blocks to finality on Ethereum and Solana).
networkFeestringNetwork fee taken from a payout in this coin.
usdstring | nullCurrent USD price, if known.
reservestringWhat we can pay out in this coin right now: the most an order into it can receive.
Response 200
[
  {
    "coin": "BTC",
    "name": "Bitcoin",
    "decimals": 8,
    "enabled": true,
    "minDeposit": "0.0002",
    "confirmations": 2,
    "networkFee": "0.00003",
    "usd": "67012.4",
    "reserve": "1.84213"
  },
  {
    "coin": "XMR",
    "name": "Monero",
    "decimals": 12,
    "enabled": true,
    "minDeposit": "0.02",
    "confirmations": 10,
    "networkFee": "0.0002",
    "usd": "163.21",
    "reserve": "412.5"
  }
]

Pairs

POST/api/partner/v1/pairs120/min

Every pair of enabled coins and the rate types it supports. Body: {}.

Response 200
[
  {
    "from": "BTC",
    "to": "XMR",
    "rateTypes": [
      "float",
      "fixed"
    ]
  },
  {
    "from": "XMR",
    "to": "BTC",
    "rateTypes": [
      "float",
      "fixed"
    ]
  }
]

Price

POST/api/partner/v1/price600/min

A quote for a pair. Nothing is reserved: create the order to get a deposit address.

FieldTypeDescription
fromstringCoin the customer sends.
tostringCoin the customer receives.
rateTypestringfloat or fixed.
amountstring, optionalAmount of the from coin the customer sends. Without it (and without amountOut), amountIn and amountOut are null.
amountOutstring, optionalQuote by the amount to receive instead: amountIn becomes the smallest deposit that receives at least this. Not together with amount.
markupPercentnumber, optionalYour markup for this request, 0 up to the cap shown in your dashboard, at most 2 decimals. Without it, your dashboard markup applies.
Request
{
  "from": "BTC",
  "to": "XMR",
  "rateType": "fixed",
  "amount": "0.01"
}
Response 200
{
  "from": "BTC",
  "to": "XMR",
  "rateType": "fixed",
  "amountIn": "0.01",
  "amountOut": "4.044423745",
  "rate": "410.6217",
  "feePercent": 1.5,
  "networkFee": "0.0002",
  "minDeposit": "0.0002",
  "maxDeposit": "0.45",
  "rateLockMinutes": 10,
  "maxRateDriftPercent": 1.2
}
  • amountOut is what the customer receives, after feePercent and networkFee. rate is our exchange rate before fees.
  • feePercent already includes your own markup (see Affiliate program), so show it as it is.
  • minDeposit and maxDeposit are the deposit limits for this pair right now; the maximum follows our reserve of the receiving coin.
  • rateLockMinutes (fixed only): how long a fixed rate stays locked after the order is created. maxRateDriftPercent: how far the market may move against the quote before the customer is asked (see Order lifecycle).

Create an order

POST/api/partner/v1/create60/min
FieldTypeDescription
from, to, rateTypestringAs for price.
amountstringRequired (or amountOut instead). At least minDeposit.
amountOutstring, optionalInstead of amount: the amount the customer should receive. The deposit amount is worked out from it, as in price.
markupPercentnumber, optionalYour markup for this order, as in price. Saved with the order.
webhookUrlstring, optionalhttps URL that gets a signed POST whenever the order's status changes (see Webhooks).
payoutAddressstringThe customer's address for the to coin.
refundAddressstring, optionalThe customer's address for the from coin. Strongly recommended: without it, refunds go through support.

Optional header Idempotency-Key: 8-128 characters (A-Z, a-z, 0-9, - and _), unique per order on your side. If a create times out, send it again with the same key: you get the order made the first time, with status 200 instead of 201. The retry's body is not compared, so use a new key for every new order.

Request
{
  "from": "BTC",
  "to": "XMR",
  "rateType": "fixed",
  "amount": "0.01",
  "payoutAddress": "<customer's XMR address>",
  "refundAddress": "<customer's BTC address>"
}
Response 201
{
  "id": "3f6c1a52-8d1e-4b7a-9c35-2f0e6b9d4a17",
  "status": "awaiting_deposit",
  "from": "BTC",
  "to": "XMR",
  "rateType": "fixed",
  "depositAddress": "<BTC deposit address>",
  "payoutAddress": "<customer's XMR address>",
  "refundAddress": "<customer's BTC address>",
  "quote": {
    "amountIn": "0.01",
    "amountOut": "4.044423745",
    "rate": "410.6217"
  },
  "amountIn": null,
  "amountOut": null,
  "rate": null,
  "feePercent": 1.5,
  "networkFee": "0.0002",
  "requiredConfirmations": 2,
  "deposits": [],
  "payout": null,
  "pendingDeposit": null,
  "continuing": false,
  "actions": {
    "continue": false,
    "refund": false
  },
  "refund": null,
  "note": null,
  "guarantee": "-----BEGIN PGP SIGNED MESSAGE-----\n…\n-----END PGP SIGNATURE-----",
  "rateLockedUntil": "2026-10-19T12:10:00.000Z",
  "expiresAt": "2026-10-19T12:10:00.000Z",
  "createdAt": "2026-10-19T12:00:00.000Z",
  "completedAt": null,
  "viewKey": "<view key>",
  "url": "https://anonchange.com/order/3f6c1a52-8d1e-4b7a-9c35-2f0e6b9d4a17#key=<view key>"
}
  • The response is the order (fields under Order status) plus viewKey and url.
  • url is the customer's order page. The key after #key= stays in the browser and lets it show the order's addresses and transactions; without the key these are hidden after 36 hours. Give the link to your customer or build your own status page.
  • viewKey and url are returned only on the first 201 response. A retry with the same Idempotency-Key returns the order without them, so store them right away.
  • Show the customer depositAddress, the amount to send and expiresAt. Only the from coin on its own network is credited.
API_KEY="ak_your_api_key"
API_SECRET="your_api_secret"
REQ_PATH="/api/partner/v1/create"
BODY='{"from":"BTC","to":"XMR","rateType":"fixed","amount":"0.01","payoutAddress":"<customer XMR address>","refundAddress":"<customer BTC address>"}'

TS=$(date +%s)
NONCE=$(openssl rand -hex 16)
IDEMPOTENCY_KEY="checkout-1234"
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | sed 's/^.* //')
SIGN=$(printf '%s\n%s\nPOST\n%s\n%s\n%s' "$TS" "$NONCE" "$REQ_PATH" "$IDEMPOTENCY_KEY" "$BODY_HASH" \
  | openssl dgst -sha256 -hmac "$API_SECRET" -hex | sed 's/^.* //')

curl -sS -X POST "https://anonchange.com$REQ_PATH" \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: $API_KEY" \
  -H "X-API-TIMESTAMP: $TS" \
  -H "X-API-NONCE: $NONCE" \
  -H "X-API-SIGN: $SIGN" \
  -H "Idempotency-Key: checkout-1234" \
  --data-binary "$BODY"

Order status

POST/api/partner/v1/status600/min

The current state of one of your orders. Body: { "id": "3f6c1a52-8d1e-4b7a-9c35-2f0e6b9d4a17" }. You always get the full order, with addresses, transaction IDs and the letter of guarantee.

FieldDescription
idOrder ID (UUID). Use it with status, continue and refund.
statusCurrent state, see the order lifecycle below.
from, to, rateTypeAs given at creation.
depositAddressWhere the customer sends the coins. Show it while the status is awaiting_deposit.
payoutAddress, refundAddressAs given at creation (refundAddress may be null).
quoteThe quoted amountIn, amountOut and rate at creation.
amountIn, amountOut, rateWhat was actually received, paid out and applied. Null until the deposit is credited.
feePercent, networkFeeTotal fee in percent (your markup included) and the network fee taken from the payout.
requiredConfirmationsConfirmations a deposit needs before it is credited.
depositsDeposits counted for this order: txid, amount, confirmations, credited, seenAt.
pendingDepositA deposit seen on the network but not counted yet (display only), or null.
payout, refundThe outgoing transaction, or null: status (pending, broadcast, confirmed, failed), amount, txid, broadcastAt, confirmedAt.
continuingtrue after the customer chose continue, until the order settles at a fresh price (usually within a minute).
actionscontinue / refund: true when the customer may choose (see Continue or refund).
noteA short message for the customer, or null.
guaranteeThe PGP-signed letter of guarantee, or null if signing failed.
rateLockedUntilFixed rate: the locked rate applies to a deposit detected before this time (coins whose deposits are only seen once mined, like Bitcoin, get a few extra minutes). Null for float, and after a late deposit.
expiresAtUntil when the order waits for the deposit: the rate lock for fixed, 60 minutes (by default) for float. A later deposit is still exchanged.
createdAt, completedAtISO 8601 timestamps (UTC). completedAt is null until completed.
Response 200
{
  "id": "3f6c1a52-8d1e-4b7a-9c35-2f0e6b9d4a17",
  "status": "completed",
  "from": "BTC",
  "to": "XMR",
  "rateType": "fixed",
  "depositAddress": "<BTC deposit address>",
  "payoutAddress": "<customer's XMR address>",
  "refundAddress": "<customer's BTC address>",
  "quote": {
    "amountIn": "0.01",
    "amountOut": "4.044423745",
    "rate": "410.6217"
  },
  "amountIn": "0.01",
  "amountOut": "4.044423745",
  "rate": "410.6217",
  "feePercent": 1.5,
  "networkFee": "0.0002",
  "requiredConfirmations": 2,
  "deposits": [
    {
      "txid": "<deposit txid>",
      "amount": "0.01",
      "confirmations": 2,
      "credited": true,
      "seenAt": "2026-10-19T12:04:12.000Z"
    }
  ],
  "payout": {
    "status": "confirmed",
    "amount": "4.044423745",
    "txid": "<payout txid>",
    "broadcastAt": "2026-10-19T12:31:40.000Z",
    "confirmedAt": "2026-10-19T12:52:03.000Z"
  },
  "pendingDeposit": null,
  "continuing": false,
  "actions": {
    "continue": false,
    "refund": false
  },
  "refund": null,
  "note": null,
  "guarantee": "-----BEGIN PGP SIGNED MESSAGE-----\n…\n-----END PGP SIGNATURE-----",
  "rateLockedUntil": "2026-10-19T12:10:00.000Z",
  "expiresAt": "2026-10-19T12:10:00.000Z",
  "createdAt": "2026-10-19T12:00:00.000Z",
  "completedAt": "2026-10-19T12:52:03.000Z"
}

List orders

POST/api/partner/v1/orders60/min

Your API orders, newest first. While more is true, ask for the next page with before set to the last id you got. Your dashboard also lists the orders from your referral link and exports everything as CSV.

FieldTypeDescription
limitnumber, optional1-100 orders, default 50.
beforestring, optionalAn order id from the previous page: returns the orders older than it.
statusstring, optionalOnly orders in this status (see Order lifecycle).
Request
{
  "limit": 2
}
Response 200
{
  "orders": [
    {
      "id": "3f6c1a52-8d1e-4b7a-9c35-2f0e6b9d4a17",
      "status": "completed",
      "from": "BTC",
      "to": "XMR",
      "rateType": "fixed",
      "amountIn": "0.01",
      "amountOut": "2.93811204",
      "feePercent": 1.5,
      "markupPercent": 0.5,
      "source": "api",
      "earningUsd": "4.84",
      "createdAt": "2026-10-19T10:02:11.000Z",
      "completedAt": "2026-10-19T10:31:40.000Z"
    },
    {
      "id": "9b2e7c40-1f3a-4d6e-8a51-7c0d2e4f6a18",
      "status": "awaiting_deposit",
      "from": "LTC",
      "to": "XMR",
      "rateType": "float",
      "amountIn": null,
      "amountOut": null,
      "feePercent": 1.5,
      "markupPercent": 0,
      "source": "api",
      "earningUsd": null,
      "createdAt": "2026-10-19T09:48:03.000Z",
      "completedAt": null
    }
  ],
  "more": true
}

earningUsd is set once an order completes. Orders from your referral link belong to their customers: in the dashboard and CSV they have no id or amounts and only the date.

Continue or refund

POST/api/partner/v1/continue30/min
POST/api/partner/v1/refund30/min

If the market rate moves against the quote by more than maxRateDriftPercent before the deposit is confirmed, the order stops in refund_required and waits for the customer. Ask them, then call one of these with { "id": "…" }. A deposit that cannot be exchanged (below the minimum, or too small to pay out) also stops in refund_required, with only actions.refund.

  • continue (when actions.continue is true): exchange at the market rate. The order shows continuing: true (both actions false) until it settles at a price fetched after the call, usually within a minute.
  • refund (when actions.refund is true): send the deposit back to the order's refund address, minus the network fee. Only possible if the order has a refund address.
Status while waiting (excerpt)
{
  "id": "3f6c1a52-8d1e-4b7a-9c35-2f0e6b9d4a17",
  "status": "refund_required",
  "continuing": false,
  "actions": {
    "continue": true,
    "refund": true
  },
  "note": "Market rate moved by 1.6% since your quote"
}

Both return the updated order (after continue still refund_required, with continuing: true). When the matching action is false you get 409. If neither action is true, the order needs our support team. After continue the order goes on like any other (it can still be held for review).

Webhooks

Pass webhookUrl to create and we POST the order to it right after creation, whenever its status changes and when its deposit is first seen, instead of you polling status. The order is the same JSON as status returns.

Webhook body
{
  "event": "order.updated",
  "order": {
    "id": "3f6c1a52-8d1e-4b7a-9c35-2f0e6b9d4a17",
    "status": "confirming",
    "from": "BTC",
    "to": "XMR",
    "rateType": "fixed",
    "depositAddress": "<BTC deposit address>",
    "payoutAddress": "<customer's XMR address>",
    "refundAddress": "<customer's BTC address>",
    "quote": {
      "amountIn": "0.01",
      "amountOut": "4.044423745",
      "rate": "410.6217"
    },
    "amountIn": null,
    "amountOut": null,
    "rate": null,
    "feePercent": 1.5,
    "networkFee": "0.0002",
    "requiredConfirmations": 2,
    "deposits": [],
    "payout": null,
    "pendingDeposit": null,
    "continuing": false,
    "actions": {
      "continue": false,
      "refund": false
    },
    "refund": null,
    "note": null,
    "guarantee": "-----BEGIN PGP SIGNED MESSAGE-----\n…\n-----END PGP SIGNATURE-----",
    "rateLockedUntil": "2026-10-19T12:10:00.000Z",
    "expiresAt": "2026-10-19T12:10:00.000Z",
    "createdAt": "2026-10-19T12:00:00.000Z",
    "completedAt": null
  }
}
HeaderValue
X-Webhook-TimestampUnix time in seconds when the request was sent.
X-Webhook-SignLowercase hex HMAC-SHA256 of `${timestamp}\n${body}` (the exact body bytes), keyed with your API secret.
Content-Type, User-Agentapplication/json and AnonChange-Webhook/1.
  • Check the signature with your API secret and reject old timestamps (for example more than 5 minutes), then answer any 2xx within 10 seconds. Treat each delivery as “the order changed”: they can arrive late or more than once, and a status that passed quickly can be skipped, so use the order's status, not the order of arrival.
  • Failed deliveries (no answer within 10 seconds, or not 2xx) are retried with growing pauses for about 12 hours, then given up.
  • Deliveries stop after completed or refunded was delivered, and 7 days after creation. An expired order is still watched (every 10 minutes) because a late deposit moves it on.
  • Only https URLs on port 443 with a public address; redirects are not followed. Deliveries come from changing IP addresses: verify the signature instead of allowlisting IPs.
  • After rotating your API secret, new deliveries are signed with the new one.
// Express: keep the raw body, check the signature and the time, then answer 2xx quickly.
import crypto from "node:crypto";
import express from "express";

const app = express();
app.post("/hooks/anonchange", express.raw({ type: "application/json" }), (req, res) => {
  const timestamp = req.get("X-Webhook-Timestamp");
  const expected = crypto
    .createHmac("sha256", process.env.ANONCHANGE_API_SECRET)
    .update(`${timestamp}\n${req.body}`)
    .digest("hex");
  const given = Buffer.from(req.get("X-Webhook-Sign") ?? "");
  const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;
  if (!fresh || given.length !== expected.length || !crypto.timingSafeEqual(given, Buffer.from(expected))) {
    return res.sendStatus(401);
  }
  const { order } = JSON.parse(req.body);
  // update your copy of order.id with order.status ...
  res.sendStatus(204);
});

Order lifecycle

The usual path is awaiting_deposit → confirming → exchanging → sending → completed. An order that received a deposit ends completed or refunded.

StatusMeaningWhat to show
awaiting_depositWaiting for the customer’s deposit.Deposit address, amount and the time left until expiresAt.
confirmingDeposit seen, waiting for network confirmations.“Deposit received”, with confirmations against requiredConfirmations.
exchangingDeposit credited, the payout is queued.“Exchanging”, the final amountIn and amountOut.
sendingPayout broadcast, waiting for its confirmation.“Sending”, with payout.txid.
completedPayout confirmed. Final.“Completed”, amountOut and payout.txid.
expiredNo deposit before expiresAt.“Expired”. A late deposit is still exchanged at the market rate and moves the order on.
reviewHeld for a manual check (for example a large amount or not enough reserve at that moment).“Under review”. It ends completed or refunded.
refund_requiredCannot be exchanged as it is: the rate moved, the deposit was below the minimum, or the payout is not possible.The customer’s choice if actions.continue or actions.refund is true; otherwise support handles it.
refundingRefund queued or broadcast to the refund address.“Refunding”, with refund.txid once sent.
refundedRefund sent. Final.“Refunded”, refund.amount and refund.txid.
failedThe payout needs a manual check by our team.The note, and a link to support.

Float and fixed rates

  • Float: the customer gets the market rate at the moment the deposit is confirmed; the quote is an estimate. If that rate is more than maxRateDriftPercent worse than the quote, the order stops for the customer's decision (see Continue or refund).
  • Fixed: the quoted rate is locked for rateLockMinutes (10 minutes by default, see rateLockedUntil) for a deposit of up to the quoted amount. A larger deposit gets the market rate or the fixed one, whichever is lower; a later deposit gets the market rate (either stops for the customer's decision like a float order if that rate moved too far). If the market moves so sharply that the locked rate cannot be paid, the order is held for review.

Review and refunds

  • review means a manual check by our team, for example for a large amount. After it the order is completed or refunded.
  • Refunds go to the order's refundAddress, minus the network fee. Always pass a refund address: without one the customer has to contact support.
  • A deposit below the minimum is not exchanged: the order stops in refund_required for a refund.
  • A float order waits 60 minutes (by default) for the deposit, a fixed one as long as its rate lock (see expiresAt). After that it is expired, but a late deposit is still exchanged at the market rate.

Limits and polling

Requests per API key per minute:

EndpointPer minute
currencies120
pairs120
price600
create60
status600
orders60
continue, refund30
  • Unpaid open orders (awaiting_deposit) per partner are capped. Above the cap, create returns 503 until older orders are paid or expire.
  • On top of that, up to 3000 requests per minute per IP address reach the partner API.
  • A 429 carries a Retry-After header (seconds). currencies and pairs change slowly: cache them for a minute.
  • Poll status every 15-30 seconds while an order is open, and stop once it is completed or refunded. Or skip polling with webhooks.

Common errors and fixes

Every error is { "error": "message" }. These are the messages integrations run into most, as the API sends them, and what to change.

StatusErrorFix
400Body is not valid JSON but content-type is set to 'application/json'Send valid JSON. An endpoint without fields takes {}.
400body must be objectSend a JSON object as the body, at least {}.
400body must NOT have additional propertiesA field name is wrong or unknown: amount (not amountIn) in price and create, id (not orderId) in status, continue and refund.
400body must have required property 'rateType'Add the named field. Names are case-sensitive.
400Enter a valid amount (digits and up to 12 decimals)Amounts are strings like "0.01", never JSON numbers, with a dot and no thousands separators. Also for amountOut ("body/amountOut must be string").
400Invalid BTC amount (at most 8 decimals)Round to the coin's decimals from currencies.
400body/from must be equal to one of the allowed valuesUse the coin code from currencies, in capitals: BTC, XMR, USDT_SOL.
400Choose two different coinsfrom and to must differ.
400This pair is not availableOne of the coins is disabled right now (enabled: false in currencies). Hide it and check again later.
400Send either amount or amountOut, not bothPick one: what the customer sends (amount) or receives (amountOut).
400Orders need an amountEvery order needs amount or amountOut.
400Amount must be within the deposit limitsThe deposit must be at least minDeposit (from price or currencies).
400Invalid XMR payout addresspayoutAddress is for the to coin, refundAddress for the from coin ("Invalid BTC refund address"). Mainnet addresses only, as the customer gave them.
400Enter a valid payout addresspayoutAddress is missing or shorter than 20 characters.
400That XMR payout address belongs to this exchange; use an address from your own walletA deposit address of ours was pasted as the payout address. Ask the customer for their own.
400markupPercent can be at most 2Stay within the cap shown in your dashboard. It is a number (0.5), not a string, with at most 2 decimals.
400webhookUrl must use httpsOnly https URLs on port 443 with a public host name or address (also "must use the default https port (443)", "must be a public address").
400Idempotency-Key must be 8-128 letters, digits, - or _For example your checkout id, or a UUID.
401X-API-KEY, X-API-TIMESTAMP, X-API-NONCE and X-API-SIGN headers are requiredSend all four headers on every request, including currencies and pairs.
401X-API-TIMESTAMP must be the current unix time in secondsUse seconds (10 digits), not milliseconds, and keep your server clock synced (NTP): we accept 60 seconds either way.
401X-API-NONCE must be 16-64 letters, digits, - or _For example 32 random hex characters (openssl rand -hex 16).
401X-API-NONCE was already usedMake a new nonce for every request, retries included.
401Invalid API key or signatureUsually the signed string differs from the request: the path must include /api/partner/v1 and nothing after it (no query string, no trailing slash); hash the exact body bytes you send, not a re-serialized copy; the method is POST in capitals; no newline at the end; hex digests, not base64. Also check you use the current secret (rotating it ends the old one).
403This partner account is disabledContact support.
404Unknown endpoint GET /api/partner/v1/…: every endpoint is a POST, see /docsUse POST, the exact endpoint name and no trailing slash.
404Order not foundThe id is not one of your orders (orders made with another API key or on the site are not visible). In orders, before must be one of your order ids.
409This order cannot be continuedOnly when the order's actions.continue is true (after a price move). Check status first.
409This order cannot be refunded by you; please contact supportOnly when actions.refund is true. Otherwise support handles the order.
409Payouts are paused right now, try again laterAlso "No fresh market rate, try again shortly". Retry after a minute; the customer's choice is kept.
415Unsupported Media TypeSet Content-Type: application/json.
429Rate limit exceeded, retry in 1 minuteWait Retry-After seconds. Cache currencies and pairs (they change slowly), poll status every 15-30 seconds or use webhooks.
503Rates temporarily unavailableAlso "Exchange is temporarily disabled" and "BTC deposits are temporarily unavailable, try again later". Show the pair as unavailable and retry later.
503Insufficient payout liquidity for this amountThe amount is above what we can pay out right now: stay within maxDeposit from price. Also "Fixed-rate exposure limit reached" (try float) and "Payout liquidity is not verified; try again shortly".
503Too many unpaid orders open for this API keyCreate an order only when the customer is ready to pay; unpaid ones expire.
503No deposit address available, try again in a minuteAlso "Too many open orders right now, try again later". Retry with the same Idempotency-Key.

Mistakes without an error

SymptomFix
viewKey and url missing from createA retry with an Idempotency-Key that was used before returns the first order (200) without them. Store them from the first 201. The retry's body is not compared: use a new key per order.
Your webhook check failsCompute the HMAC over the raw body bytes before parsing the JSON, with your current API secret, and the timestamp header as it came.
The customer sent the coin on another networkOnly the from coin on its own network is credited (USDT_SOL: USDT on Solana, not on Ethereum or Tron). Show the network next to the deposit address.
An expired order moves onA deposit after expiresAt is still exchanged, at the market rate. Keep watching an expired order for a few days, or use webhooks (they continue).

Stuck? Log the exact signed string and the headers you sent (never the secret) and compare them with the curl example in Authentication, which is known to work.

Letter of guarantee

Every order comes with a letter of guarantee in its guarantee field: a PGP cleartext-signed text with the order's terms (amounts, rate, fees, addresses, validity). It states that:

  • the order is either completed on the terms in the letter or refunded;
  • no KYC: we never ask for identity documents or personal information;
  • we do not freeze funds; some orders need a manual check, after which they are completed or refunded;
  • refunds go to the refund address, minus the network fee (an amount too small to cover it cannot be refunded).

Anyone can verify it with GnuPG:

bash
curl -s https://anonchange.com/pgp.txt | gpg --import
# save the order's "guarantee" field as letter.asc, e.g.
jq -r .guarantee order.json > letter.asc
gpg --verify letter.asc

Compare the fingerprint gpg shows with the one on our PGP key page. You can pass the letter on to your customers as it is.

Affiliate program

One partner account covers both referral links and the API.

  • Referral links: share https://anonchange.com/?ref=YOUR_CODE with your code from the dashboard. The visitor's browser keeps the referral for 30 days, and the visitor pays the normal site price.
  • API orders: you earn a share of our fee plus your own optional markup. Set the markup in your dashboard, or per request with markupPercent (it is capped); it is included in feePercent.
  • Your share, the markup cap and the minimum payout are shown in your dashboard.
  • Earnings are counted in USD when an order completes.
  • Request a payout in the dashboard in any supported coin once your balance is above the minimum. We send it after a check, minus that coin's network fee.

Swap widget

No code needed: paste the widget on your site. Visitors see a live quote; Exchange opens the swap on AnonChange in a new tab with your referral, so it earns like your referral link. Your dashboard has the code with your referral filled in.

HTML
<iframe
  src="https://anonchange.com/widget?ref=YOUR_CODE&from=BTC&to=XMR"
  title="AnonChange swap"
  width="100%"
  height="400"
  style="border:0;max-width:456px"
></iframe>
ParameterMeaning
refYour referral code (from the dashboard). Swaps opened from the widget earn like your referral link.
from, toOptional: the coins preselected, e.g. BTC and XMR.
amountOptional: the amount to send, preselected.

If you sandbox the iframe, allow allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox so the swap can open.

Support

Questions about the integration or an order? Contact us on Telegram or Signal (anonchange.83) with the order ID.

API documentation | AnonChange