Platform guide
Integrate CoinAvio
into your platform.
For software that serves many merchants: panel scripts, billing systems and carts. Each merchant connects their own CoinAvio account by pasting two values. Your code creates USD invoices, sends the buyer to our hosted checkout and credits the order from a signed webhook.
CoinAvio is a custodial gateway: the buyer pays in crypto and the merchant is credited in their CoinAvio balance. Your platform never handles wallets, keys or blockchains.
https://api.coinavio.com/v1What your merchants paste
Add a CoinAvio payment method with two required fields and one optional switch. There is no Merchant ID: the API key identifies the merchant. The merchant does not configure a webhook URL either, because your platform sends its own callbackUrl with every invoice.
| Field | Required | Format | Where the merchant finds it |
|---|---|---|---|
| API key | Required | ck_live_… or ck_test_… | Dashboard → Developers → Create API key. Shown once; the merchant copies the full key. |
| Webhook secret | Required | whsec_… | Dashboard → Developers → Signing secret. |
| Buyer pays fee | Optional | Yes / No | Your own setting. Sent as buyerPaysFee on each invoice. |
- Detect the mode from the key prefix:
ck_test_is test,ck_live_is live. The full key has the form<keyId>.<secret>; send it unchanged asAuthorization: Bearer <apiKey>. - Keys are available once CoinAvio has approved the merchant account. An invalid or revoked key returns 401; an account that is not active returns 403.
- Optional: when the merchant saves the settings, call
GET /v1/accountto check the key and show the merchant name and mode.
GET https://api.coinavio.com/v1/account
Authorization: Bearer <apiKey>
{
"merchant": { "id": "cm1…", "name": "Demo Store", "status": "ACTIVE", "feeBps": 0, "feePercent": "0.00" },
"apiKey": { "test": false, "mode": "live" },
"webhook": { "configured": false, "url": null },
"currencies": [ { "currency": "USDT", "network": "BSC", "live": true, … } ]
}Swipe within the code to read long lines.
Payment flow
- Create a payment record on your side, then call
POST /v1/invoiceswithpriceAmount(USD, as a string),orderId,successUrl,cancelUrl,callbackUrland, if the merchant enabled it,buyerPaysFee: true. - Save the returned invoice
idwith your payment record, log anywarnings, and redirect the buyer tocheckoutUrlwith HTTP 303. The buyer chooses the coin and network on our page. - CoinAvio sends signed webhooks to your
callbackUrl. Verify them, then credit the order oninvoice.paidorinvoice.overpaid.
callbackUrl is your own HTTPS endpoint, set per invoice, so it overrides any webhook URL saved in the merchant's dashboard. Put your merchant or site identifier in its path: your handler then knows which webhook secret to verify with before it trusts the body.
curl -X POST "https://api.coinavio.com/v1/invoices" \
-H "Authorization: Bearer $MERCHANT_API_KEY" \
-H "Content-Type: application/json" \
--data '{
"priceAmount": "25.00",
"orderId": "shop-17:1048",
"description": "Balance top-up #1048",
"successUrl": "https://panel.example.com/addfunds?payment=1048",
"cancelUrl": "https://panel.example.com/addfunds",
"callbackUrl": "https://platform.example.com/coinavio/webhook/shop-17",
"buyerPaysFee": false
}'
# 201 → { "id": "cmgq…", "orderId": "shop-17:1048", "status": "NEW", "testMode": false,
# "price": { "amount": "25", "currency": "USD", "baseAmount": "25", … },
# "checkoutUrl": "https://coinavio.com/pay/cmgq…", "expiresAt": "…", …, "warnings": [] }Swipe within the code to read long lines.
Status mapping
Map data.invoice.status to the states your platform already has. Treat the status in the newest event as the source of truth, and never let an older event undo a completed payment.
| CoinAvio | Your platform | What to do |
|---|---|---|
| PAID, OVERPAID | Completed | Credit price.baseAmount once, on invoice.paid or invoice.overpaid. |
| UNDERPAID | Underpaid / on hold | Do not credit automatically. The merchant reviews it; if they accept the shortfall, invoice.paid follows and the invoice becomes PAID. |
| NEW | Awaiting payment | Nothing to do. The buyer has not paid yet. |
| PENDING, CONFIRMING | Pending | A payment is on its way. Wait for the next webhook. |
| reviewReason AML_SCREENING or AML_REVIEW | On hold (pending) | A received payment is under compliance review. The invoice stays PENDING or CONFIRMING and does not expire; invoice.paid arrives after release. |
| EXPIRED, CANCELLED | Expired / failed | Do not credit. Create a new invoice if the buyer wants to try again. |
These events are informational and must never credit an order: invoice.payment_detected, invoice.payment_confirmed, invoice.payment_orphaned, invoice.review_requested, invoice.discovery_delayed. Do not credit from reviewReason values either.
A payment in the wrong coin, or one that arrives after the invoice closed, is credited to the merchant's CoinAvio balance but does not complete your order. The buyer settles it with the merchant; CoinAvio does not refund automatically.
Amount to credit
- Credit
price.baseAmount: the USD amount you requested. Do it only oninvoice.paidorinvoice.overpaid, with statusPAIDorOVERPAID. - Do not use
price.amount(it includes a buyer-paid fee),pay.amount(the crypto amount) orpaidUsd(a valuation at confirmation time). - Amounts are decimal strings without trailing zeros, for example
"25"for 25.00. Compare them as numbers (in cents), not as strings, and check that the amount matches your payment record. - On an overpayment, credit the requested amount. The surplus stays in the merchant's CoinAvio balance.
Many merchants, many sites
- Use
orderId=<site-id>:<payment-id>, where site-id is a stable identifier of the merchant's site on your platform.orderIdis idempotent per merchant and mode: sending the same value again returns the existing invoice. Without the prefix, two sites of the same merchant that both send payment 1048 collide: the second request gets the first site's invoice back, or an error. - Deduplicate webhooks on the event
id(also sent asX-Webhook-Id) with a unique constraint. Deliveries can arrive more than once and out of order. - Before crediting, check that the invoice
idandorderIdbelong to the merchant whose secret verified the webhook. - Keep test and live payments apart. The same
orderIdcan exist once in each mode.
Fees and currency
Fees. buyerPaysFee: true adds the CoinAvio fee on top, so the merchant receives the full requested amount. If your platform already charges the buyer a fee for this payment method, keep buyerPaysFee off so the buyer is not charged twice. Either way, credit price.baseAmount.
Currency. Invoices are always priced in USD. If the merchant's platform uses another currency, convert to USD with your own rate before creating the invoice, store both amounts with the payment, and credit the stored local amount when the invoice is paid.
Return URLs
- For live invoices, a
successUrlorcancelUrlon a domain the merchant verified in Dashboard → My sites is used as given. Thewww.host and subdomains of a verified domain count. - Any other domain, such as a mirror the merchant never verified, does not fail the request. CoinAvio replaces the URL with the merchant's verified website URL, or drops it (no return link,
nullon the invoice) when none is verified. For a key bound to one site, the replacement is that site's root,https://<site domain>/. - The create response lists each replacement in
warnings. The array is always present: empty when nothing changed, in test mode and on an idempotentorderIdreplay. GET responses and webhooks do not include it. - Log warnings so the merchant can verify the domain. Read
codeandfield; themessageis for people and its wording may change. - A value that is not a valid web address (another scheme, an IP address instead of a domain name, or text that cannot be parsed) still returns HTTP 400. Test invoices have no domain check.
callbackUrlneeds no verification, but it must be HTTPS on a public host, use port 443, 8443, 80 or 8080 and contain no username or password.
"successUrl": "https://shop.example.com/",
"warnings": [
{ "code": "RETURN_URL_REPLACED", "field": "successUrl", "message": "…" }
]Swipe within the code to read long lines.
Webhooks
Every webhook carries X-Webhook-Id, X-Webhook-Event, X-Webhook-Timestamp, X-Webhook-Signature, X-CoinAvio-Mode and X-CoinAvio-Mode-Signature. The body is { id, type, createdAt, data: { invoice } }, where invoice has the same shape as GET /v1/invoices/:id.
- Verify both signatures on the raw request body:
v1 = HMAC_SHA256(webhookSecret, "<t>.<rawBody>")and the mode signature over"<t>.<mode>.<rawBody>", as lowercase hex, with equal timestamps. Reject timestamps more than 5 minutes from your clock. - Webhooks do not follow redirects. Use the final HTTPS URL, including the exact path and trailing slash your server expects.
- Return 2xx within 10 seconds, after your database update is committed. Any other answer is retried: 8 attempts in total over about an hour. The merchant can redeliver an exhausted event from Dashboard → Developers.
<?php
// Same check as the reference verifier in the CoinAvio docs, plus $expectedMode:
// a merchant on a test key only accepts test events, a live key only live events.
function verify_coinavio_webhook($secret, $signature, $rawBody, $mode, $modeSignature, $expectedMode = 'live') {
if (!$secret || $mode !== $expectedMode) return false;
$pattern = '/^t=([0-9]+),v1=([a-fA-F0-9]{64})$/D';
if (!is_string($signature) || !is_string($modeSignature) ||
!preg_match($pattern, $signature, $a) ||
!preg_match($pattern, $modeSignature, $b) || $a[1] !== $b[1]) return false;
if (abs(microtime(true) * 1000 - (float) $a[1]) > 300000) return false;
$legacy = hash_hmac('sha256', $a[1] . '.' . $rawBody, $secret);
$bound = hash_hmac('sha256', $a[1] . '.' . $mode . '.' . $rawBody, $secret);
if (!hash_equals($legacy, strtolower($a[2])) ||
!hash_equals($bound, strtolower($b[2]))) return false;
$event = json_decode($rawBody, true);
return is_array($event) && is_string($event['type'] ?? null) &&
strpos($event['type'], 'invoice.') === 0 &&
($event['data']['invoice']['testMode'] ?? null) === ($expectedMode === 'test');
}
// $tenant = the merchant site identified by your callbackUrl path.
$expectedMode = strpos($tenant['api_key'], 'ck_test_') === 0 ? 'test' : 'live';
$rawBody = file_get_contents('php://input');
if ($rawBody === false || !verify_coinavio_webhook(
$tenant['webhook_secret'], $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '',
$rawBody, $_SERVER['HTTP_X_COINAVIO_MODE'] ?? '',
$_SERVER['HTTP_X_COINAVIO_MODE_SIGNATURE'] ?? '', $expectedMode
)) { http_response_code(400); exit; }
$event = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
// Dedupe on $event['id'], match the order, credit only PAID/OVERPAID, then return 200.
http_response_code(200);Swipe within the code to read long lines.
import { createHmac, timingSafeEqual } from 'node:crypto';
// Same check as the reference verifier in the CoinAvio docs, plus expectedMode:
// a merchant on a test key only accepts test events, a live key only live events.
export function verifyWebhook(secret, signature, rawBody, mode, modeSignature, expectedMode = 'live') {
if (!secret || mode !== expectedMode) return false;
const pattern = /^t=([0-9]+),v1=([a-fA-F0-9]{64})$/;
const legacy = typeof signature === 'string' && pattern.exec(signature);
const signedMode = typeof modeSignature === 'string' && pattern.exec(modeSignature);
if (!legacy || !signedMode || legacy[1] !== signedMode[1]) return false;
const timestampMs = Number(legacy[1]);
if (!Number.isSafeInteger(timestampMs) ||
Math.abs(Date.now() - timestampMs) > 300_000) return false;
const valid = (prefix, hex) => {
const expected = createHmac('sha256', secret).update(prefix).update(rawBody).digest();
return timingSafeEqual(expected, Buffer.from(hex, 'hex'));
};
if (!valid(legacy[1] + '.', legacy[2]) ||
!valid(legacy[1] + '.' + mode + '.', signedMode[2])) return false;
try {
const event = JSON.parse(rawBody.toString('utf8'));
return typeof event.type === 'string' && event.type.startsWith('invoice.') &&
event.data?.invoice?.testMode === (expectedMode === 'test');
} catch { return false; }
}
// rawBody must be the exact bytes (a Buffer), read BEFORE any JSON middleware.
// const expectedMode = tenant.apiKey.startsWith('ck_test_') ? 'test' : 'live';
// if (!verifyWebhook(tenant.webhookSecret, req.headers['x-webhook-signature'], rawBody,
// req.headers['x-coinavio-mode'], req.headers['x-coinavio-mode-signature'], expectedMode)) {
// res.writeHead(400); res.end(); return;
// }Swipe within the code to read long lines.
Fallback: read the invoice
If a buyer returns before the webhook, or a payment stays pending on your side, read the invoice with the merchant's key and apply the same rules as for a webhook. URL-encode the orderId.
GET https://api.coinavio.com/v1/invoices/by-order/shop-17%3A1048
Authorization: Bearer <apiKey>Swipe within the code to read long lines.
Complete PHP example
One file for PHP 7.4 or later: it creates the invoice and redirects the buyer, then verifies webhooks, deduplicates events and credits the balance once. Replace the table names and the session lookup with your own.
<?php
// coinavio.php: one-file CoinAvio integration for a platform that serves many merchants.
// PHP 7.4+ with the curl, json and PDO extensions. Serve it over HTTPS only.
//
// POST coinavio.php?action=pay&tenant=<id> your add-funds form (field: amount, in USD)
// POST coinavio.php?action=webhook&tenant=<id> CoinAvio webhooks (sent to the callbackUrl)
//
// A tenant is one merchant site on your platform. Its API key and webhook secret are the
// two values the merchant pasted into your settings screen. Tables used below:
// tenants(id, domain, api_key, webhook_secret, buyer_pays_fee)
// payments(id, tenant_id, user_id, amount_usd, status, invoice_id) status: pending|completed|hold|failed
// users(id, balance_cents)
// coinavio_events(event_id PRIMARY KEY)
declare(strict_types=1);
const COINAVIO_API = 'https://api.coinavio.com';
const CALLBACK_BASE = 'https://platform.example.com/coinavio.php';
function db(): PDO
{
static $pdo = null;
if ($pdo === null) {
$pdo = new PDO((string) getenv('DB_DSN'), (string) getenv('DB_USER'), (string) getenv('DB_PASS'), [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
}
return $pdo;
}
function load_tenant(string $tenantId): array
{
$stmt = db()->prepare('SELECT id, domain, api_key, webhook_secret, buyer_pays_fee FROM tenants WHERE id = ?');
$stmt->execute([$tenantId]);
$tenant = $stmt->fetch();
if (!$tenant) {
http_response_code(404);
exit;
}
return $tenant;
}
// The key prefix tells you the mode: ck_test_... is test, ck_live_... is live.
function key_mode(string $apiKey): string
{
return strpos($apiKey, 'ck_test_') === 0 ? 'test' : 'live';
}
// "25", "25.5" or "25.50" -> 2500, 2550, 2550. Returns null for anything else.
function usd_cents(string $amount): ?int
{
if (!preg_match('/^([0-9]{1,15})(?:[.]([0-9]+))?$/D', $amount, $m)) {
return null;
}
$fraction = rtrim($m[2] ?? '', '0');
if (strlen($fraction) > 2) {
return null;
}
return (int) $m[1] * 100 + (int) str_pad($fraction, 2, '0');
}
function coinavio_request(string $apiKey, string $method, string $path, ?array $body = null): array
{
$headers = ['Authorization: Bearer ' . $apiKey, 'Accept: application/json'];
$options = [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30];
if ($body !== null) {
$headers[] = 'Content-Type: application/json';
$options[CURLOPT_POSTFIELDS] = json_encode($body, JSON_THROW_ON_ERROR);
}
$options[CURLOPT_HTTPHEADER] = $headers;
$ch = curl_init(COINAVIO_API . $path);
curl_setopt_array($ch, $options);
$raw = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (!is_string($raw)) {
throw new RuntimeException('CoinAvio request failed: ' . curl_error($ch));
}
$data = json_decode($raw, true);
if ($status < 200 || $status >= 300 || !is_array($data)) {
$message = is_array($data) && isset($data['message']) ? json_encode($data['message']) : 'HTTP ' . $status;
throw new RuntimeException('CoinAvio error: ' . $message);
}
return $data;
}
// Same check as the reference verifier in the CoinAvio docs, plus $expectedMode:
// a merchant on a test key only accepts test events, a live key only live events.
function verify_coinavio_webhook($secret, $signature, $rawBody, $mode, $modeSignature, $expectedMode = 'live') {
if (!$secret || $mode !== $expectedMode) return false;
$pattern = '/^t=([0-9]+),v1=([a-fA-F0-9]{64})$/D';
if (!is_string($signature) || !is_string($modeSignature) ||
!preg_match($pattern, $signature, $a) ||
!preg_match($pattern, $modeSignature, $b) || $a[1] !== $b[1]) return false;
if (abs(microtime(true) * 1000 - (float) $a[1]) > 300000) return false;
$legacy = hash_hmac('sha256', $a[1] . '.' . $rawBody, $secret);
$bound = hash_hmac('sha256', $a[1] . '.' . $mode . '.' . $rawBody, $secret);
if (!hash_equals($legacy, strtolower($a[2])) ||
!hash_equals($bound, strtolower($b[2]))) return false;
$event = json_decode($rawBody, true);
return is_array($event) && is_string($event['type'] ?? null) &&
strpos($event['type'], 'invoice.') === 0 &&
($event['data']['invoice']['testMode'] ?? null) === ($expectedMode === 'test');
}
function start_payment(array $tenant): void
{
session_start();
$userId = (int) ($_SESSION['user_id'] ?? 0); // your signed-in user; add your usual CSRF check
$amount = trim((string) ($_POST['amount'] ?? '')); // USD, already converted from the panel currency
$cents = preg_match('/^[0-9]+([.][0-9]{1,2})?$/D', $amount) ? usd_cents($amount) : null;
if ($userId <= 0 || $cents === null || $cents < 50) {
http_response_code(422);
exit('Enter an amount of at least 0.50 USD.');
}
$pdo = db();
$pdo->prepare('INSERT INTO payments (tenant_id, user_id, amount_usd, status) VALUES (?, ?, ?, ?)')
->execute([$tenant['id'], $userId, $amount, 'pending']);
$paymentId = (string) $pdo->lastInsertId();
$returnUrl = 'https://' . $tenant['domain'] . '/addfunds'; // other domains are replaced (see warnings)
$invoice = coinavio_request((string) $tenant['api_key'], 'POST', '/v1/invoices', [
'priceAmount' => $amount,
'orderId' => $tenant['id'] . ':' . $paymentId, // site prefix: no collisions between sites
'description' => 'Balance top-up #' . $paymentId,
'successUrl' => $returnUrl . '?payment=' . $paymentId,
'cancelUrl' => $returnUrl,
'callbackUrl' => CALLBACK_BASE . '?action=webhook&tenant=' . rawurlencode((string) $tenant['id']),
'buyerPaysFee' => (bool) $tenant['buyer_pays_fee'],
]);
$pdo->prepare('UPDATE payments SET invoice_id = ? WHERE id = ?')->execute([$invoice['id'], $paymentId]);
foreach ($invoice['warnings'] ?? [] as $warning) {
// For example RETURN_URL_REPLACED: the panel domain is not verified in the merchant's CoinAvio account.
error_log('CoinAvio ' . $warning['code'] . ' for ' . $warning['field'] . ' (tenant ' . $tenant['id'] . ')');
}
header('Location: ' . $invoice['checkoutUrl'], true, 303);
exit;
}
function handle_webhook(array $tenant): void
{
$rawBody = file_get_contents('php://input'); // the exact bytes, before any JSON parsing
if (!is_string($rawBody) || !verify_coinavio_webhook(
(string) $tenant['webhook_secret'], $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '',
$rawBody, $_SERVER['HTTP_X_COINAVIO_MODE'] ?? '',
$_SERVER['HTTP_X_COINAVIO_MODE_SIGNATURE'] ?? '', key_mode((string) $tenant['api_key'])
)) {
http_response_code(400);
exit;
}
$event = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
$invoice = $event['data']['invoice'];
$prefix = $tenant['id'] . ':';
$orderId = (string) ($invoice['orderId'] ?? '');
if (strpos($orderId, $prefix) !== 0) {
http_response_code(200); // not a top-up created by this file (for example a payment link)
exit;
}
$paymentId = substr($orderId, strlen($prefix));
$pdo = db();
try {
$pdo->beginTransaction();
try {
// Each event id is processed once: the primary key rejects duplicates.
$pdo->prepare('INSERT INTO coinavio_events (event_id) VALUES (?)')->execute([(string) $event['id']]);
} catch (PDOException $e) {
// SQLSTATE class 23 means this event id is already stored; anything else is a real error.
if (substr((string) $e->getCode(), 0, 2) !== '23') {
throw $e;
}
$pdo->rollBack();
http_response_code(200); // duplicate delivery, already handled
exit;
}
$stmt = $pdo->prepare('SELECT id, user_id, amount_usd, invoice_id FROM payments WHERE id = ? AND tenant_id = ?');
$stmt->execute([$paymentId, $tenant['id']]);
$payment = $stmt->fetch();
if ($payment && ($payment['invoice_id'] === null || $payment['invoice_id'] === $invoice['id'])) {
$type = (string) $event['type'];
$status = (string) $invoice['status'];
if (in_array($type, ['invoice.paid', 'invoice.overpaid'], true) && in_array($status, ['PAID', 'OVERPAID'], true)) {
// Credit the USD amount you asked for, never the crypto amount.
$credit = usd_cents((string) $invoice['price']['baseAmount']);
$ok = $credit !== null && $credit === usd_cents((string) $payment['amount_usd']);
// The status guard makes crediting happen at most once, whatever the event order.
$claim = $pdo->prepare("UPDATE payments SET status = ?, invoice_id = ? WHERE id = ? AND status IN ('pending', 'hold')");
$claim->execute([$ok ? 'completed' : 'hold', $invoice['id'], $payment['id']]);
if ($ok && $claim->rowCount() === 1) {
$pdo->prepare('UPDATE users SET balance_cents = balance_cents + ? WHERE id = ?')
->execute([$credit, $payment['user_id']]);
}
} elseif ($type === 'invoice.underpaid') {
// Partial payment: hold for the merchant. If they accept it, invoice.paid follows.
$pdo->prepare("UPDATE payments SET status = 'hold' WHERE id = ? AND status = 'pending'")
->execute([$payment['id']]);
} elseif (in_array($type, ['invoice.expired', 'invoice.cancelled'], true)) {
$pdo->prepare("UPDATE payments SET status = 'failed' WHERE id = ? AND status = 'pending'")
->execute([$payment['id']]);
}
// invoice.pending, invoice.confirming and informational events change nothing here.
}
$pdo->commit();
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
error_log('CoinAvio webhook failed: ' . $e->getMessage());
http_response_code(500); // CoinAvio retries the delivery
exit;
}
http_response_code(200);
}
$tenant = load_tenant((string) ($_GET['tenant'] ?? ''));
$action = (string) ($_GET['action'] ?? '');
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'pay') {
start_payment($tenant);
} elseif ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'webhook') {
handle_webhook($tenant);
} else {
http_response_code(404);
}Swipe within the code to read long lines.
Test mode
After approval, a merchant can create a test key (ck_test_…) in Developers. Its invoices have testMode: true. Pick a coin on the checkout page and press Simulate payment, or call POST /v1/invoices/:id/simulate with the test key. The normal signed webhooks follow, with X-CoinAvio-Mode: test. Test balances are isolated and can never be withdrawn. Do not send real funds to a test invoice.
Limits
| Item | Limit |
|---|---|
| Minimum invoice | 0.50 USD (the amount the buyer is charged, after any buyer-paid fee) |
| priceAmount | JSON string, digits with up to 2 decimals ("49.99", "25"), at most 21 characters, below 10^18 |
| orderId | String, up to 128 characters, no character restrictions. Idempotent per merchant and mode. URL-encode it in by-order lookups. |
| description | Up to 500 characters |
| successUrl, cancelUrl, callbackUrl | Up to 2048 characters each |
| Payment window | 60 minutes by default; expiresInMinutes accepts 5–1440. The crypto amount is fixed from coin selection until expiry. |
| Grace period | 60 minutes after expiresAt for PENDING/CONFIRMING invoices (paymentDeadline), for confirmations and top-ups |
| Rate limits | 300 requests per minute per IP for the whole API; POST /v1/invoices 60 per minute per IP. Excess returns 429. |
| Request body | 64 KiB |
| List page size | limit 1–100, default 20 |
| Webhook response | 2xx within 10 seconds; only the first 4 KB of your response is read |
| Webhook retries | 8 attempts: the first delivery, then waits of 30 s, 1, 2, 4, 8, 16 and 32 min (63.5 min of waits in total) |
| Signature timestamp | Reject when t differs from your clock by more than 5 minutes |
Errors, statuses and every field are described in the developer documentation, the Partner API PDF, the Markdown reference and the OpenAPI spec.
Logo and support
Need our logo for your payment-method list? Email support@coinavio.com.
Questions about your integration: write to support@coinavio.com or use the partnership contact form.
Contact us