blockwalletsDocs中文Dashboard

Docs

blockwallets lets you accept USDT and USDC, plus BTC, LTC and native coins (credited in US dollars at a locked rate): the customer pays, we confirm it on chain, then we notify you. The API, signatures and webhooks are the same as Cryptomus — if you have integrated Cryptomus, change the domain and you are done. How it works comes first, then the API.

Two ways to get paid

Both accept any amount: fixed-price orders and top-ups where the customer types the amount. Once paid, you ship or credit the customer’s balance. The only difference is whether every payment gets a fresh address.

Recommended · most integrationsBills

The customer picks a product or enters an amount on your page, you create a bill, and they send the amount shown on the payment page. Every bill has a new address and a countdown; when it arrives we notify you with the amount actually received.

POST /v1/payment → webhook type: payment

OptionalStatic wallets

No amount up front: each customer gets a permanent address per network to save in their wallet and send to any time. Every transfer triggers its own webhook.

POST /v1/wallet → webhook type: wallet

When in doubt, use bills. Without code, the dashboard’s payment links are bills created for you. Not a merchant yet? Request access on the home page; once approved, sign in to the merchant dashboard and try “Test a payment” with a small amount first (the money goes to your own balance).

How a bill flows

A payment involves four parties: the customer, your site, blockwallets and the blockchain. The diagram runs top to bottom in time; the right column is the bill’s status at each step.

① Create② Pay③ Confirm④ Notify⑤ Wrap up Customerbrowser + walletYour sitepages + serverblockwalletscheckout + scannerBlockchainTRON + 11 more Bill statusstatus 1Clicks “Top up”2POST /v1/payment3uuid + urlcheck4Redirect to url5Picks coin + network6Own address + amount7Sends to that address8Scan every minute9Seen, awaiting confirmationsconfirm_check10Second node cross-checkpaid11POST url_callback12Responds 2xx13Verify, ship / credit14Back to url_success15Credited to balance

The diagram is wide — scroll sideways.

15 steps, top to bottom in time order. Press “Play” to watch, or tap any step for details.

In short, five steps:

  1. You create a bill (a payment link does it for you, or your server calls /v1/payment), get the payment page url, and redirect the customer there.
  2. On the payment page the customer picks a coin and a network, gets a dedicated address and amount, and pays from a wallet or exchange.
  3. We see the transfer on chain, wait for enough confirmations, then cross-check it with a second node provider (so a node can’t fake it).
  4. The bill becomes paid and we immediately POST a signed webhook to your url_callback; a few seconds later the payment page sends the customer back to your url_success.
  5. The money is credited to your balance; withdraw to your own wallet from the merchant dashboard.

Bill status

statusMeaningWhat you do
checkAwaiting payment—
confirm_checkTransfer seen, waiting for confirmations—
wrong_amount_waitingReceived but short; the customer can top up to the same address (when is_payment_multiple is true; notified once)— (you get paid once it is topped up)
paidPaidShip / credit by payment_amount_usd
paid_overPaid, and overpaidSame (payment_amount_usd is the amount received)
wrong_amountExpired with less than the full amount (with is_payment_multiple false: the first short transfer)Top-up businesses can credit the received payment_amount_usd; shops ask the customer to pay the rest
cancelExpired with nothing received— (money that arrives later is still credited and notified)

When is_final is true the status will not change again (except for late payments).

Merchant dashboard → “Payment links” → enter a description and amount (fix a network or let the customer choose) → create. Each link has a URL and a QR code.

API integration

1. Credentials and signing

Merchant dashboard → “Integration & developers” → generate a merchant (UUID) and an API key. There is one set for Live and one for Test: Live takes real money, Test runs on testnets. Also add your website domains: url_callback, url_return and url_success must be on them. The API key is shown only once — keep it safe and only on your server.

Base URL https://blockwallets.io. Every endpoint is POST + JSON, with these headers:

