AvaSettle API
Accept crypto on any site. Create an invoice, send your customer to the payment link, and mark the order paid when the signed webhook arrives. Every payment is credited to your balance in USDT (BEP20).
Overview
The API speaks JSON over HTTPS. The base URL is https://pay.avasettle.com. Every request is signed with your API secret, so it can't be changed or replayed on the way.
- Hosted invoice (recommended): you create an invoice and redirect the customer. AvaSettle shows coins, addresses and QR codes, follows the blockchain and sends the customer back to your
success_url. - Direct payment: you pick the coin yourself and show the address in your own page.
Get your API key, API secret and webhook secret in your AvaSettle account under API keys and Payment notifications. Keys can be limited to your server's IP addresses.
Authentication
Send these four headers with every request:
| Header | Type | Value |
|---|---|---|
X-AVS-Key | string | Your public API key, pk_ + 32 hex characters. |
X-AVS-Timestamp | integer | Unix time in seconds. Requests more than 5 minutes off are refused. |
X-AVS-Nonce | string | 16 to 64 letters or digits, new for every request. |
X-AVS-Signature | hex | HMAC-SHA256 of the string below, keyed with your API secret. |
{timestamp}.{nonce}.{METHOD}.{path with query}.{raw body}The path includes the query string (for example /v1/payments?order_id=1001). For GET requests the body is empty, so the string ends with a dot. Sign the exact bytes you send.
<?php
function avasettle(string $method, string $path, ?array $body = null): array
{
$base = 'https://pay.avasettle.com';
$key = getenv('AVASETTLE_KEY'); // pk_...
$secret = getenv('AVASETTLE_SECRET'); // sk_...
$raw = $body === null ? '' : json_encode($body, JSON_UNESCAPED_SLASHES);
$ts = (string) time();
$nonce = bin2hex(random_bytes(12));
$sig = hash_hmac('sha256', "$ts.$nonce.$method.$path.$raw", $secret);
$ch = curl_init($base . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => $body === null ? null : $raw,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
"X-AVS-Key: $key",
"X-AVS-Timestamp: $ts",
"X-AVS-Nonce: $nonce",
"X-AVS-Signature: $sig",
],
]);
$res = json_decode((string) curl_exec($ch), true);
curl_close($ch);
return $res;
}import crypto from 'node:crypto';
export async function avasettle(method, path, body) {
const raw = body === undefined ? '' : JSON.stringify(body);
const ts = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomBytes(12).toString('hex');
const sig = crypto.createHmac('sha256', process.env.AVASETTLE_SECRET)
.update(`${ts}.${nonce}.${method}.${path}.${raw}`).digest('hex');
const res = await fetch('https://pay.avasettle.com' + path, {
method,
body: body === undefined ? undefined : raw,
headers: {
'content-type': 'application/json',
'x-avs-key': process.env.AVASETTLE_KEY,
'x-avs-timestamp': ts,
'x-avs-nonce': nonce,
'x-avs-signature': sig,
},
});
return res.json();
}import hashlib, hmac, json, os, secrets, time, urllib.request
def avasettle(method, path, body=None):
raw = '' if body is None else json.dumps(body, separators=(',', ':'))
ts, nonce = str(int(time.time())), secrets.token_hex(12)
sig = hmac.new(os.environ['AVASETTLE_SECRET'].encode(),
f'{ts}.{nonce}.{method}.{path}.{raw}'.encode(), hashlib.sha256).hexdigest()
req = urllib.request.Request('https://pay.avasettle.com' + path, method=method,
data=None if body is None else raw.encode(), headers={
'Content-Type': 'application/json', 'X-AVS-Key': os.environ['AVASETTLE_KEY'],
'X-AVS-Timestamp': ts, 'X-AVS-Nonce': nonce, 'X-AVS-Signature': sig})
with urllib.request.urlopen(req) as r:
return json.load(r)KEY=pk_your_key; SECRET=sk_your_secret
BODY='{"order_id":"1001","amount":"49.90","currency":"USD"}'
TS=$(date +%s); NONCE=$(openssl rand -hex 12)
SIG=$(printf '%s' "$TS.$NONCE.POST./v1/invoices.$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')
curl -s https://pay.avasettle.com/v1/invoices \
-H "Content-Type: application/json" -H "X-AVS-Key: $KEY" \
-H "X-AVS-Timestamp: $TS" -H "X-AVS-Nonce: $NONCE" -H "X-AVS-Signature: $SIG" \
-d "$BODY"Quick start
- Create an invoice when the customer chooses crypto at checkout:
POST /v1/invoices. - Redirect the customer to
invoice.url. They choose a coin and pay. - Receive the webhook
payment.paid, check its signature, read the payment again withGET /v1/payments/{id}and mark the order paid. - Also check on return: when the customer lands on your
success_url, callGET /v1/payments?order_id=…. This covers the rare case where your webhook endpoint was down.
Create an invoice
POST/v1/invoices
Creates a hosted payment page for one order. Calling it again with the same order_id and amount while the invoice is open returns the same invoice, so retries are safe.
| Field | Type | Description |
|---|---|---|
order_id | string, required | Your order number. Letters, digits and _ . : -, up to 120 characters. Use a prefix if several stores share one account. |
amount | decimal string, required | Amount to charge, for example "49.90". Up to 8 decimals. |
currency | string, required | The amount's currency: USD, EUR and other fiat codes. |
description | string | Shown to the customer, up to 200 characters. |
success_url | https URL | Where the customer goes after paying. |
cancel_url | https URL | Where the "back to store" link points. |
expires_in_minutes | integer | 15 to 10080. Default: the payment window set in your account. |
metadata | object | Your own data, returned on every payment and webhook. Up to 4 KB. |
{
"order_id": "1001",
"amount": "49.90",
"currency": "USD",
"description": "Order #1001 at Example Store",
"success_url": "https://store.example/checkout/thanks?order=1001",
"cancel_url": "https://store.example/cart",
"expires_in_minutes": 60,
"metadata": { "customer": 42 }
}{
"invoice": {
"id": "inv_3f0c1b8e2a9d4c7f8e1a2b3c4d5e6f70",
"url": "https://pay.avasettle.com/i/inv_3f0c1b8e2a9d4c7f8e1a2b3c4d5e6f70",
"order_id": "1001",
"amount": "49.90",
"currency": "USD",
"description": "Order #1001 at Example Store",
"status": "open",
"success_url": "https://store.example/checkout/thanks?order=1001",
"cancel_url": "https://store.example/cart",
"metadata": { "customer": 42 },
"created_at": "2026-10-08T09:00:00.000Z",
"expires_at": "2026-10-08T10:00:00.000Z",
"payment": null,
"payments": []
}
}Get an invoice
GET/v1/invoices/{id}
Returns the invoice with its current status: open, processing (a payment arrived and is confirming), paid, partial or expired. payment is the latest payment and payments lists every coin the customer tried.
Available coins
GET/v1/options
Coins and networks switched on in your account, for building your own coin picker.
{"options":{"USDT":{"name":"Tether","networks":[{"code":"TRON","label":"TRON (TRC20)","token":true},{"code":"BSC","label":"BNB Smart Chain (BEP20)","token":true}]},"BTC":{"name":"Bitcoin","networks":[{"code":"BTC","label":"Bitcoin","token":false}]}}}Create a payment
POST/v1/payments
For your own payment page: AvaSettle returns a fresh deposit address and the exact crypto amount for the coin you choose. Show address, amount and qr to the customer.
| Field | Type | Description |
|---|---|---|
order_id | string, required | As above. |
amount | decimal string, required | Fiat amount. |
currency | string, required | Fiat currency. |
coin | string, required | For example BTC, USDT, XMR. |
network | string, required | A network code from /v1/options, for example TRON or BSC. |
metadata | object | Your own data. |
{"order_id":"1001","amount":"49.90","currency":"USD","coin":"USDT","network":"TRON"}The response is {"payment": {…}} with the payment object below. The quoted amount is valid until expires_at.
Get a payment
GET/v1/payments/{id}
Add ?refresh=1 to check the blockchain right now instead of waiting for the next scan (at most once every 10 seconds per payment). If the network is slow the response carries "warning": "chain_unreachable" and the last known state.
Payments for an order
GET/v1/payments?order_id=1001
Every payment created for that order, newest first, as {"payments": [ … ]}.
The payment object
{
"id": "pay_6c2f9a1e0b7d4e3f8a9b0c1d",
"order_id": "1001",
"status": "paid",
"coin": "USDT",
"network": "TRON",
"network_label": "TRON (TRC20)",
"address": "TQ5n8kWmRbV3yLx2Jd9HfPc7Gs4Ae6Tu1Z",
"memo": null,
"amount": "49.90",
"received": "49.90",
"remaining": "0",
"confirmations": 20,
"required_confirmations": 20,
"fiat_amount": "49.90",
"fiat_currency": "USD",
"paid_fiat": "49.90",
"txid": "9f2c...e41a",
"tx_url": "https://tronscan.org/#/transaction/9f2c...e41a",
"qr": "TQ5n8kWmRbV3yLx2Jd9HfPc7Gs4Ae6Tu1Z",
"metadata": { "checkout": "inv_3f0c1b8e2a9d4c7f8e1a2b3c4d5e6f70" },
"created_at": "2026-10-08T09:01:12.000Z",
"expires_at": "2026-10-08T09:31:12.000Z",
"final_at": "2026-10-08T09:05:40.000Z"
}| Field | Meaning |
|---|---|
amount | Crypto amount the customer has to send. |
received / remaining | What has arrived so far and what is still missing. |
fiat_amount, fiat_currency | The amount you asked for. Compare these with your order before marking it paid. |
paid_fiat | Value received, in your currency, once the payment is final. |
memo | Required destination tag or memo on some networks. Show it when it is not null. |
qr | Text for a QR code (a wallet URI when the network supports one). |
txid, tx_url | The customer's transaction and a block explorer link. |
Payment statuses
| Status | Meaning |
|---|---|
pending | Waiting for the customer to send. |
confirming | Seen on the blockchain, waiting for confirmations. |
underpaid | Less than the amount arrived. The customer can send the rest to the same address. |
paid | Final. The full amount (within your tolerance) is confirmed. Fulfil the order. |
partial | Final, but short. Decide yourself: paid_fiat tells you how much arrived. |
expired | Nothing arrived before the quote ran out. |
paid and partial never change again. Payments that arrive late are still followed and credited.
Webhooks
Set your endpoint in your account under Payment notifications. AvaSettle sends a POST for each event:
| Event | When |
|---|---|
payment.detected | The transaction is on the blockchain. |
payment.underpaid | Less than the amount arrived. |
payment.paid | Final and complete. |
payment.partial | Final and short. |
payment.expired | Nothing arrived in time. |
payment.test | Sent by the "Send test" button. |
POST /your/webhook
X-AVS-Event: payment.paid
X-AVS-Delivery: 1842
X-AVS-Timestamp: 1791450340
X-AVS-Signature: 5b1f…(64 hex chars)
{
"event": "payment.paid",
"created_at": "2026-10-08T09:05:40.000Z",
"data": { "id": "pay_6c2f9a1e0b7d4e3f8a9b0c1d", "order_id": "1001", "status": "paid", "...": "the payment object" }
}Verify X-AVS-Signature = hex(HMAC-SHA256(webhook secret, {timestamp}.{raw body})) and refuse timestamps older than 5 minutes. Then read the payment again with your API key and act on that copy. Answer with any 2xx status. Failed deliveries are retried at the interval and number of times set in your account.
<?php
$raw = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_AVS_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_AVS_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $ts . '.' . $raw, getenv('AVASETTLE_WEBHOOK_SECRET'));
if (!ctype_digit($ts) || abs(time() - (int) $ts) > 300 || !hash_equals($expected, $sig)) {
http_response_code(401);
exit;
}
$event = json_decode($raw, true);
if ($event['event'] === 'payment.test') exit('ok');
// Never trust the body alone: read the payment again with your API key.
$payment = avasettle('GET', '/v1/payments/' . $event['data']['id'])['payment'];
if ($payment['status'] === 'paid'
&& $payment['fiat_amount'] === $order->total // the amount you asked for
&& $payment['fiat_currency'] === $order->currency) {
$order->markPaid($payment['txid']);
}
echo 'ok';import crypto from 'node:crypto';
app.post('/avasettle/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
const ts = req.get('x-avs-timestamp') ?? '';
const expected = crypto.createHmac('sha256', process.env.AVASETTLE_WEBHOOK_SECRET)
.update(`${ts}.${req.body}`).digest('hex');
const sig = req.get('x-avs-signature') ?? '';
const fresh = Math.abs(Date.now() / 1000 - Number(ts)) <= 300;
if (!fresh || sig.length !== 64 || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) return res.sendStatus(401);
const event = JSON.parse(req.body);
if (event.event !== 'payment.test') {
const { payment } = await avasettle('GET', `/v1/payments/${event.data.id}`);
if (payment.status === 'paid') await markOrderPaid(payment.order_id, payment);
}
res.send('ok');
});Errors and limits
{"error":{"code":"invalid_request","message":"Invalid or missing field: amount (decimal string)"}}| HTTP | Code | Meaning |
|---|---|---|
| 400 | invalid_request | A field is missing or has the wrong format. |
| 401 | unauthorized | Missing headers, bad signature, old timestamp, reused nonce or revoked key. |
| 403 | forbidden | The request came from an IP address not allowed for this key. |
| 404 | not_found | No such invoice or payment in your account. |
| 409 | varies | The invoice is already paid, funded or expired. |
| 429 | rate_limited | More than 300 requests a minute for one key. |
Plugins and libraries
Ready-made modules for WHMCS, WooCommerce, PrestaShop, OpenCart, Magento 2 and Drupal Commerce are on the integrations page. Each one includes AvaSettleApi.php, a small PHP client you can reuse in your own code.
Updated