原生 REST 接口
原生接口是 blockwallets 自己的 REST 接口:发票按美分计价,每个订单一个防重的 ref,充值账户一次覆盖所有网络,回调用 HMAC-SHA256 签名。它和 Cryptomus 兼容接口建的是同一种账单、同一个收银台,费率和余额都一样。
密钥在商户后台「开发者 › 接入与密钥」自助生成:正式和测试各一把密钥和对应的回调签名密钥,明文只显示一次,列表里只显示前缀。开了两步验证的账户,生成或轮换正式密钥要输验证码。还没有商户账号?在首页申请开通。已经接过 Cryptomus?看 Cryptomus 兼容接口(怎么选)。
密钥与请求
基础地址 https://blockwallets.io,请求和应答都是 JSON。每个请求带:
Authorization: Bearer bw_live_…或Bearer bw_test_…:前缀决定模式,Test 密钥只看得到 Test 发票、走测试网POST请求带Content-Type: application/json- 密钥只放在你的服务器上,不要放进网页或 App;我们只存它的哈希
出错时回 HTTP 状态码和 {"error": {"code": "…", "message": "…"}}:400 invalid_request(字段缺了或格式不对)、401 unauthorized(没带密钥、密钥不认识或已停用)、404 not_found、409 conflict(和发票当前的状态冲突)、422 currency_unsupported。
接口一览
| 接口 | 做什么 |
|---|---|
GET /v1/networks | 这把密钥的模式下现在能收的网络:最低额、确认数、一般到账时间和手续费估算;价格币带当前汇率 |
POST /v1/invoices | 创建发票(201);同一个 ref 已有发票就返回它(200) |
GET /v1/invoices?ref=… | 按你的 ref 查发票 |
GET /v1/invoices/:id | 按 id 查发票 |
POST /v1/invoices/:id/network | 你自己替发票选网络(白标收银台):返回要展示的 address 和 pay_amount |
POST /v1/invoices/:id/close | 关掉一张还没见到任何转账的待付发票 |
POST /v1/deposit-accounts | 给一个用户开(201)或取(200)充值账户:每个网络一个长期地址 |
GET /v1/deposit-accounts?customer_ref=… | 按 customer_ref 查充值账户 |
GET /v1/deposit-accounts/:id | 按 id 查充值账户 |
GET /v1/deposits | 列一个账户的充值(account_id 或 customer_ref,limit 1–200,默认 50) |
GET /v1/deposits/:id | 查一笔充值 |
GET /v1/events | 一张发票或一笔充值的回调投递记录(invoice_id 或 deposit_id) |
POST /v1/events/:id/redeliver | 重新投递一条回调(202) |
查发票或充值账户时加 ?probe=1,顺手去链上看一眼(最多 10 秒一次);不加就是扫链器最新的结果,扫链器每分钟查一次。
创建发票 POST /v1/invoices
用户下单时调 POST /v1/invoices,把用户跳到应答里的 pay_url。
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}}'| 字段 | 必填 | 说明 |
|---|---|---|
ref | 是 | 你的订单号:1–64 个字母、数字或 _ . : -。同一模式下唯一;同一个 ref、同样的字段再发一次返回同一张发票,字段不同回 409 |
amount | 是 | 金额,美分整数:4200 = $42.00。选定网络后不能低于该网络的最低额 |
title | 是 | 收银台上给用户看的标题,最多 64 字 |
return_url | 是 | 收银台「返回」去的地址;必须在你的网站域名下 |
success_url | 否 | 付完款去的地址;默认同 return_url |
network | 否 | 预选网络,填网络表里的 id;不填让用户在收银台选 |
customer_ref | 否 | 你的用户 id,最多 64 字,原样返回 |
lifetime | 否 | 有效期(秒),600 – 43200,默认 3600 |
amount_rule | 否 | "any" = 到多少记多少(充值类):一确认就是 paid。不传按后台「设置 › 收款规则与费率」的「收款金额规则」 |
accuracy_percent | 否 | 少付多少仍算付清,0 – 5(百分比),默认 0 |
subtract_percent | 否 | 平台手续费由付款人承担的比例,0 – 100(百分比),加进 pay_amount;不传按后台默认值 |
payment_multiple | 否 | 少付后能否往同一地址补转,默认按后台设置(没改过就是开);false:第一笔不够数就以 wrong_amount 结束 |
meta | 否 | 任意 JSON 对象,最多 2 KB,查询和回调里原样返回 |
currency | 否 | 只能是 USD(默认);别的回 422 |
所有地址都必须在你的网站域名下(后台「开发者 › 接入与密钥」)。应答就是下面的发票对象;选了网络以后才有 network、address 和 pay_amount。
发票对象
id 以 inv_ 开头。amount 是美分;pay_amount、paid_amount 是所选网络上的币数(小数字符串)。
{
"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 | 意思 | 你要做什么 |
|---|---|---|
pending | 等待付款 | — |
confirming | 看到转账、正在确认,或者到账不够、还能补 | — |
paid | 已收款 | 发货 / 加余额 |
paid_over | 已收款,而且多付了 | 同上;多出来的在你的余额里 |
wrong_amount | 以不够数结束(到期时,或 payment_multiple 为 false 时的第一笔不够数) | 充值类业务可以按实收入账(paid_amount;按汇率计价的币另有 paid_cents);卖货的联系用户补款 |
closed | 到期没收到钱,或者被你关掉了 | —(之后才到的钱仍会入账并回调你) |
其他字段:pay_url(托管收银台)、payments(每笔转账,含 txid、amount、confirmations、confirmed)、expires_at、paid_at、closed_at;BTC、LTC、DOGE、BCH 和原生币还有 rate(1 个币值多少美元,锁定 30 分钟)、rate_locked_until 和 paid_cents(已到账部分折合的美分)。
网络
把 id 填进 network。GET /v1/networks 返回你这个模式下现在开着的网络;网络可能临时暂停,最好读它而不是写死。
| network | 币种 | 链 | 最低 |
|---|---|---|---|
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 |
充值账户(静态地址) POST /v1/deposit-accounts
POST /v1/deposit-accounts 传 {"customer_ref": "user_42"},给这个用户每个网络一个长期地址(所有 EVM 链共用一个地址)。转来多少都按美元入账,每笔转账一条 deposit.credited 回调。
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 个字母、数字或_ . : @ -;同一个值永远返回同一个账户。可选return_url,要在你的域名下- 应答里有
deposit_url(托管页面:地址、二维码和最近的充值)和networks(每个网络的id、address、min_amount、confirmations) - 充值 id 以
dep_开头;amount是美分,amount_token是币数,价格币带入账用的rate
低于最低额(一般 $1,BTC $5)的转账记为 below_min,不自动入账;联系我们人工入账。
回调
我们往商户后台里填的回调地址 POST JSON(「开发者 › 接入与密钥」;Live 和 Test 各有自己的地址和密钥)。请求体就是事件:
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 秒>,v1=<hex>,其中v1= HMAC-SHA256(该模式的回调密钥,t+.+ 原始请求体)X-BW-Event:事件 id(ev_…);按它去重X-BW-Delivery:第几次投递,从 1 开始
| 事件 | 什么时候 |
|---|---|
invoice.paid | 发票变成 paid 或 paid_over(包括过期后才到的钱) |
invoice.wrong_amount | 发票以 wrong_amount 结束 |
invoice.closed | 发票到期没收到钱,或者被你关掉了 |
deposit.credited | 一笔转进充值账户的钱入账了 |
用原始请求体验签(别先解析再序列化),和本机时间差超过 300 秒的 t 拒掉,用常量时间比较。请求体里的 invoice / deposit 和查询返回的对象一样。
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));
}回 2xx 算送达;否则 1 分钟、5 分钟、30 分钟、2 小时、12 小时、24 小时、24 小时后重试(一共 8 次)。
事件与重投
GET /v1/events?invoice_id=inv_…(或 deposit_id=dep_…)列出一张发票或一笔充值的回调,带 state、attempts、last_status、last_error。POST /v1/events/ev_…/redeliver 立刻重新排队投递,比如你修好了接收端以后。后台「开发者 › 回调日志」能看到每次投递的请求和应答。
测试模式
用 bw_test_ 密钥建的发票走测试网,不碰真钱。托管收银台上有「模拟付款」按钮(全额 / 一半 / 多付),不用测试币也能看到状态变化和回调。