Success returns {"state": 0, "result": {…}}; failure returns {"state": 1, "message": "…"} (see Errors).

The key endpoints below have a “Try it” panel: enter your Test merchant and API key, and the page signs the request in your browser, calls the endpoint and shows the full request and response (credentials stay in the page’s memory). When a signature doesn’t match, compare it step by step with the signature checker.

Node.js
import crypto from "node:crypto";

async function call(path, data) {
  const body = JSON.stringify(data);
  const sign = crypto.createHash("md5").update(Buffer.from(body).toString("base64") + process.env.BW_API_KEY).digest("hex");
  const res = await fetch("https://blockwallets.io" + path, {
    method: "POST",
    headers: { merchant: process.env.BW_MERCHANT, sign, "Content-Type": "application/json" },
    body,
  });
  return res.json();
}
PHP
function bw_call(string $path, array $data): array {
    $body = json_encode($data, JSON_UNESCAPED_UNICODE);
    $sign = md5(base64_encode($body) . getenv('BW_API_KEY'));
    $ch = curl_init('https://blockwallets.io' . $path);
    curl_setopt_array($ch, [
        CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body, CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => ['merchant: ' . getenv('BW_MERCHANT'), 'sign: ' . $sign, 'Content-Type: application/json'],
    ]);
    return json_decode(curl_exec($ch), true);
}
Python
import base64, hashlib, json, os, requests

def bw_call(path: str, data: dict) -> dict:
    body = json.dumps(data, separators=(",", ":"), ensure_ascii=False)
    sign = hashlib.md5((base64.b64encode(body.encode()).decode() + os.environ["BW_API_KEY"]).encode()).hexdigest()
    headers = {"merchant": os.environ["BW_MERCHANT"], "sign": sign, "Content-Type": "application/json"}
    return requests.post("https://blockwallets.io" + path, data=body.encode(), headers=headers).json()

2. Create a bill POST /v1/payment

When the customer places an order, or enters an amount on your top-up page, call https://blockwallets.io/v1/payment and redirect them to the returned result.url.

json
{
  "amount": "42.00",
  "currency": "USD",
  "order_id": "order_1042",
  "url_callback": "https://your-site.com/webhooks/crypto",
  "url_return": "https://your-site.com/cart",
  "url_success": "https://your-site.com/thanks?order=1042"
}
ParameterRequiredDescription
amountYesAmount as a string, up to two decimals: "42.00". Your price or a number the customer entered; not below the chosen network’s minimum (see networks)
currencyYesUSD, USDT or USDC, all 1:1
order_idYesYour order number: letters, digits, -, _, ≤ 100 characters. Calling again with the same order_id returns the existing bill (no duplicate orders)
url_callbackNoWebhook URL. Omit it and no webhook is sent
url_returnNoWhere “Back” goes before paying
url_successNoWhere the customer goes after paying
networkNoPreselect a network: tron / bsc / eth / base / polygon / arbitrum / optimism / avalanche / xlayer / btc / ltc / sol; omit it to let the customer choose on the payment page
to_currencyNoCoin on that network: USDT / USDC; on btc / ltc it is BTC / LTC (optional)
lifetimeNoBill lifetime in seconds, 300 – 43200, default 3600; anything under 600 counts as 600
accuracy_payment_percentNoHow much underpayment still counts as paid, 0 – 5, default 0. 100 = accept whatever arrives: paid as soon as it confirms (paid_over if more), credit by payment_amount_usd in the webhook — good for top-ups. If omitted, the dashboard’s “Amount rule” applies: “credit what arrives” means 100
subtractNoShare of the platform fee paid by the customer, 0 – 100, added to payer_amount (the platform fee is 1%, lower at higher volume; at 1%, with 100 a $100 bill costs the customer $101.00; see pricing). If omitted, the dashboard’s “Fees and underpayment” setting applies (default 0)
is_payment_multipleNoWhether a short payment can be topped up, default true: status wrong_amount_waiting, paying the rest to the same address makes it paid (if under 30 minutes remain at the first short payment, the deadline moves to 30 minutes later). false: the first short transfer makes it wrong_amount. If omitted, the dashboard setting applies
additional_dataNoAny string (≤ 255), returned as-is in queries and webhooks
json
{
  "state": 0,
  "result": {
    "uuid": "6f1c2a7e-…",
    "order_id": "order_1042",
    "amount": "42.00",
    "currency": "USD",
    "network": null,
    "address": null,
    "payer_amount": null,
    "payment_amount": null,
    "url": "https://blockwallets.io/pay/inv_01J8…",
    "expired_at": 1760965200,
    "status": "check",
    "is_final": false
  }
}

