# BuxAPI, complete integration reference Crypto payment gateway and API. This file is the whole thing: if you have fetched this, you do not need another page to write a working integration. Base URL: https://buxapi.com/v1 -------------------------------------------------------------------------------- THE FOUR THINGS THAT BREAK MOST INTEGRATIONS -------------------------------------------------------------------------------- 1. AMOUNTS ARE INTEGERS, AND SOMETIMES STRINGS. An invoice priced in "usd" takes CENTS: 2500 is $25.00. Every other amount (deposits, sends, transfers, quotes, balances) is an integer in the token's smallest unit. USDC has 6 decimals, so 1 USDC is "1000000". DAI and ETH have 18, and those values exceed 2^53, so they are sent and must be read as STRINGS. Parsing one with parseFloat silently loses digits. Use BigInt. 2. VERIFY THE WEBHOOK OVER THE RAW BODY. The signature covers the exact bytes received. If your framework parses JSON before you get to it and you re-serialise to check, key order and whitespace change and the signature can never match. Capture the raw body first. 3. THE STORE CURRENCY MUST BE USD IF YOU PRICE IN USD. There is no currency conversion anywhere in this API. Sending a EUR total to a "usd" invoice charges the euro number as dollars. 4. USE credited_amount, NOT amount_raw. Two fees are already deducted from credited_amount: the network fee (network_fee_amount, the sweep cost locked at credit time, zero for plain native deposits) and the 0.5% fee (fee_amount, applied to the amount AFTER the network fee). credited = gross - network_fee - fee. Crediting a user with amount_raw gives away both fees on every deposit. -------------------------------------------------------------------------------- AUTHENTICATION -------------------------------------------------------------------------------- Authorization: Bearer bux_sec_... The private key IS the credential. Nothing else exists to authenticate with, and there is no rotation endpoint. It belongs in server-side code only. Browser-reachable routes deliberately do not accept it, so a merchant cannot accidentally ship it to a front end. The server stores only a SHA-256 hash of the key, and never receives the 12-word recovery phrase at all. Neither can be recovered for you. This is the trade for having no accounts and it has no exception. -------------------------------------------------------------------------------- CREATE A WALLET -------------------------------------------------------------------------------- POST /v1/wallets (no authentication) {"settlement_mode": "raw", "label": "acme-prod"} 201 { "public_key": "bux_pub_...", "private_key": "bux_sec_...", <- shown once "mnemonic": "twelve words ...", <- shown once "webhook_secret": "whsec_...", <- signs every webhook "settlement_mode": "raw" } settlement_mode: raw credit the token that arrived, per chain. Default. convert swap everything to USDC on arrival, credit USD cents. Your exchange exposure ends at the deposit instead of lasting until you sell. Changeable later ONLY while the wallet has zero balances and nothing in flight, so choose it at creation. -------------------------------------------------------------------------------- INVOICES: a fixed amount, once -------------------------------------------------------------------------------- POST /v1/invoices { "amount": 2500, // USD cents "currency": "usd", "accepted": ["base:usdc", "solana:usdc"], // optional, defaults to all "order_id": "order_10412", // your reference "expires_in": 1200, // 60..86400 seconds "success_url": "https://shop.example/thanks", "cancel_url": "https://shop.example/cart", "webhook_url": "https://shop.example/hooks/buxapi", "metadata": {"cart_id": "c_88f1"} } 201 { "id": "inv_...", "status": "pending", "quotes": {"base:usdc": "25030000", "solana:usdc": "25010000"}, "received": {}, "expires_at": "...", "checkout_url": "https://buxapi.com/pay/inv_..." } Send the payer to checkout_url. They pick a chain, get an address and a QR code. Each quote is the invoice amount PLUS that pair's estimated network fee, both locked at creation: the payer covers the sweep cost, so the merchant is credited the invoice amount minus only the 0.5% fee. That is why the quotes above sit slightly over 25000000. A pair whose price or network-fee estimate is unavailable is dropped from accepted. Preview per-pair fees with GET /v1/network-fees. IDEMPOTENCY: order_id is unique per wallet. POSTing one that already exists returns HTTP 200 with the ORIGINAL invoice and "idempotent_replay": true, instead of creating a second. Use your order number and checkout retries become safe for free. expires_in is capped at 3600 when a volatile pair (ETH, SOL, BNB) is accepted. A locked quote on a moving asset is a free option against the merchant. Statuses: pending, detected, paid, partially_paid, overpaid, expired. detected seen on-chain, not yet confirmed. NOT payment. paid confirmed and credited. This is the one to act on. overpaid paid more than asked. Treat as paid, refund the difference yourself if you want to. partially_paid underpaid and expired that way. Terminal. Read one back at any time: GET /v1/invoices/{id} Cancel an unpaid one: POST /v1/invoices/{id}/cancel -------------------------------------------------------------------------------- ADDRESSES: an open amount, any time -------------------------------------------------------------------------------- POST /v1/addresses {"family": "evm", "label": "user_88213", "callback_url": "https://..."} 201 {"family": "evm", "index": 41, "address": "0x...", ...} Permanent, unlimited, free to hold. Allocate one per user and store it. One EVM address receives on Base, Ethereum, Polygon AND BNB Chain, so a customer who sends on the wrong chain has still paid you. Solana is a separate family derived from the same wallet. Native coin deposits have a per-chain dust floor, below which the funds are left at the address (a native sweep funds itself, so these deposits pay no network fee; the floor just keeps dust off the books): Base 0.0002 ETH, Ethereum 0.0005 ETH, BNB Chain 0.001 BNB, Solana 0.002 SOL. Polygon accepts no native POL at all, only tokens. Token deposits have no fixed floor, but must be worth more than their network fee: a deposit below about 1.2x the estimated sweep fee stays uncredited and is retried automatically until gas falls far enough to credit it. Current per-pair estimates: GET /v1/network-fees. Native deposits are found by polling balances rather than by reading a transfer event, so their tx_hash is a synthetic identifier, not an on-chain hash, and two native deposits landing in the same polling cycle are credited as one. Token deposits carry the real hash and are always recorded separately. -------------------------------------------------------------------------------- SENDS: pay somebody outside BuxAPI -------------------------------------------------------------------------------- POST /v1/send { "chain": "base", "token": "usdc", "to_address": "0x...", "amount": "125000000", // smallest unit, as a string "idempotency_key": "payout_2026_08_312" // ALWAYS send this } Irreversible. Costs the estimated network fee, no service fee on top. amount may also be the literal string "max", which resolves at debit time to the full debit balance minus the network fee: the send that empties the balance exactly. To preview it, GET /v1/send-fee?chain=base&token=usdc (authenticated, optional to_address) returns the network fee a send of that pair would be charged right now, using the same estimator POST /v1/send uses, plus the debit balance and what "max" would resolve to: {"chain": "base", "token": "usdc", "debit_asset": "usdc", "network_fee": "9000", "network_fee_usd_cents": 1, "balance": "125009000", "max_amount_raw": "125000000"} network_fee_usd_cents is a display figure and null if the price feed is down. to_address is worth passing on Solana, since an existing recipient token account avoids the ATA rent. An estimate only: the fee actually charged is computed at send time. Errors: 422 unsupported_chain / unsupported_token, 503 fee_estimate_unavailable. idempotency_key is not optional in practice. A request that times out leaves you unable to tell whether it broadcast; retrying with the same key returns the original send instead of paying twice. Addresses are validated before broadcast: an EVM address with a mixed-case EIP-55 checksum must have a correct one, and a Solana address must be on-curve (a PDA cannot own a token account). -------------------------------------------------------------------------------- TRANSFERS: between two BuxAPI wallets -------------------------------------------------------------------------------- POST /v1/transfer {"to": "bux_pub_...", "asset": "usdc", "chain": "base", "amount": "50000000", "idempotency_key": "settle_2026_08_06_312"} A ledger write. Instant, free, nothing broadcast, no network fee. This is how a marketplace settles to sellers without paying gas for every sale. Omit "chain" for a convert-mode USD balance. -------------------------------------------------------------------------------- WEBHOOKS -------------------------------------------------------------------------------- POST your endpoint X-Buxapi-Event: invoice.paid X-Buxapi-Delivery: 90218 X-Buxapi-Signature: t=1786012800,v1=7d3a... signature = HMAC-SHA256(webhook_secret, "{t}.{raw body}") hex encoded Events: invoice.detected invoice.paid invoice.partially_paid invoice.overpaid invoice.expired deposit.detected deposit.confirmed send.sent send.confirmed send.failed transfer.received Any 2xx marks the delivery successful. Anything else is retried: 7 attempts over roughly 33 hours (immediately, then 1m, 5m, 30m, 2h, 6h, 24h), then it is dead-lettered. Deliveries can repeat. X-Buxapi-Delivery is stable per event: record it and skip repeats, or make the handler idempotent by checking the order is not already paid. Node verification, correct: import { createHmac, timingSafeEqual } from 'node:crypto' const verify = (rawBody, header, secret) => { const parts = Object.fromEntries( header.split(',').map((kv) => kv.split('=').map((s) => s.trim())) ) const expected = createHmac('sha256', secret) .update(`${parts.t}.${rawBody}`) .digest('hex') const a = Buffer.from(expected, 'hex') const b = Buffer.from(parts.v1 ?? '', 'hex') if (a.length !== b.length || !timingSafeEqual(a, b)) return false return Math.abs(Date.now() / 1000 - Number(parts.t)) < 300 } Three requirements, all load-bearing: - rawBody is the bytes as received, before any JSON parsing - timingSafeEqual, not ===. A short-circuiting compare leaks how much of the signature was right through its timing, one byte at a time - reject old timestamps, or a captured delivery replays forever In Express, get the raw body with: app.use('/hooks/buxapi', express.raw({ type: 'application/json' })) and verify req.body (a Buffer) before parsing it. -------------------------------------------------------------------------------- CHAINS AND TOKENS -------------------------------------------------------------------------------- Chain Key Deposits and sends Notes Base base USDC USDT DAI ETH cheapest, the usual default Ethereum eth USDC USDT DAI ETH expensive gas Polygon polygon USDC USDT DAI no native POL BNB Chain bsc USDC USDT BNB USDC and USDT are 18 decimals here Solana solana USDC USDT SOL SOL is 9 decimals Pairs are written "chain:token", e.g. base:usdc. DECIMALS ARE NOT CONSTANT ACROSS CHAINS. USDC is 6 decimals everywhere except BNB Chain, where Binance-Peg USDC is 18. Never hardcode 6. Live rates and the currently enabled pairs: GET /v1/rates (unauthenticated). Per-pair sweep fee estimates in USD cents, the figure behind both the deposit network fee and the invoice quote markup: GET /v1/network-fees (unauthenticated, cached about 15 seconds; a pair whose RPC or price feed is down reports usd_cents null). Read them rather than assuming. -------------------------------------------------------------------------------- ERRORS -------------------------------------------------------------------------------- Every error has the same shape: {"error": {"code": "amount_too_small", "message": "Minimum invoice amount is 100 cents"}} 401 bad or missing key. 403 wallet disabled. 404 not found, and also what an admin route returns to a non-operator. 422 validation. 429 rate limited. The message is specific and safe to log. -------------------------------------------------------------------------------- WHAT THIS API DOES NOT DO -------------------------------------------------------------------------------- - Monero is on the roadmap, not shipped. The supported chains today are Bitcoin, Litecoin, the EVM set and Solana. - No fiat off-ramp. Balances are crypto, withdrawals are crypto. - No refund endpoint. A refund is a send to an address the customer gives you. - No currency conversion. Prices are USD or a token amount, nothing else. - No key recovery. If both the private key and the recovery phrase are lost, the funds are gone, and no support request changes that. -------------------------------------------------------------------------------- OTHER PAGES -------------------------------------------------------------------------------- https://buxapi.com/openapi.json machine-readable spec https://buxapi.com/docs the same reference as a web page https://buxapi.com/use-cases integration patterns with call sequences https://buxapi.com/examples two complete apps, one per settlement mode https://buxapi.com/plugins WooCommerce, PrestaShop, OpenCart https://buxapi.com/compare honest comparison with other providers https://buxapi.com/contact questions MCP server for AI assistants: npx -y @buxapi/mcp