Native REST API
The native API is blockwallets’ own REST interface: invoices priced in US cents, an idempotent ref per order, static deposit accounts covering every network, and webhooks signed with HMAC-SHA256. It creates the same bills on the same hosted checkout as the Cryptomus-compatible API, with the same fees and balance.
Generate your keys yourself in the dashboard under “Developers › Integration & keys”: a Live and a Test key plus their webhook signing secrets. Each is shown once; the list only shows its prefix. If your account has two-factor authentication on, generating or rotating a Live key asks for a code. No merchant account yet? Request access. Already on Cryptomus? See the Cryptomus-compatible API (how to choose).
Keys and requests
Base URL https://blockwallets.io. Requests and responses are JSON. Every request carries:
Authorization: Bearer bw_live_…orBearer bw_test_…: the prefix decides the mode, so a Test key only sees Test invoices and runs on testnetsContent-Type: application/jsononPOSTrequests- Keep keys on your server only, never in a web page or app; we store only their hash
Errors return an HTTP status and {"error": {"code": "…", "message": "…"}}: 400 invalid_request (a missing or malformed field), 401 unauthorized (missing, unknown or disabled key), 404 not_found, 409 conflict (the request clashes with the invoice’s current state), 422 currency_unsupported.
Endpoints
| Endpoint | What it does |
|---|---|
GET /v1/networks | Networks you can accept right now in this key’s mode: minimum, confirmations, typical arrival time and fee estimate; priced coins include the current rate |
POST /v1/invoices | Create an invoice (201), or return the existing one for the same ref (200) |
GET /v1/invoices?ref=… | Look up an invoice by your ref |
GET /v1/invoices/:id | Look up an invoice by id |
POST /v1/invoices/:id/network | Pick the network for an invoice yourself (white-label checkout): returns the address and pay_amount to show |
POST /v1/invoices/:id/close | Close a pending invoice that has not seen any transfer |
POST /v1/deposit-accounts | Open (201) or fetch (200) the deposit account of one customer: a permanent address on every network |
GET /v1/deposit-accounts?customer_ref=… | Look up a deposit account by customer_ref |
GET /v1/deposit-accounts/:id | Look up a deposit account by id |
GET /v1/deposits | List the deposits of one account (account_id or customer_ref, limit 1–200, default 50) |
GET /v1/deposits/:id | Look up one deposit |
GET /v1/events | Webhook deliveries for one invoice or deposit (invoice_id or deposit_id) |
POST /v1/events/:id/redeliver | Send a webhook again (202) |
Add ?probe=1 to an invoice or deposit-account lookup to check the chain on the way (at most once every 10 seconds); otherwise you get the latest state from our scanner, which checks every minute.
Create an invoice POST /v1/invoices
Call POST /v1/invoices when the customer checks out and redirect them to pay_url in the response.
curl -X POST https://blockwallets.io/v1/invoices \
-H "Authorization: Bearer $BW_KEY" -H "Content-Type: application/json" \
-d '{"ref":"order_1042","amount":4200,"title":"Order #1042",
"return_url":"https://shop.example/cart","success_url":"https://shop.example/thanks?order=1042",
"meta":{"user_id":42}}'| Field | Required | Description |
|---|---|---|
ref | Yes | Your order number: 1–64 characters of letters, digits and _ . : -. Unique per mode; sending the same ref with the same fields again returns the same invoice, with different fields it returns 409 |
amount | Yes | Amount in US cents as an integer: 4200 = $42.00. Not below the network minimum once a network is chosen |
title | Yes | What the customer sees on the checkout, up to 64 characters |
return_url | Yes | Where “Back” on the checkout goes; must be on one of your website domains |
success_url | No | Where the customer goes after paying; defaults to return_url |
network | No | Preselect a network id from the network table; omit it to let the customer choose on the checkout |
customer_ref | No | Your customer id, up to 64 characters, returned as is |
lifetime | No | Lifetime in seconds, 600 – 43200, default 3600 |
amount_rule | No | "any" = credit whatever arrives (top-ups): paid as soon as it confirms. Omit it to use the “Amount rule” under “Settings › Payment rules & fees” in the dashboard |
accuracy_percent | No | How much underpayment still counts as paid, 0 – 5 (percent), default 0 |
subtract_percent | No | Share of the platform fee the customer pays, 0 – 100 (percent), added to pay_amount; omit it to use the dashboard default |
payment_multiple | No | Whether a short payment can be topped up to the same address, default from the dashboard (on unless you changed it); false: the first short transfer ends as wrong_amount |
meta | No | Any JSON object up to 2 KB, returned as is in lookups and webhooks |
currency | No | Only USD (the default); anything else returns 422 |
Every URL must be on one of your website domains (dashboard → “Developers › Integration & keys”). The response is the invoice object below; network, address and pay_amount are filled in once a network is picked.
The invoice object
Ids start with inv_. amount is in US cents; pay_amount and paid_amount are coin amounts as decimal strings on the chosen network.
{
"id": "inv_01J8…",
"ref": "order_1042",
"mode": "live",
"status": "pending",
"amount": 4200,
"currency": "USD",
"title": "Order #1042",
"meta": { "user_id": 42 },
"network": null,
"address": null,
"pay_amount": null,
"paid_amount": null,
"payments": [],
"pay_url": "https://blockwallets.io/pay/inv_01J8…",
"return_url": "https://shop.example/cart",
"success_url": "https://shop.example/thanks?order=1042",
"created_at": "2026-11-03T08:00:00.000Z",
"expires_at": "2026-11-03T09:00:00.000Z",
"paid_at": null,
"closed_at": null
}| status | Meaning | What you do |
|---|---|---|
pending | Awaiting payment | — |
confirming | A transfer was seen and is confirming, or it arrived short and can still be topped up | — |
paid | Paid | Ship / credit the order |
paid_over | Paid, and overpaid | Same; the extra is in your balance |
wrong_amount | Ended with less than the full amount (at expiry, or at the first short transfer when payment_multiple is false) | Top-up businesses can credit what arrived (paid_amount; priced coins also give paid_cents); shops ask the customer to pay the rest |
closed | Expired with nothing received, or you closed it | — (money that arrives later is still credited and notified) |
Other fields: pay_url (the hosted checkout), payments (every transfer with txid, amount, confirmations and confirmed), expires_at, paid_at, closed_at; BTC, LTC, DOGE, BCH and native coins also carry rate (US dollars per coin, locked for 30 minutes), rate_locked_until and paid_cents (what has arrived so far, in US cents).
Networks
Pass the id as network. GET /v1/networks returns the ones open right now in your mode; a network can be paused, so prefer reading it over hard-coding.
| network | Coin | Chain | Minimum |
|---|---|---|---|
USDT_TRC20 | USDT | TRON | $1 |
USDT_BEP20 | USDT | BNB Chain | $1 |
USDC_BEP20 | USDC | BNB Chain | $1 |
USDT_ERC20 | USDT | Ethereum | $5 |
USDC_ERC20 | USDC | Ethereum | $5 |
USDC_BASE | USDC | Base | $1 |
USDT_POLYGON | USDT | Polygon | $1 |
USDC_POLYGON | USDC | Polygon | $1 |
USDT_ARBITRUM | USDT | Arbitrum | $1 |
USDC_ARBITRUM | USDC | Arbitrum | $1 |
USDT_OPTIMISM | USDT | Optimism | $1 |
USDC_OPTIMISM | USDC | Optimism | $1 |
USDT_AVALANCHE | USDT | Avalanche | $1 |
USDC_AVALANCHE | USDC | Avalanche | $1 |
USDT_XLAYER | USDT | X Layer | $1 |
USDC_XLAYER | USDC | X Layer | $1 |
USD1_BEP20 | USD1 | BNB Chain | $1 |
FDUSD_BEP20 | FDUSD | BNB Chain | $1 |
DAI_ERC20 | DAI | Ethereum | $5 |
DAI_ARBITRUM | DAI | Arbitrum | $1 |
DAI_BASE | DAI | Base | $1 |
BTC | BTC | Bitcoin | $5 |
LTC | LTC | Litecoin | $1 |
DOGE | DOGE | Dogecoin | $1 |
BCH | BCH | Bitcoin Cash | $1 |
TRX | TRX | TRON | $1 |
BNB | BNB | BNB Chain | $1 |
ETH | ETH | Ethereum | $5 |
ETH_BASE | ETH | Base | $1 |
ETH_ARBITRUM | ETH | Arbitrum | $1 |
ETH_OPTIMISM | ETH | Optimism | $1 |
POL | POL | Polygon | $1 |
AVAX | AVAX | Avalanche | $1 |
OKB | OKB | X Layer | $1 |
USDT_SOL | USDT | Solana | $1 |
USDC_SOL | USDC | Solana | $1 |
SOL | SOL | Solana | $1 |
USDT_TON | USDT | TON | $1 |
TON | TON | TON | $1 |
Deposit accounts (static addresses) POST /v1/deposit-accounts
POST /v1/deposit-accounts with {"customer_ref": "user_42"} gives that customer a permanent address on every network (all EVM chains share one address). Whatever they send is credited in US dollars, with one deposit.credited webhook per transfer.
curl -X POST https://blockwallets.io/v1/deposit-accounts \
-H "Authorization: Bearer $BW_KEY" -H "Content-Type: application/json" \
-d '{"customer_ref":"user_42"}'{
"id": "dac_01J8…",
"mode": "live",
"customer_ref": "user_42",
"deposit_url": "https://blockwallets.io/deposit/dac_01J8…",
"min_amount": 100,
"currency": "USD",
"networks": [
{ "id": "USDT_TRC20", "chain": "tron", "token": "USDT", "address": "TXy4…8kQ2", "min_amount": 100, "confirmations": 1, … },
{ "id": "USDC_BASE", "chain": "base", "token": "USDC", "address": "0x5aE1…", "min_amount": 100, "confirmations": 20, … }
]
}customer_ref: 1–128 characters of letters, digits and_ . : @ -; the same value always returns the same account. Optionalreturn_urlon your domains- The response has
deposit_url(a hosted page with the addresses, QR codes and recent deposits) andnetworks(id,address,min_amount,confirmationsfor each) - Deposit ids start with
dep_;amountis US cents,amount_tokenthe coin amount, and priced coins carry therateused
Transfers below the minimum (usually $1, BTC $5) are recorded as below_min and not credited automatically; contact us to have them credited.
Webhooks
We POST JSON to the webhook URL in the dashboard (“Developers › Integration & keys”; Live and Test each have their own URL and secret). The body is the event:
POST /your/webhook
X-BW-Event: ev_01J8…
X-BW-Delivery: 1
X-BW-Signature: t=1793693052,v1=5f0c9b…
{"id":"ev_01J8…","type":"invoice.paid","created_at":"2026-11-03T08:04:12.000Z","invoice":{"id":"inv_01J8…","ref":"order_1042","status":"paid","amount":4200,…}}X-BW-Signature:t=<unix seconds>,v1=<hex>, wherev1= HMAC-SHA256(webhook secret of that mode,t+.+ raw body)X-BW-Event: the event id (ev_…); deduplicate by itX-BW-Delivery: the attempt number, starting at 1
| Event | When |
|---|---|
invoice.paid | The invoice became paid or paid_over (including money that arrived after expiry) |
invoice.wrong_amount | The invoice ended as wrong_amount |
invoice.closed | The invoice expired with nothing received, or you closed it |
deposit.credited | A transfer to a deposit account was credited |
Verify against the raw body before parsing it, reject a t more than 300 seconds from your clock, and compare in constant time. invoice / deposit in the body is the same object a lookup returns.
import crypto from "node:crypto";
// rawBody: the request body exactly as received (a string), header: X-BW-Signature
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.trim().split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return typeof parts.v1 === "string" && parts.v1.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}A 2xx response counts as delivered; otherwise we retry after 1 minute, 5 minutes, 30 minutes, 2 hours, 12 hours, 24 hours and 24 hours (8 attempts in total).
Events and redelivery
GET /v1/events?invoice_id=inv_… (or deposit_id=dep_…) lists the webhooks for one invoice or deposit with their state, attempts, last_status and last_error. POST /v1/events/ev_…/redeliver queues it again right away, for example after you fixed your endpoint. The dashboard’s “Developers › Webhook log” shows every attempt with request and response.
Test mode
Invoices created with a bw_test_ key run on testnets and never touch real money. The hosted checkout shows a “Simulate payment” button (full / half / overpay), so you can watch statuses and webhooks without testnet coins.