文档
blockwallets 帮你收 USDT / USDC,以及 BTC / LTC(按锁定汇率折成美元入账):用户付款,我们在链上确认到账,然后通知你。接口、签名和回调与 Cryptomus 相同,接过 Cryptomus 的换个域名就能用。下面先讲怎么用,再讲接口。
两种收款方式
两种都能收任意金额:固定价格的订单、用户自己填金额的充值都可以,收到之后你给用户发货或加余额,让他慢慢用。区别只在于每次付款是不是新开一个地址。
用户在你的页面选好商品或填好金额,你建一张账单,用户在收银台按显示的金额转账。每张账单一个新地址、有倒计时,到账后按实际收到的金额回调你。
POST /v1/payment → 回调 type: payment
不用先填金额:每个用户在每个网络一个长期地址,用户存进钱包随时转。每到一笔单独回调一次。
POST /v1/wallet → 回调 type: wallet
拿不准就用账单。不写代码的话,商户后台的收款链接就是一张张自动生成的账单。还没开通商户?在首页申请;开通后登录商户后台,建议先到「体验收款」付一小笔走一遍(钱进你自己的余额)。
一张账单是怎么走的
一笔收款牵涉四方:用户、你的网站、blockwallets 和区块链。下图从上往下按时间排,右边是账单在每一步的状态。
图比较宽,可以左右滑动查看。
简单说就是五步:
- 你创建账单(收款链接自动帮你建,或你的服务器调
/v1/payment),拿到收银台地址url,把用户跳过去。 - 用户在收银台选币种(USDT / USDC / BTC / LTC)和网络,得到一个专属收款地址和金额,用钱包或交易所转账。
- 我们在链上看到转账,等够确认数,再找第二家节点核对一遍(防止节点作假)。
- 账单变成
paid,我们马上 POST 一条签名回调到你的url_callback;收银台几秒后把用户送回你的url_success。 - 钱记入你的余额,在商户后台申请提现到你自己的钱包。
账单状态
| status | 意思 | 你要做什么 |
|---|---|---|
check | 等待付款 | — |
confirm_check | 看到转账了,在等确认 | — |
wrong_amount_waiting | 已到账但还差一点,用户可以往同一地址补转(is_payment_multiple 为 true 时;只回调一次) | —(补齐后会再回调 paid) |
paid | 已收款 | 按 payment_amount_usd 发货 / 加余额 |
paid_over | 已收款,而且多付了 | 同上(payment_amount_usd 是实收金额) |
wrong_amount | 到期了,收到的钱不够(is_payment_multiple 为 false 时,第一笔到账不够就是它) | 充值类业务可以按实收的 payment_amount_usd 入账;卖货的联系用户补款 |
cancel | 到期没收到钱 | —(之后才到的钱仍会入账并回调你) |
is_final 为 true 时状态不会再变(晚到的钱除外)。
收款链接(不写代码)
商户后台 →「收款链接」→ 填收款说明和金额(可以固定网络,也可以让用户自己选)→ 创建。每个链接有一个网址和一个二维码。
- 每次有人打开链接,都会生成一张新账单,所以一个链接可以反复用(比如「会员费 $99」)。
- 收到的钱在「收款记录」里能看到每一笔,在「余额与提现」里提到你的钱包。
- 在「接入与开发者」生成了凭据、填了回调地址的话,链接上的每笔到账也会照下面的格式回调你。
- 不想再收了,点「停用」,链接立刻失效。
API 接入
1. 凭据与签名
商户后台 →「接入与开发者」→ 生成 merchant(UUID)和 API key。Live 和 Test 各一套:Live 收真钱,Test 走测试网。再填上你的网站域名:url_callback、url_return、url_success 都必须在这些域名下。API key 只显示一次,请保存好,只放在服务器上。
基础地址 https://blockwallets.io,全部接口都是 POST + JSON,请求头带:
merchant:你的 merchant UUID(决定是 Live 还是 Test)sign:md5(base64(请求体) + API key),请求体就是你发出去的那串 JSON 原文
成功返回 {"state": 0, "result": {…}},失败返回 {"state": 1, "message": "…"}(见错误)。
下面几个接口旁有「试一下」:填上 Test 的 merchant 和 API key,在浏览器里签名、直接调用,看到完整的请求和响应(凭据只留在页面内存里)。签名对不上时用在线验签逐步对比。
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();
}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);
}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. 创建账单 POST /v1/payment
用户下单,或在你的充值页填好金额后,调 https://blockwallets.io/v1/payment,把用户跳转到返回的 result.url。
{
"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"
}| 参数 | 必填 | 说明 |
|---|---|---|
amount | 是 | 金额,字符串,最多两位小数:"42.00"。可以是你定的价格,也可以是用户自己填的数;不能低于所选网络的最低额(见网络表) |
currency | 是 | USD、USDT 或 USDC,三者 1:1 |
order_id | 是 | 你的订单号,字母、数字、-、_,≤ 100 字。同一个 order_id 再调一次,原样返回已有的账单(防重复下单) |
url_callback | 否 | 回调地址。不传就不回调 |
url_return | 否 | 用户付款前点「返回」去哪 |
url_success | 否 | 付款成功后送回哪 |
network | 否 | 预先指定网络:tron / bsc / eth / base / polygon / arbitrum / optimism / avalanche / xlayer / btc / ltc / sol;不传就让用户在收银台选 |
to_currency | 否 | 配合 network 指定币种:USDT / USDC;btc / ltc 网络就是 BTC / LTC(可不传) |
lifetime | 否 | 账单有效期(秒),300 ~ 43200,默认 3600;不到 600 的按 600 算 |
accuracy_payment_percent | 否 | 少付多少以内也算付清,0 ~ 5,默认 0。传 100 = 收多少算多少:到账确认后立即 paid(多了是 paid_over),按回调里的 payment_amount_usd 入账,适合充值。不传时按商户后台「收款金额规则」:选了「到多少记多少」就是 100 |
subtract | 否 | 平台手续费里由用户承担的百分比,0 ~ 100,加进 payer_amount(平台费率 1% 起、按流水降档,按 1% 算传 100 时 100 美元的账单用户付 101.00;费率见 价格)。不传按商户后台「手续费与少付」,出厂 0 |
is_payment_multiple | 否 | 少付后能不能补转,默认 true:状态 wrong_amount_waiting,用户往同一地址补齐就 paid(离到期不足 30 分钟时有效期顺延到 30 分钟后)。false:第一笔到账不够直接 wrong_amount。不传按商户后台设置 |
additional_data | 否 | 任意字符串(≤ 255),原样出现在查询结果和回调里 |
{
"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、payer_amount 在用户选好网络之后才有。
3. 查询账单
POST /v1/payment/info{"order_id": "…"}或{"uuid": "…"}:查一张,会顺便去链上看一眼(10 秒内只看一次)。POST /v1/payment/list:列表,每页 15 张,可传date_from/date_to,下一页用返回的paginate.nextCursor(?cursor=…)。POST /v1/payment/services:当前可用的网络和每个网络的最低金额。POST /v1/payment/resend{"order_id": "…"}:重发已付 / 少付账单的回调。
回调是首选方式,查询用来兜底(比如你的服务器宕机错过了回调)。
4. 静态钱包 POST /v1/wallet
给一个用户在某个网络开一个长期地址。同一个 order_id(一般就是你的用户 id)+ 网络永远是同一个地址;所有 EVM 链(BNB Chain、Ethereum、Base、Polygon、Arbitrum、Optimism、Avalanche、X Layer、Solana)上是同一个地址,TRON、BTC、LTC 各一个。用户转多少到账多少,每笔都单独回调;BTC / LTC 每笔按那笔被看到时的汇率折成美元(payment_amount_usd)。
{
"order_id": "user_42",
"currency": "USDT",
"network": "tron",
"url_callback": "https://your-site.com/webhooks/crypto"
}{
"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 是托管的充值页(地址 + 二维码 + 最近到账),可以直接给用户。不到 $1 的转账不自动入账,在商户后台人工处理。暂不支持封禁地址(/v1/wallet/block-address)。
回调通知
账单状态变化(paid、paid_over、wrong_amount_waiting、wrong_amount、cancel)和静态钱包每到一笔,我们 POST 一条 JSON 到 url_callback。你的接口返回 2xx 就算送达;否则按 1 分钟、5 分钟、30 分钟、2 小时、12 小时、24 小时、24 小时重试,在商户后台或用 /v1/payment/resend 也能重发。
{
"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…"
}| type | 什么时候 | 入账 |
|---|---|---|
payment | 账单状态变了,看 status | paid / paid_over 时按 payment_amount_usd 入账,按 uuid 去重 |
wallet | 静态钱包到账一笔(status 总是 paid,order_id 是开钱包时给的) | 按 payment_amount_usd 入账;每笔 uuid 不同,按 uuid 去重 |
验证签名(必须做)
签名在回调体的 sign 字段里:去掉 sign,剩下的按 PHP json_encode 的格式重新编码(紧凑、/ 转义成 \/、中文不转义),md5(base64(编码结果) + API key) 应当等于 sign。用的是同一个 mode 的 API key。同一条回调可能重复,按 uuid + status 去重。
验不过时把收到的原始回调体贴进在线验签,它会列出每一步的结果并标出和你的不同之处(全程在浏览器里算,API key 不上传)。商户后台「回调日志」里每条回调都能直接在验签工具里打开。
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));
}$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;
}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 的 merchant 和 API key 建的账单走测试网,和真钱无关。收银台上会多出「模拟付款」按钮(付全款 / 只付一半 / 多付),不用领测试币就能看到状态变化和回调。
POST /v1/test-webhook/payment、/v1/test-webhook/wallet:往url_callback发一条签好名的样例回调(传currency、network,可选status、order_id),用来调验签代码。- 商户后台「接入与开发者」→「发一条测试通知」:往你填的回调地址发一条样例。
- 商户后台「回调日志」:每次投递的时间、目标地址、HTTP 状态、耗时、请求体和你的服务器的应答(前 4 KB),可按订单 / 状态筛选、一键重发;保留 30 天。
从 Cryptomus 迁移
- 商户后台「接入与开发者」→ 生成 merchant 和 API key。
- 把代码里的
https://api.cryptomus.com换成https://blockwallets.io,换上新的 merchant 和 API key。 - 在「网站域名」里填上你的域名(
url_callback/url_return/url_success只接受这些域名)。
支持:/v1/payment、/v1/payment/info、/v1/payment/list、/v1/payment/services、/v1/payment/resend、/v1/wallet、/v1/test-webhook/payment、/v1/test-webhook/wallet。
不同之处:账单只按 USD / USDT / USDC 计价(三者 1:1);commission 是我们的平台手续费(实收 × 建单时商户的费率,标准 1%、按流水最低 0.5%;付款人承担的那部分不再收),merchant_amount = 实收 − commission;用户选 BTC / LTC 付时,按多家交易所的中位数报价、锁定 30 分钟,payer_amount 是要付的币数,payer_amount_exchange_rate 是 1 个币值多少美元,到账按锁定汇率折成 payment_amount_usd(币留在我们这里,不做兑换);网络 tron / bsc / eth / base / polygon / arbitrum / optimism / avalanche / xlayer / btc / ltc / sol;账单有效期最短 10 分钟;不支持 is_refresh 和封禁静态钱包;回调来源 IP 不固定,请用 sign 验签,不要按 IP 白名单。
从 NOWPayments 或 Coinbase Commerce 过来?我们也有这两家的兼容接口,换 base URL 和密钥即可,见迁移指南。
网络与金额
| 币种 | 网络 | network 参数 | 最低金额 | 到账 |
|---|---|---|---|---|
| USDT | TRON | tron | $1 | 约 1 分钟 |
| USDT | BNB Chain | bsc | $1 | 约 1 分钟 |
| USDC | BNB Chain | bsc | $1 | 约 1 分钟 |
| USDT | Ethereum | eth | $5 | 约 10 分钟 |
| USDC | Ethereum | eth | $5 | 约 10 分钟 |
| USDC | Base | base | $1 | 约 3 分钟 |
| USDT | Polygon | polygon | $1 | 约 1 分钟 |
| USDC | Polygon | polygon | $1 | 约 1 分钟 |
| USDT | Arbitrum | arbitrum | $1 | 约 12 分钟 |
| USDC | Arbitrum | arbitrum | $1 | 约 12 分钟 |
| USDT | Optimism | optimism | $1 | 约 5 分钟 |
| USDC | Optimism | optimism | $1 | 约 5 分钟 |
| USDT | Avalanche | avalanche | $1 | 约 1 分钟 |
| USDC | Avalanche | avalanche | $1 | 约 1 分钟 |
| USDT | X Layer | xlayer | $1 | 约 2 分钟 |
| USDC | X Layer | xlayer | $1 | 约 2 分钟 |
| BTC | Bitcoin | btc | $5 | 约 10 分钟 |
| LTC | Litecoin | ltc | $1 | 约 8 分钟 |
| TRX | TRON | tron | $1 | 约 1 分钟 |
| BNB | BNB Chain | bsc | $1 | 约 1 分钟 |
| ETH | Ethereum | eth | $5 | 约 10 分钟 |
| ETH | Base | base | $1 | 约 3 分钟 |
| ETH | Arbitrum | arbitrum | $1 | 约 12 分钟 |
| ETH | Optimism | optimism | $1 | 约 5 分钟 |
| POL | Polygon | polygon | $1 | 约 1 分钟 |
| AVAX | Avalanche | avalanche | $1 | 约 1 分钟 |
| OKB | X Layer | xlayer | $1 | 约 2 分钟 |
| USDT | Solana | sol | $1 | 约 1 分钟 |
| USDC | Solana | sol | $1 | 约 1 分钟 |
| SOL | Solana | sol | $1 | 约 1 分钟 |
ERC-20 手续费贵,最低 $5;BTC 链上手续费也不低,最低 $5;其余 $1。用户要按收银台显示的网络转,转错网络的钱不会自动到账。
BTC / LTC 在内存池里看到就显示「已检测到」,够确认数才算到账;被替换(RBF)或掉出内存池的交易不算。
各链原生币(TRX、BNB、ETH、POL、AVAX、OKB)和 BTC / LTC 一样按锁定汇率报价、按美元入账;要写明网络和币种:建单 network: "bsc", to_currency: "BNB",静态钱包 network: "bsc", currency: "BNB";只写网络默认是这条链上的 USDT(Base 是 USDC)。
合约转出的(多签、批量出款)也能收到;个别链上拿不到这类转账的交易哈希,回调里 txid 为 null。
错误
{ "state": 1, "message": "amount must be a number like 10.28" }| HTTP | 常见原因 |
|---|---|
| 422 | 参数缺失或格式不对;金额低于该网络最低额;回调 / 跳转地址不在你登记的域名下 |
| 401 | merchant 不对、sign 算错(Invalid Sign.)或 API key 已轮换 |
| 404 | 账单不存在或不属于你(Payment not found);不支持的币种 / 网络组合(The service was not found) |
| 503 | 这个网络暂时不可用,换一个或不指定网络 |
签名错(Invalid Sign.)时,应答里多一个 debug:body_base64 是我们收到的请求体的 base64(就是你发的内容,不含 API key),body_bytes 是字节数。拿它和你签名时的 base64 比:不一样,说明发出去的 body 和签名的不是同一串(框架重新序列化、代理改了编码、多了换行);一样,就是 API key 不对。贴进在线验签能直接标出第一处不同。
{
"state": 1,
"message": "Invalid Sign.",
"debug": {
"body_base64": "eyJhbW91bnQiOiI0Mi4wMCIsImN1cnJlbmN5IjoiVVNEIiwi…",
"body_bytes": 158,
"sign_formula": "md5(body_base64 + API key)"
}
}常见问题
用户想充多少就充多少,用哪种?
用账单就行:你的充值页让用户填金额,用这个金额建账单,到账后按 payment_amount_usd 加余额。想让用户把一个地址存起来反复转,再用静态钱包。
用户少付了怎么办?
到期前用户可以在收银台补转差额(收银台会显示「还差多少」,状态 wrong_amount_waiting;第一次少付时离到期不足 30 分钟的,有效期顺延到 30 分钟后)。到期还不够,账单变成 wrong_amount,回调里的 payment_amount_usd 是实收金额;充值类业务直接按它入账即可。
用户付晚了、或者转了两次?
过期后才到的钱照样入账并回调你;付清后又到的钱也会记录,在商户后台能看到。
用户转错网络了?
比如账单是 TRON,用户从 BNB Chain 转:钱不会自动到账,联系我们人工处理。
收费多少?
每笔实收收 1% 平台手续费,上个月流水大的自动降档(满 $10,000 降到 0.9%、满 $25,000 降到 0.8%、满 $50,000 降到 0.7%、满 $100,000 降到 0.6%、满 $200,000 降到 0.5%),没有月费、开户费。链上网络费由付款人转账时自付;想让付款人承担手续费,建单传 subtract 或在商户后台设默认值。提现不另收手续费。详见 价格。
钱什么时候到我手里?
付款确认后马上记入余额。在「余额与提现」申请提现到你自己的钱包,我们审核通过后出款,出款后能在提现记录里看到链上交易。
也可以开自动提现:可提余额满你设的金额就自动提,或者每天 UTC 0 点(北京时间 8 点)自动提一次,每次提出当时的全部可提余额。自动开出的提现单一样要我们审核;每次修改设置,都会给你的邮箱发一封通知。