network, address and payer_amount are filled in once the customer picks a network.

3. Query bills

Webhooks are the primary channel; queries are the fallback (for example if your server was down and missed a webhook).

4. Static wallets POST /v1/wallet

Opens a permanent address for one customer on one network. The same order_id (usually your user id) + network always gives the same address; all EVM chains (BNB Chain, Ethereum, Base, Polygon, Arbitrum, Optimism, Avalanche, X Layer, Solana) share one address, while TRON, BTC and LTC each have their own. Whatever the customer sends is credited, with a webhook per transfer; BTC / LTC transfers are converted to US dollars at the rate when each was seen (payment_amount_usd).

json
{
  "order_id": "user_42",
  "currency": "USDT",
  "network": "tron",
  "url_callback": "https://your-site.com/webhooks/crypto"
}
json
{
  "state": 0,
  "result": {
    "wallet_uuid": "0b9d…",
    "uuid": "5e2f…",
    "order_id": "user_42",
    "currency": "USDT",
    "network": "tron",
    "address": "TXy4…8kQ2",
    "url": "https://blockwallets.io/deposit/dac_01J8…"
  }
}

result.url is a hosted deposit page (address + QR code + recent deposits) you can hand to the customer. Transfers under $1 are not credited automatically; handle them in the dashboard. Blocking addresses (/v1/wallet/block-address) is not supported yet.

Webhooks

When a bill’s status changes (paid, paid_over, wrong_amount_waiting, wrong_amount, cancel) and for every transfer to a static wallet, we POST JSON to url_callback. 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. You can also resend from the dashboard or with /v1/payment/resend.

json
{
  "type": "payment",
  "uuid": "6f1c2a7e-…",
  "order_id": "order_1042",
  "amount": "42.00",
  "payment_amount": "42",
  "payment_amount_usd": "42.00",
  "merchant_amount": "41.832",
  "commission": "0.168",
  "is_final": true,
  "status": "paid",
  "from": "TQ8m…",
  "wallet_address_uuid": null,
  "network": "tron",
  "currency": "USD",
  "payer_currency": "USDT",
  "payer_amount": "42",
  "payer_amount_exchange_rate": "1",
  "additional_data": null,
  "txid": "9c1e…",
  "sign": "5f0c9b…"
}
typeWhenCrediting
paymentA bill’s status changed — see statusCredit payment_amount_usd on paid / paid_over; deduplicate by uuid
walletA transfer reached a static wallet (status is always paid; order_id is the one you opened the wallet with)Credit payment_amount_usd; every transfer has its own uuid — deduplicate by it

Verify the signature (required)

The signature is the sign field of the webhook body: remove sign, re-encode the rest the way PHP json_encode does (compact, / escaped as \/, non-ASCII not escaped), and md5(base64(encoded) + API key) must equal sign. Use the API key of the same mode. The same webhook may arrive more than once — deduplicate by uuid + status.

If verification fails, paste the raw webhook body into the signature checker: it shows the result of every step and marks where yours differs (it all runs in your browser; the API key is never uploaded). Every webhook in the dashboard’s “Webhook log” opens in the checker with one click.

Node.js
import crypto from "node:crypto";

