# blockwallets 原生 REST 接口

> blockwallets 原生 REST 接口:Bearer 密钥、按美分计价且 ref 防重的发票、充值账户、HMAC-SHA256 签名的回调、事件查询与重投。
>
> HTML: https://blockwallets.io/docs/api · English: https://blockwallets.io/docs/api.md?lang=en

## 原生 REST 接口

原生接口是 blockwallets 自己的 REST 接口:发票按美分计价,每个订单一个防重的 `ref`,充值账户一次覆盖所有网络,回调用 HMAC-SHA256 签名。它和 [Cryptomus 兼容接口](https://blockwallets.io/docs/cryptomus?lang=zh)建的是同一种账单、同一个收银台,费率和余额都一样。

密钥在商户后台「开发者 › 接入与密钥」自助生成:正式和测试各一把密钥和对应的回调签名密钥,明文只显示一次,列表里只显示前缀。开了两步验证的账户,生成或轮换正式密钥要输验证码。还没有商户账号?在[首页](https://blockwallets.io/?lang=zh#join)申请开通。已经接过 Cryptomus?看 [Cryptomus 兼容接口](https://blockwallets.io/docs/cryptomus?lang=zh)([怎么选](https://blockwallets.io/docs?lang=zh#which))。

### 密钥与请求

基础地址 `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
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` | 否 | 预选网络,填[网络表](https://blockwallets.io/docs/api#networks)里的 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` 是所选网络上的币数(小数字符串)。

```json
{
  "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
curl -X POST https://blockwallets.io/v1/deposit-accounts \
  -H "Authorization: Bearer $BW_KEY" -H "Content-Type: application/json" \
  -d '{"customer_ref":"user_42"}'
```

```json
{
  "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 各有自己的地址和密钥)。请求体就是事件:

```http
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` 和查询返回的对象一样。

```nodejs
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_` 密钥建的发票走测试网,不碰真钱。托管收银台上有「模拟付款」按钮(全额 / 一半 / 多付),不用测试币也能看到状态变化和回调。
