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.
Base URL: https://anonchange.com/api/partner/v1
POST with a JSON body and Content-Type: application/json. Send {} when an endpoint has no fields. Unknown fields are rejected.{ "error": "message" } with an HTTP status code."0.01"), never numbers, in whole coins. Percentages and counts are numbers. Times are ISO 8601 in UTC.BTC, XMR or USDT_SOL (USDT on Solana).| Status | Meaning |
|---|---|
| 400 | Invalid input: unknown or missing field, bad amount or address, pair not available, body not JSON. |
| 401 | Missing or invalid authentication headers, expired timestamp, reused nonce or wrong signature. |
| 403 | Your partner account is disabled. |
| 404 | Order not found (or not one of yours), or no such endpoint (every endpoint is a POST). |
| 409 | The order is not in a state that allows this action, or it has to wait a moment. |
| 413 | Body larger than 16 KB. |
| 415 | Content-Type is not application/json. |
| 429 | Rate limit reached. Wait for the Retry-After header's seconds and retry. |
| 500 | Our error. Retry later; for create, retry with the same Idempotency-Key. |
| 503 | Temporarily 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.
Every request carries four headers:
| Header | Value |
|---|---|
| X-API-KEY | Your API key. |
| X-API-TIMESTAMP | Current unix time in seconds. Must be within 60 seconds of our clock. |
| X-API-NONCE | A random string of 16-64 characters (A-Z, a-z, 0-9, - and _). Each nonce works once within 120 seconds. |
| X-API-SIGN | Lowercase hex HMAC-SHA256 of the signed string, keyed with your API secret. |
${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"The whole API as an OpenAPI 3.1 spec (for code generators and API tools) and as a Postman collection.
apiKey and apiSecret. Its pre-request script signs every request, so they work as they are.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() });All coins with their limits. Body: {}. Only coins with enabled: true can be exchanged right now.
| Field | Type | Description |
|---|---|---|
| coin, name | string | Coin code (used everywhere else) and display name. |
| decimals | number | Decimals the coin's amounts can have. |
| enabled | boolean | Available for new orders. |
| minDeposit | string | Smallest deposit we exchange. Smaller deposits are refunded. |
| confirmations | number | Confirmations a deposit needs, as an order's requiredConfirmations shows them (blocks to finality on Ethereum and Solana). |
| networkFee | string | Network fee taken from a payout in this coin. |
| usd | string | null | Current USD price, if known. |
| reserve | string | What we can pay out in this coin right now: the most an order into it can receive. |
[
{
"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"
}
]Every pair of enabled coins and the rate types it supports. Body: {}.
[
{
"from": "BTC",
"to": "XMR",
"rateTypes": [
"float",
"fixed"
]
},
{
"from": "XMR",
"to": "BTC",
"rateTypes": [
"float",
"fixed"
]
}
]A quote for a pair. Nothing is reserved: create the order to get a deposit address.
| Field | Type | Description |
|---|---|---|
| from | string | Coin the customer sends. |
| to | string | Coin the customer receives. |
| rateType | string | float or fixed. |
| amount | string, optional | Amount of the from coin the customer sends. Without it (and without amountOut), amountIn and amountOut are null. |
| amountOut | string, optional | Quote by the amount to receive instead: amountIn becomes the smallest deposit that receives at least this. Not together with amount. |
| markupPercent | number, optional | Your markup for this request, 0 up to the cap shown in your dashboard, at most 2 decimals. Without it, your dashboard markup applies. |
{
"from": "BTC",
"to": "XMR",
"rateType": "fixed",
"amount": "0.01"
}{
"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).| Field | Type | Description |
|---|---|---|
| from, to, rateType | string | As for price. |
| amount | string | Required (or amountOut instead). At least minDeposit. |
| amountOut | string, optional | Instead of amount: the amount the customer should receive. The deposit amount is worked out from it, as in price. |
| markupPercent | number, optional | Your markup for this order, as in price. Saved with the order. |
| webhookUrl | string, optional | https URL that gets a signed POST whenever the order's status changes (see Webhooks). |
| payoutAddress | string | The customer's address for the to coin. |
| refundAddress | string, optional | The 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.
{
"from": "BTC",
"to": "XMR",
"rateType": "fixed",
"amount": "0.01",
"payoutAddress": "<customer's XMR address>",
"refundAddress": "<customer's BTC address>"
}{
"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>"
}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.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"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.
| Field | Description |
|---|---|
| id | Order ID (UUID). Use it with status, continue and refund. |
| status | Current state, see the order lifecycle below. |
| from, to, rateType | As given at creation. |
| depositAddress | Where the customer sends the coins. Show it while the status is awaiting_deposit. |
| payoutAddress, refundAddress | As given at creation (refundAddress may be null). |
| quote | The quoted amountIn, amountOut and rate at creation. |
| amountIn, amountOut, rate | What was actually received, paid out and applied. Null until the deposit is credited. |
| feePercent, networkFee | Total fee in percent (your markup included) and the network fee taken from the payout. |
| requiredConfirmations | Confirmations a deposit needs before it is credited. |
| deposits | Deposits counted for this order: txid, amount, confirmations, credited, seenAt. |
| pendingDeposit | A deposit seen on the network but not counted yet (display only), or null. |
| payout, refund | The outgoing transaction, or null: status (pending, broadcast, confirmed, failed), amount, txid, broadcastAt, confirmedAt. |
| continuing | true after the customer chose continue, until the order settles at a fresh price (usually within a minute). |
| actions | continue / refund: true when the customer may choose (see Continue or refund). |
| note | A short message for the customer, or null. |
| guarantee | The PGP-signed letter of guarantee, or null if signing failed. |
| rateLockedUntil | Fixed 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. |
| expiresAt | Until 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, completedAt | ISO 8601 timestamps (UTC). completedAt is null until completed. |
{
"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"
}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.
| Field | Type | Description |
|---|---|---|
| limit | number, optional | 1-100 orders, default 50. |
| before | string, optional | An order id from the previous page: returns the orders older than it. |
| status | string, optional | Only orders in this status (see Order lifecycle). |
{
"limit": 2
}{
"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.
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.{
"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).
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.
{
"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
}
}| Header | Value |
|---|---|
| X-Webhook-Timestamp | Unix time in seconds when the request was sent. |
| X-Webhook-Sign | Lowercase hex HMAC-SHA256 of `${timestamp}\n${body}` (the exact body bytes), keyed with your API secret. |
| Content-Type, User-Agent | application/json and AnonChange-Webhook/1. |
status, not the order of arrival.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.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.// 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);
});The usual path is awaiting_deposit → confirming → exchanging → sending → completed. An order that received a deposit ends completed or refunded.
| Status | Meaning | What to show |
|---|---|---|
| awaiting_deposit | Waiting for the customer’s deposit. | Deposit address, amount and the time left until expiresAt. |
| confirming | Deposit seen, waiting for network confirmations. | “Deposit received”, with confirmations against requiredConfirmations. |
| exchanging | Deposit credited, the payout is queued. | “Exchanging”, the final amountIn and amountOut. |
| sending | Payout broadcast, waiting for its confirmation. | “Sending”, with payout.txid. |
| completed | Payout confirmed. Final. | “Completed”, amountOut and payout.txid. |
| expired | No deposit before expiresAt. | “Expired”. A late deposit is still exchanged at the market rate and moves the order on. |
| review | Held for a manual check (for example a large amount or not enough reserve at that moment). | “Under review”. It ends completed or refunded. |
| refund_required | Cannot 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. |
| refunding | Refund queued or broadcast to the refund address. | “Refunding”, with refund.txid once sent. |
| refunded | Refund sent. Final. | “Refunded”, refund.amount and refund.txid. |
| failed | The payout needs a manual check by our team. | The note, and a link to support. |
maxRateDriftPercent worse than the quote, the order stops for the customer's decision (see Continue or refund).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 means a manual check by our team, for example for a large amount. After it the order is completed or refunded.refundAddress, minus the network fee. Always pass a refund address: without one the customer has to contact support.refund_required for a refund.expiresAt). After that it is expired, but a late deposit is still exchanged at the market rate.Requests per API key per minute:
| Endpoint | Per minute |
|---|---|
| currencies | 120 |
| pairs | 120 |
| price | 600 |
| create | 60 |
| status | 600 |
| orders | 60 |
| continue, refund | 30 |
awaiting_deposit) per partner are capped. Above the cap, create returns 503 until older orders are paid or expire.Retry-After header (seconds). currencies and pairs change slowly: cache them for a minute.status every 15-30 seconds while an order is open, and stop once it is completed or refunded. Or skip polling with webhooks.Every error is { "error": "message" }. These are the messages integrations run into most, as the API sends them, and what to change.
| Status | Error | Fix |
|---|---|---|
| 400 | Body is not valid JSON but content-type is set to 'application/json' | Send valid JSON. An endpoint without fields takes {}. |
| 400 | body must be object | Send a JSON object as the body, at least {}. |
| 400 | body must NOT have additional properties | A field name is wrong or unknown: amount (not amountIn) in price and create, id (not orderId) in status, continue and refund. |
| 400 | body must have required property 'rateType' | Add the named field. Names are case-sensitive. |
| 400 | Enter 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"). |
| 400 | Invalid BTC amount (at most 8 decimals) | Round to the coin's decimals from currencies. |
| 400 | body/from must be equal to one of the allowed values | Use the coin code from currencies, in capitals: BTC, XMR, USDT_SOL. |
| 400 | Choose two different coins | from and to must differ. |
| 400 | This pair is not available | One of the coins is disabled right now (enabled: false in currencies). Hide it and check again later. |
| 400 | Send either amount or amountOut, not both | Pick one: what the customer sends (amount) or receives (amountOut). |
| 400 | Orders need an amount | Every order needs amount or amountOut. |
| 400 | Amount must be within the deposit limits | The deposit must be at least minDeposit (from price or currencies). |
| 400 | Invalid XMR payout address | payoutAddress is for the to coin, refundAddress for the from coin ("Invalid BTC refund address"). Mainnet addresses only, as the customer gave them. |
| 400 | Enter a valid payout address | payoutAddress is missing or shorter than 20 characters. |
| 400 | That XMR payout address belongs to this exchange; use an address from your own wallet | A deposit address of ours was pasted as the payout address. Ask the customer for their own. |
| 400 | markupPercent can be at most 2 | Stay within the cap shown in your dashboard. It is a number (0.5), not a string, with at most 2 decimals. |
| 400 | webhookUrl must use https | Only 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"). |
| 400 | Idempotency-Key must be 8-128 letters, digits, - or _ | For example your checkout id, or a UUID. |
| 401 | X-API-KEY, X-API-TIMESTAMP, X-API-NONCE and X-API-SIGN headers are required | Send all four headers on every request, including currencies and pairs. |
| 401 | X-API-TIMESTAMP must be the current unix time in seconds | Use seconds (10 digits), not milliseconds, and keep your server clock synced (NTP): we accept 60 seconds either way. |
| 401 | X-API-NONCE must be 16-64 letters, digits, - or _ | For example 32 random hex characters (openssl rand -hex 16). |
| 401 | X-API-NONCE was already used | Make a new nonce for every request, retries included. |
| 401 | Invalid API key or signature | Usually 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). |
| 403 | This partner account is disabled | Contact support. |
| 404 | Unknown endpoint GET /api/partner/v1/…: every endpoint is a POST, see /docs | Use POST, the exact endpoint name and no trailing slash. |
| 404 | Order not found | The 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. |
| 409 | This order cannot be continued | Only when the order's actions.continue is true (after a price move). Check status first. |
| 409 | This order cannot be refunded by you; please contact support | Only when actions.refund is true. Otherwise support handles the order. |
| 409 | Payouts are paused right now, try again later | Also "No fresh market rate, try again shortly". Retry after a minute; the customer's choice is kept. |
| 415 | Unsupported Media Type | Set Content-Type: application/json. |
| 429 | Rate limit exceeded, retry in 1 minute | Wait Retry-After seconds. Cache currencies and pairs (they change slowly), poll status every 15-30 seconds or use webhooks. |
| 503 | Rates temporarily unavailable | Also "Exchange is temporarily disabled" and "BTC deposits are temporarily unavailable, try again later". Show the pair as unavailable and retry later. |
| 503 | Insufficient payout liquidity for this amount | The 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". |
| 503 | Too many unpaid orders open for this API key | Create an order only when the customer is ready to pay; unpaid ones expire. |
| 503 | No deposit address available, try again in a minute | Also "Too many open orders right now, try again later". Retry with the same Idempotency-Key. |
| Symptom | Fix |
|---|---|
| viewKey and url missing from create | A 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 fails | Compute 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 network | Only 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 on | A 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.
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:
Anyone can verify it with GnuPG:
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.ascCompare the fingerprint gpg shows with the one on our PGP key page. You can pass the letter on to your customers as it is.
One partner account covers both referral links and the API.
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.markupPercent (it is capped); it is included in feePercent.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.
<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>| Parameter | Meaning |
|---|---|
| ref | Your referral code (from the dashboard). Swaps opened from the widget earn like your referral link. |
| from, to | Optional: the coins preselected, e.g. BTC and XMR. |
| amount | Optional: 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.
Questions about the integration or an order? Contact us on Telegram or Signal (anonchange.83) with the order ID.