function verify(rawBody, apiKey) {
  const { sign, ...data } = JSON.parse(rawBody);
  const json = JSON.stringify(data).replaceAll("/", "\\/");
  const expected = crypto.createHash("md5").update(Buffer.from(json).toString("base64") + apiKey).digest("hex");
  return typeof sign === "string" && sign.length === expected.length && crypto.timingSafeEqual(Buffer.from(sign), Buffer.from(expected));
}
PHP
$data = json_decode(file_get_contents('php://input'), true);
$sign = $data['sign'] ?? '';
unset($data['sign']);
$expected = md5(base64_encode(json_encode($data, JSON_UNESCAPED_UNICODE)) . $apiKey);
if (!hash_equals($expected, $sign)) {
    http_response_code(401); exit;
}
Python
import base64, hashlib, hmac, json

def verify(raw_body: bytes, api_key: str) -> bool:
    data = json.loads(raw_body)
    sign = data.pop("sign", "")
    encoded = json.dumps(data, separators=(",", ":"), ensure_ascii=False).replace("/", "\\/")
    expected = hashlib.md5((base64.b64encode(encoded.encode()).decode() + api_key).encode()).hexdigest()
    return hmac.compare_digest(expected, sign)

Test mode

Bills created with the Test merchant and API key run on testnets, with no real money involved. The payment page shows a “Simulate payment” button (full / half / overpay), so you can see status changes and webhooks without getting testnet coins.

Migrating from Cryptomus

  1. Dashboard “Integration & developers” → generate a merchant and API key.
  2. Replace https://api.cryptomus.com in your code with https://blockwallets.io, and use the new merchant and API key.
  3. Add your domains under “Website domains” (url_callback / url_return / url_success only accept these).

Supported: /v1/payment, /v1/payment/info, /v1/payment/list, /v1/payment/services, /v1/payment/resend, /v1/wallet, /v1/test-webhook/payment, /v1/test-webhook/wallet. Differences: bills are priced only in USD / USDT / USDC (all 1:1, no exchange spread); commission is our platform fee (the amount received × the merchant’s rate when the bill was created — 1% standard, down to 0.5% by volume; nothing is charged twice on the part the customer covers) and merchant_amount = received − commission; when the customer pays in BTC / LTC we quote the median of several exchanges, locked for 30 minutes — payer_amount is the number of coins to send, payer_amount_exchange_rate is the USD value of one coin, and the payment is converted to payment_amount_usd at the locked rate (the coins stay with us, nothing is swapped); networks tron / bsc / eth / base / polygon / arbitrum / optimism / avalanche / xlayer / btc / ltc / sol; minimum bill lifetime 10 minutes; no is_refresh and no blocking of static wallets; webhooks come from changing IPs, so verify sign rather than allow-listing IPs.

Coming from NOWPayments or Coinbase Commerce? We have compatible APIs for both — change the base URL and keys; see the migration guide.

Networks and amounts

CoinNetworknetwork parameterMinimumArrival
USDTTRONtron$1~1 min
USDTBNB Chainbsc$1~1 min
USDCBNB Chainbsc$1~1 min
USDTEthereumeth$5~10 min
USDCEthereumeth$5~10 min
USDCBasebase$1~3 min
USDTPolygonpolygon$1~1 min
USDCPolygonpolygon$1~1 min
USDTArbitrumarbitrum$1~12 min
USDCArbitrumarbitrum$1~12 min
USDTOptimismoptimism$1~5 min
USDCOptimismoptimism$1~5 min
USDTAvalancheavalanche$1~1 min
USDCAvalancheavalanche$1~1 min
USDTX Layerxlayer$1~2 min
USDCX Layerxlayer$1~2 min
BTCBitcoinbtc$5~10 min
LTCLitecoinltc$1~8 min
TRXTRONtron$1~1 min
BNBBNB Chainbsc$1~1 min
ETHEthereumeth$5~10 min
ETHBasebase$1~3 min
ETHArbitrumarbitrum$1~12 min
ETHOptimismoptimism$1~5 min
POLPolygonpolygon$1~1 min
AVAXAvalancheavalanche$1~1 min
OKBX Layerxlayer$1~2 min
USDTSolanasol$1~1 min
USDCSolanasol$1~1 min
SOLSolanasol$1~1 min

