从 NOWPayments / Coinbase Commerce 迁移
blockwallets 除了 Cryptomus 兼容接口,也提供 NOWPayments 和 Coinbase Commerce 的兼容接口:路径、鉴权头、请求 / 响应字段、状态和回调签名都照它们的官方文档,接过这两家的代码通常只改 base URL 和密钥。Coinbase Commerce 自 2026-03-31 起不再服务美国 / 新加坡以外的商户,可以直接换过来。
底层就是 blockwallets 的账单:收款地址是你自己的,付款页是我们的收银台,到账后链上确认、再找第二家节点核对一遍才算付款成功。下面先讲三步接入,再分别列出两家的支持范围和与官方接口的差异——没实现的参数会明确报错,不会悄悄忽略。
三步接入
- 开通商户(首页申请),在商户后台「网站域名」里填上你的域名。回调地址(
ipn_callback_url)和跳转地址(success_url/cancel_url/redirect_url)只接受这些域名和回调地址的域名——即使密钥泄露,也没法把回调指到别处。 - 在商户后台「接入与开发者」页生成兼容凭据(live 和 test 各一套):NOWPayments 是 API key + IPN secret,Coinbase Commerce 是 API key + webhook shared secret。密钥只显示一次,我们这边只存哈希 / 密文。
- 把代码里的 base URL 换成下面的地址,换上新密钥。先用 test 凭据在测试网走一遍,再换 live。
| 原来 | 换成 | 鉴权头 | 回调签名 |
|---|---|---|---|
https://api.nowpayments.io/v1 | https://blockwallets.io/nowpayments/v1 | x-api-key | x-nowpayments-sig(HMAC-SHA512) |
https://api.commerce.coinbase.com | https://blockwallets.io/coinbase/ | X-CC-Api-Key | X-CC-Webhook-Signature(HMAC-SHA256) |
NOWPayments
- const NP = "https://api.nowpayments.io/v1";
+ const NP = "https://blockwallets.io/nowpayments/v1";
const res = await fetch(`${NP}/payment`, {
method: "POST",
headers: { "x-api-key": process.env.NOWPAYMENTS_API_KEY, "content-type": "application/json" },
body: JSON.stringify({
price_amount: 49.9, price_currency: "usd", pay_currency: "usdttrc20",
order_id: "A-1001", order_description: "Pro plan",
ipn_callback_url: "https://shop.example/ipn/nowpayments",
}),
});
const { payment_id, pay_address, pay_amount } = await res.json();支持的接口
| 接口 | 说明 |
|---|---|
GET /status | {"message":"OK"},不要密钥 |
GET /currencies · /merchant/coins · /full-currencies | 当前可收的币(ticker 见下表);fixed_rate 参数忽略 |
GET /min-amount · GET /estimate | 最低金额与估价;只认 USD 计价(USDT / USDC 按 1:1) |
POST /payment | 建付款,直接返回 pay_address / pay_amount(你自己展示收款信息) |
POST /invoice | 建账单,invoice_url 是我们的收银台,付款人在里面选币种和网络 |
POST /invoice-payment | 给账单定币种,返回付款 |
GET /payment/:id | 查付款状态 |
GET /payment/ | 付款列表:limit(1–500)/ page / orderBy / dateFrom / dateTo / invoiceId,sortBy 只支持 created_at |
POST /auth | 列表只看 x-api-key,不需要 JWT;这里回一个不校验的占位 token,让原来先登录再查列表的代码照常跑(不校验邮箱密码) |
没有:出金(payout)、换汇(conversion)、托管子账户(sub-partner)、订阅(subscriptions)、update-merchant-estimate、法币付款。这些路径返回 404。
IPN 回调
状态变到终态时,我们 POST 到建单时给的 ipn_callback_url,头 x-nowpayments-sig = HMAC-SHA512(IPN secret, 按 key 递归排序后的 JSON),和 NOWPayments 的验签代码完全一样。我们发出的 body 本身就是排好序的紧凑 JSON,所以直接对原始 body 算 HMAC 也对(PHP / Python 遇到很小的小数时重新编码可能和 JS 不一致,对原始 body 算最稳)。回调失败按 1 分钟、5 分钟、30 分钟……重试,共 8 次。
import crypto from "node:crypto";
// 和 NOWPayments 文档的写法一样(我们发出的 body 本身已经排好序,直接对原始 body 算也对)
function sortObject(obj) {
return Object.keys(obj).sort().reduce((r, k) => {
r[k] = obj[k] && typeof obj[k] === "object" ? sortObject(obj[k]) : obj[k];
return r;
}, {});
}
function verifyIpn(params, sig, ipnSecret) {
const hmac = crypto.createHmac("sha512", ipnSecret).update(JSON.stringify(sortObject(params))).digest("hex");
return crypto.timingSafeEqual(Buffer.from(hmac), Buffer.from(sig || ""));
}
// verifyIpn(req.body, req.headers["x-nowpayments-sig"], process.env.NOWPAYMENTS_IPN_SECRET)$raw = file_get_contents('php://input');
$data = json_decode($raw, true);
tksort($data); // NOWPayments 文档里的递归 ksort
$hmac = hash_hmac('sha512', json_encode($data, JSON_UNESCAPED_SLASHES), trim($ipnSecret));
$ok = hash_equals($hmac, $_SERVER['HTTP_X_NOWPAYMENTS_SIG'] ?? '');| payment_status | 什么时候 | 发 IPN |
|---|---|---|
waiting | 等待付款(账单还没选币也是它) | — |
confirming | 链上见到付款,在等确认 | —(查单能看到) |
finished | 已收款(含多付;actually_paid 是实收) | ✓ |
partially_paid | 收到的不够:到期前用户还能往同一地址补转(第一次少付时发一次),到期仍不够再发一次 | ✓ |
expired | 到期没付 | ✓ |
refunded | 你在商户后台整单退款、已退出 | —(查单能看到) |
不会出现 confirmed、sending、failed:我们确认完就直接是 finished,钱记进你的余额,不再转发。
币种 ticker
| 币种 | 网络 | pay_currency | network | 最低金额 |
|---|---|---|---|---|
| USDT | TRON | usdttrc20 | trx | $1 |
| USDT | BNB Chain | usdtbsc usdtbep20 | bsc | $1 |
| USDC | BNB Chain | usdcbsc usdcbep20 | bsc | $1 |
| USDT | Ethereum | usdterc20 | eth | $5 |
| USDC | Ethereum | usdc usdcerc20 | eth | $5 |
| USDC | Base | usdcbase | base | $1 |
| USDT | Polygon | usdtmatic usdtpolygon | matic | $1 |
| USDC | Polygon | usdcmatic usdcpolygon | matic | $1 |
| USDT | Arbitrum | usdtarb | arbitrum | $1 |
| USDC | Arbitrum | usdcarb | arbitrum | $1 |
| USDT | Optimism | usdtop | op | $1 |
| USDC | Optimism | usdcop | op | $1 |
| USDT | Avalanche | usdtarc20 usdtavax / usdtavaxc | avaxc | $1 |
| USDC | Avalanche | usdcarc20 usdcavax / usdcavaxc | avaxc | $1 |
| USDT | X Layer | usdtxlayer | xlayer | $1 |
| USDC | X Layer | usdcxlayer | xlayer | $1 |
| BTC | Bitcoin | btc | btc | $5 |
| LTC | Litecoin | ltc | ltc | $1 |
| TRX | TRON | trx | trx | $1 |
| BNB | BNB Chain | bnbbsc bnb | bsc | $1 |
| ETH | Ethereum | eth | eth | $5 |
| ETH | Base | ethbase | base | $1 |
| ETH | Arbitrum | etharb | arbitrum | $1 |
| ETH | Optimism | ethop | op | $1 |
| POL | Polygon | maticmainnet pol / polmainnet / matic | matic | $1 |
| AVAX | Avalanche | avaxc avax | avaxc | $1 |
| OKB | X Layer | okbxlayer | xlayer | $1 |
| USDT | Solana | usdtsol | sol | $1 |
| USDC | Solana | usdcsol | sol | $1 |
| SOL | Solana | sol | sol | $1 |
与 NOWPayments 的差异
price_currency只支持usd(以及按 1:1 的usdt/usdc),其他法币返回 400。pay_amount(指定加密币金额)、payout_address/payout_currency/payout_extra_id、customer_email不支持,传了返回 400。is_fixed_rate收下但不起作用:我们本来就锁价(BTC / LTC 等按多家交易所中位数锁 30 分钟)。is_fee_paid_by_user= true 时平台手续费由用户承担、加进pay_amount(平台费率 1% 起、按流水降档,见 价格)。- 付款最长有效 12 小时(NOWPayments 是 7 天)。
partially_paid_url会记下并原样返回,但收银台不会跳到它。 payment_id是 10 位数字;经/invoice建的账单,付款的payment_id和账单id相同,一张账单只有一笔付款(不会有parent_payment_id的重复充值)。- 只发终态和
partially_paid的 IPN(见上表),不发waiting/confirming;fee.serviceFee是平台手续费(实收 × 建单时的费率,标准 1%),其余费用都是 0。
Coinbase Commerce
官方 SDK 都能配 base URL,改一行即可(注意结尾的 /):
// Node(coinbase-commerce-node)
const { Client } = require("coinbase-commerce-node");
- Client.init(process.env.COINBASE_COMMERCE_API_KEY);
+ Client.init(process.env.COINBASE_COMMERCE_API_KEY, "https://blockwallets.io/coinbase/");
# Python(coinbase-commerce)
- client = Client(api_key=API_KEY)
+ client = Client(api_key=API_KEY, base_api_uri="https://blockwallets.io/coinbase/")
// PHP(coinbase/coinbase-commerce)
- ApiClient::init($apiKey);
+ ApiClient::init($apiKey, 'https://blockwallets.io/coinbase/');curl -X POST https://blockwallets.io/coinbase/charges \
-H "X-CC-Api-Key: $COINBASE_COMMERCE_API_KEY" -H "X-CC-Version: 2018-03-22" \
-H "content-type: application/json" \
-d '{"name":"Order #12345","description":"Pro plan","pricing_type":"fixed_price",
"local_price":{"amount":"100.00","currency":"USD"},
"redirect_url":"https://shop.example/success","cancel_url":"https://shop.example/cancel",
"metadata":{"order_id":"12345"}}'
# → 201 {"data": {"id": "…", "code": "7C7V5ECK", "hosted_url": "https://blockwallets.io/pay/inv_…", "timeline": [{"status": "NEW", …}], …}}支持的接口
| 接口 | 说明 |
|---|---|
POST /charges | 建 charge,hosted_url 是我们的收银台;有效 1 小时 |
GET /charges/:code_or_id | 按 8 位 code 或 id 查 |
GET /charges | 列表:limit(≤100)/ starting_after / ending_before / order,返回同样的 pagination 对象 |
POST /charges/:code_or_id/cancel | 取消还没见到付款的 charge |
没有:/charges/:code/resolve(返回 400,少付的单在商户后台处理)、/checkouts、/events、/invoices(返回 404)。
Webhook
Coinbase 的 webhook 地址是账户级的,我们也一样:发往你在商户后台填的回调地址(test 凭据建的单发往 test 回调地址)。body 是 Coinbase 的 {id, scheduled_for, attempt_number, event: {id, type, api_version, created_at, data}},data 是完整的 charge;头 X-CC-Webhook-Signature = HMAC-SHA256(shared secret, 原始 body),官方 SDK 的 Webhook.verifyEventBody 直接能用。重投时 event.id 不变、attempt_number 加一,按 event.id 去重即可。
import crypto from "node:crypto";
// 和官方 SDK 的 Webhook.verifySigHeader 一样:HMAC-SHA256(shared secret, 原始 body)
function verifyWebhook(rawBody, sig, sharedSecret) {
const hmac = crypto.createHmac("sha256", sharedSecret).update(rawBody, "utf8").digest("hex");
return crypto.timingSafeEqual(Buffer.from(hmac), Buffer.from(sig || ""));
}
// verifyWebhook(rawBody, req.headers["x-cc-webhook-signature"], process.env.COINBASE_WEBHOOK_SECRET)
// 官方 SDK 照用:Webhook.verifyEventBody(rawBody, signature, sharedSecret)| 事件 | 什么时候 | timeline 最后一项 |
|---|---|---|
charge:confirmed | 已收款(链上确认并核对过;多付也算) | COMPLETED |
charge:failed | 到期没付 / 你取消了 / 到期时收到的不够 | EXPIRED / CANCELED / UNRESOLVED(context UNDERPAID) |
查单时还能看到 NEW(等待付款)和 PENDING(链上见到付款、在等确认)。请按 charge:confirmed 发货:我们不发 charge:created、charge:pending、charge:delayed、charge:resolved。
与 Coinbase Commerce 的差异
pricing_type只支持fixed_price;local_price.currency只支持 USD(USDC / USDT 按 1:1)。no_price和其他法币返回 400。- 付款人在收银台选币种和网络后才有地址:
addresses只有选中的那一个网络,pricing只有local和选中后的settlement;没有exchange_rates、checkout、品牌设置。 metadata原样保存、原样返回(最多 4 KB)。- 退款不出现在 timeline 里,在商户后台看。
测试模式
test 凭据建的单走各链的测试网,回调用 test 的 IPN secret / shared secret 签名,和 live 完全隔离:test 的 key 查不到 live 的单。
其余细节(收银台、确认数、网络与最低金额、退款)和 主文档 一样。