# NOWPayments 兼容接口

> 从 NOWPayments 换到 blockwallets,只改基础地址和密钥:支持的接口、IPN 回调与签名、币种代码对照、与原版的差异。
>
> HTML: https://blockwallets.io/docs/nowpayments · English: https://blockwallets.io/docs/nowpayments.md?lang=en

## NOWPayments 兼容接口

路径、`x-api-key` 请求头、请求 / 应答字段、状态和 `x-nowpayments-sig` IPN 签名都照 NOWPayments 官方文档,现有代码通常只要换基础地址、API key 和 IPN secret。从别家迁过来?看[迁移总览](https://blockwallets.io/docs/migrate?lang=zh)。

底层就是 blockwallets 的账单:收款地址是你自己的,付款页是我们的收银台,到账后链上确认、再找第二家节点核对一遍才算付款成功。下面先讲三步接入,再分别列出两家的支持范围和**与官方接口的差异**——没实现的参数会明确报错,不会悄悄忽略。

### 三步接入

- 开通商户([首页申请](https://blockwallets.io/#join)),在商户后台「开发者 › 接入与密钥」的「网站域名」里填上你的域名。回调地址(`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) |

### NOWPayments

```javascript
- 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 次。

```nodejs
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)
```

```php
$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 |
| USD1 | BNB Chain | `usd1bsc` | `bsc` | $1 |
| FDUSD | BNB Chain | `fdusdbsc` | `bsc` | $1 |
| DAI | Ethereum | `dai` daierc20 | `eth` | $5 |
| DAI | Arbitrum | `daiarb` | `arbitrum` | $1 |
| DAI | Base | `daibase` | `base` | $1 |
| BTC | Bitcoin | `btc` | `btc` | $5 |
| LTC | Litecoin | `ltc` | `ltc` | $1 |
| DOGE | Dogecoin | `doge` | `doge` | $1 |
| BCH | Bitcoin Cash | `bch` | `bch` | $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 |
| USDT | TON | `usdtton` | `ton` | $1 |
| TON | TON | `ton` | `ton` | $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% 起、按流水降档,见 [价格](https://blockwallets.io/pricing?lang=zh))。

- 付款最长有效 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。

### 测试模式

test 凭据建的单走各链的测试网,回调用 test 的 IPN secret / shared secret 签名,和 live 完全隔离:test 的 key 查不到 live 的单。

其余细节(收银台、确认数、网络与最低金额、退款)和 [主文档](https://blockwallets.io/docs/cryptomus?lang=zh) 一样。