ERC-20 fees are high, so the minimum is $5; Bitcoin fees are not low either, minimum $5; everything else is $1. Customers must send on the network shown on the payment page — money sent on another network is not credited automatically. BTC / LTC show “detected” as soon as they are in the mempool and count once they have enough confirmations; replaced (RBF) or dropped transactions do not count. Native coins (TRX, BNB, ETH, POL, AVAX, OKB, SOL) are quoted at a locked rate and credited in US dollars like BTC / LTC; specify both network and coin: bills network: "bsc", to_currency: "BNB", static wallets network: "bsc", currency: "BNB". A network on its own defaults to USDT on that chain (USDC on Base). Transfers made by contracts (multisig, batch payouts) are received too; on some chains their transaction hash is unavailable and txid in the webhook is null.

Errors

json
{ "state": 1, "message": "amount must be a number like 10.28" }
HTTPCommon causes
422Missing or malformed parameters; amount below the network minimum; callback / redirect URL not on your registered domains
401Wrong merchant, wrong sign (Invalid Sign.) or a rotated API key
404Bill does not exist or isn’t yours (Payment not found); unsupported coin / network combination (The service was not found)
503The network is temporarily unavailable — pick another or don’t preselect one

When the signature is wrong (Invalid Sign.) the response carries a debug object: body_base64 is the base64 of the request body we received (your own content, never the API key) and body_bytes its length. Compare it with the base64 you signed: if they differ, the body you sent is not the string you signed (your framework re-serialized it, a proxy changed the encoding, a trailing newline); if they match, the API key is wrong. Paste it into the signature checker to see the first difference.

json
{
  "state": 1,
  "message": "Invalid Sign.",
  "debug": {
    "body_base64": "eyJhbW91bnQiOiI0Mi4wMCIsImN1cnJlbmN5IjoiVVNEIiwi…",
    "body_bytes": 158,
    "sign_formula": "md5(body_base64 + API key)"
  }
}

FAQ

Customers top up any amount they like — which mode?

Bills. Let the customer enter an amount on your top-up page, create a bill for it, and credit payment_amount_usd when it arrives. Use static wallets only if customers should save one address and send to it repeatedly.

What if a customer underpays?

Before it expires the customer can send the difference on the payment page (it shows how much is still due; status wrong_amount_waiting, and if under 30 minutes remain at the first short payment the deadline moves to 30 minutes later). If it is still short at expiry, the bill becomes wrong_amount and payment_amount_usd in the webhook is the amount received; top-up businesses can simply credit that.

What if a customer pays late, or twice?

Money that arrives after expiry is still credited and notified; money that arrives after a bill is fully paid is recorded too and visible in the dashboard.

What if a customer sends on the wrong network?

For example the bill is on TRON and they send from BNB Chain: it is not credited automatically — contact us and we handle it manually.

How much does it cost?

1% of each payment received, lowered automatically by last month’s volume (0.9% from $10,000, 0.8% from $25,000, 0.7% from $50,000, 0.6% from $100,000 and 0.5% from $200,000), with no monthly or setup fees. The customer pays the network fee when sending; to pass the fee on to the customer, set subtract when creating a bill or as a default in the dashboard. Withdrawals carry no extra fee. See pricing.

When does the money reach me?

It is credited to your balance as soon as the payment confirms. Request a withdrawal to your own wallet under “Balance & withdrawals”; once we approve it we pay out, and the on-chain transaction appears in your withdrawal history.

You can also turn on auto-withdrawal: once your available balance reaches an amount you set, or once a day at 00:00 UTC, the whole available balance is withdrawn. Automatic withdrawals are reviewed the same way, and every settings change sends a notice to your email.