# REST API gốc của blockwallets

> REST API gốc của blockwallets: khóa Bearer, hóa đơn tính bằng cent Mỹ với ref idempotent, tài khoản nạp tiền, webhook ký HMAC-SHA256, events và gửi lại.
>
> HTML: https://blockwallets.io/docs/api?lang=vi · English: https://blockwallets.io/docs/api.md?lang=en

## REST API gốc

API gốc là giao diện REST riêng của blockwallets: hóa đơn tính bằng cent Mỹ, một `ref` idempotent cho mỗi đơn hàng, tài khoản nạp tiền tĩnh bao phủ mọi mạng, và webhook ký bằng HMAC-SHA256. Nó tạo cùng loại hóa đơn trên cùng trang thanh toán như [API tương thích Cryptomus](https://blockwallets.io/docs/cryptomus?lang=vi), với cùng phí và số dư.

Bạn tự tạo khóa trong trang quản trị tại “Nhà phát triển › Tích hợp & khóa”: một khóa Live và một khóa Test cùng webhook signing secret của chúng. Mỗi khóa chỉ hiển thị một lần; danh sách chỉ hiện tiền tố. Nếu tài khoản bật xác minh hai bước, việc tạo hoặc xoay khóa Live sẽ yêu cầu nhập mã. Chưa có tài khoản người bán? [Đăng ký dùng](https://blockwallets.io/?lang=vi#join). Đã dùng Cryptomus? Xem [API tương thích Cryptomus](https://blockwallets.io/docs/cryptomus?lang=vi) ([cách chọn](https://blockwallets.io/docs?lang=vi#which)).

### Khóa và yêu cầu

URL gốc `https://blockwallets.io`. Yêu cầu và phản hồi đều là JSON. Mọi yêu cầu đều mang:

- `Authorization: Bearer bw_live_…` hoặc `Bearer bw_test_…`: tiền tố quyết định chế độ, nên khóa Test chỉ thấy hóa đơn Test và chạy trên testnet

- `Content-Type: application/json` với các yêu cầu `POST`

- Chỉ giữ khóa trên máy chủ của bạn, không bao giờ đặt trong trang web hay ứng dụng; chúng tôi chỉ lưu giá trị băm của khóa

Lỗi trả về mã HTTP và `{"error": {"code": "…", "message": "…"}}`: 400 `invalid_request` (thiếu hoặc sai định dạng một trường), 401 `unauthorized` (khóa thiếu, không tồn tại hoặc đã bị tắt), 404 `not_found`, 409 `conflict` (yêu cầu xung đột với trạng thái hiện tại của hóa đơn), 422 `currency_unsupported`.

### Endpoint

| Endpoint | Chức năng |
| --- | --- |
| `GET /v1/networks` | Các mạng bạn có thể nhận ngay lúc này trong chế độ của khóa này: mức tối thiểu, số xác nhận, thời gian đến thông thường và ước tính phí; coin có giá kèm tỷ giá hiện tại |
| `POST /v1/invoices` | Tạo hóa đơn (201), hoặc trả về hóa đơn đã có với cùng `ref` (200) |
| `GET /v1/invoices?ref=…` | Tra cứu hóa đơn theo `ref` của bạn |
| `GET /v1/invoices/:id` | Tra cứu hóa đơn theo id |
| `POST /v1/invoices/:id/network` | Tự chọn mạng cho hóa đơn (trang thanh toán white-label): trả về `address` và `pay_amount` để hiển thị |
| `POST /v1/invoices/:id/close` | Đóng hóa đơn đang chờ chưa có giao dịch chuyển nào |
| `POST /v1/deposit-accounts` | Mở (201) hoặc lấy (200) tài khoản nạp tiền của một khách: địa chỉ cố định trên mọi mạng |
| `GET /v1/deposit-accounts?customer_ref=…` | Tra cứu tài khoản nạp tiền theo `customer_ref` |
| `GET /v1/deposit-accounts/:id` | Tra cứu tài khoản nạp tiền theo id |
| `GET /v1/deposits` | Liệt kê các khoản nạp của một tài khoản (`account_id` hoặc `customer_ref`, `limit` 1–200, mặc định 50) |
| `GET /v1/deposits/:id` | Tra cứu một khoản nạp |
| `GET /v1/events` | Các lần gửi webhook của một hóa đơn hoặc khoản nạp (`invoice_id` hoặc `deposit_id`) |
| `POST /v1/events/:id/redeliver` | Gửi lại một webhook (202) |

Thêm `?probe=1` vào lệnh tra cứu hóa đơn hoặc tài khoản nạp tiền để kiểm tra chuỗi ngay lúc đó (tối đa 10 giây một lần); nếu không, bạn nhận trạng thái mới nhất từ trình quét của chúng tôi, quét mỗi phút một lần.

### Tạo hóa đơn POST /v1/invoices

Gọi `POST /v1/invoices` khi khách thanh toán và chuyển hướng họ đến `pay_url` trong phản hồi.

```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}}'
```

| Trường | Bắt buộc | Mô tả |
| --- | --- | --- |
| `ref` | Có | Mã đơn hàng của bạn: 1–64 ký tự gồm chữ cái, chữ số và `_ . : -`. Duy nhất trong mỗi chế độ; gửi lại cùng `ref` với cùng các trường sẽ trả về cùng hóa đơn, còn khác các trường thì trả về 409 |
| `amount` | Có | Số tiền tính bằng cent Mỹ, là số nguyên: `4200` = $42.00. Không thấp hơn mức tối thiểu của mạng khi đã chọn mạng |
| `title` | Có | Nội dung khách thấy trên trang thanh toán, tối đa 64 ký tự |
| `return_url` | Có | Nơi nút “Quay lại” trên trang thanh toán dẫn đến; phải thuộc một trong các tên miền website của bạn |
| `success_url` | Không | Nơi khách đến sau khi thanh toán; mặc định là `return_url` |
| `network` | Không | Chọn trước id mạng trong [bảng mạng](https://blockwallets.io/docs/api?lang=vi#networks); bỏ trống để khách tự chọn trên trang thanh toán |
| `customer_ref` | Không | Id khách hàng của bạn, tối đa 64 ký tự, được trả về nguyên văn |
| `lifetime` | Không | Thời hạn tính bằng giây, 600 – 43200, mặc định 3600 |
| `amount_rule` | Không | `"any"` = ghi có bất cứ số tiền nào đến (nạp tiền): chuyển `paid` ngay khi được xác nhận. Bỏ trống để dùng “Quy tắc số tiền” trong “Cài đặt › Quy tắc thanh toán & phí” của trang quản trị |
| `accuracy_percent` | Không | Mức thiếu tiền vẫn được tính là đã trả, 0 – 5 (phần trăm), mặc định 0 |
| `subtract_percent` | Không | Phần phí nền tảng do khách trả, 0 – 100 (phần trăm), cộng vào `pay_amount`; bỏ trống để dùng mặc định trong trang quản trị |
| `payment_multiple` | Không | Có cho phép nạp thêm phần còn thiếu vào cùng địa chỉ hay không, mặc định theo trang quản trị (bật nếu bạn chưa đổi); `false`: khoản chuyển thiếu đầu tiên kết thúc ở `wrong_amount` |
| `meta` | Không | Bất kỳ đối tượng JSON nào tối đa 2 KB, được trả về nguyên văn trong tra cứu và webhook |
| `currency` | Không | Chỉ `USD` (mặc định); giá trị khác trả về 422 |

Mọi URL phải thuộc một trong các tên miền website của bạn (trang quản trị → “Nhà phát triển › Tích hợp & khóa”). Phản hồi là đối tượng hóa đơn bên dưới; `network`, `address` và `pay_amount` được điền khi đã chọn mạng.

### Đối tượng hóa đơn

Id bắt đầu bằng `inv_`. `amount` tính bằng cent Mỹ; `pay_amount` và `paid_amount` là số lượng coin dạng chuỗi thập phân trên mạng đã chọn.

```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 | Ý nghĩa | Bạn cần làm gì |
| --- | --- | --- |
| `pending` | Chờ thanh toán | — |
| `confirming` | Đã thấy giao dịch chuyển và đang chờ xác nhận, hoặc đã chuyển thiếu và vẫn có thể nạp thêm | — |
| `paid` | Đã thanh toán | Giao hàng / ghi có cho đơn |
| `paid_over` | Đã thanh toán, và trả dư | Như trên; phần dư nằm trong số dư của bạn |
| `wrong_amount` | Kết thúc với số tiền ít hơn đầy đủ (khi hết hạn, hoặc ở khoản chuyển thiếu đầu tiên khi `payment_multiple` là false) | Dịch vụ nạp tiền có thể ghi có phần đã đến (`paid_amount`; coin tính theo tỷ giá còn có `paid_cents`); cửa hàng thì yêu cầu khách trả nốt phần còn lại |
| `closed` | Hết hạn mà chưa nhận được gì, hoặc bạn đã đóng | — (tiền đến muộn vẫn được ghi có và thông báo) |

Các trường khác: `pay_url` (trang thanh toán), `payments` (mọi khoản chuyển cùng `txid`, `amount`, `confirmations` và `confirmed`), `expires_at`, `paid_at`, `closed_at`; BTC, LTC, DOGE, BCH và các coin gốc còn có `rate` (đô la Mỹ trên mỗi coin, chốt trong 30 phút), `rate_locked_until` và `paid_cents` (phần đã đến, tính bằng cent Mỹ).

### Mạng

Truyền id vào `network`. `GET /v1/networks` trả về các mạng đang mở ngay lúc này trong chế độ của bạn; một mạng có thể bị tạm dừng, nên hãy đọc từ đó thay vì mã hóa cứng.

| network | Coin | Chuỗi | Tối thiểu |
| --- | --- | --- | --- |
| `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 |

### Tài khoản nạp tiền (địa chỉ tĩnh) POST /v1/deposit-accounts

`POST /v1/deposit-accounts` với `{"customer_ref": "user_42"}` cấp cho khách đó một địa chỉ cố định trên mọi mạng (tất cả chuỗi EVM dùng chung một địa chỉ). Mọi khoản họ gửi đều được ghi có theo đô la Mỹ, mỗi lần chuyển có một webhook `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 ký tự gồm chữ cái, chữ số và `_ . : @ -`; cùng một giá trị luôn trả về cùng một tài khoản. `return_url` tùy chọn thuộc các tên miền của bạn

- Phản hồi có `deposit_url` (trang lưu trữ sẵn với các địa chỉ, mã QR và các khoản nạp gần đây) và `networks` (mỗi mạng có `id`, `address`, `min_amount`, `confirmations`)

- Id khoản nạp bắt đầu bằng `dep_`; `amount` tính bằng cent Mỹ, `amount_token` là số lượng coin, và coin có giá kèm `rate` đã dùng

Các khoản chuyển dưới mức tối thiểu (thường là $1, BTC $5) được ghi nhận là `below_min` và không được ghi có tự động; hãy liên hệ để chúng tôi ghi có.

### Webhook

Chúng tôi POST JSON đến URL webhook trong trang quản trị (“Nhà phát triển › Tích hợp & khóa”; Live và Test mỗi bên có URL và secret riêng). Nội dung là sự kiện:

```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 seconds>,v1=<hex>`, trong đó `v1` = HMAC-SHA256(webhook secret của chế độ đó, `t` + `.` + nội dung thô)

- `X-BW-Event`: id sự kiện (`ev_…`); hãy loại trùng theo nó

- `X-BW-Delivery`: số lần gửi, bắt đầu từ 1

| Sự kiện | Khi nào |
| --- | --- |
| `invoice.paid` | Hóa đơn chuyển sang `paid` hoặc `paid_over` (kể cả tiền đến sau khi hết hạn) |
| `invoice.wrong_amount` | Hóa đơn kết thúc ở `wrong_amount` |
| `invoice.closed` | Hóa đơn hết hạn mà chưa nhận được gì, hoặc bạn đã đóng |
| `deposit.credited` | Một khoản chuyển vào tài khoản nạp tiền đã được ghi có |

Hãy xác minh trên nội dung thô trước khi phân tích, từ chối `t` lệch quá 300 giây so với đồng hồ của bạn, và so sánh trong thời gian không đổi. `invoice` / `deposit` trong nội dung là cùng đối tượng mà một lệnh tra cứu trả về.

```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));
}
```

Phản hồi 2xx được tính là đã gửi; nếu không, chúng tôi thử lại sau 1 phút, 5 phút, 30 phút, 2 giờ, 12 giờ, 24 giờ và 24 giờ (tổng cộng 8 lần).

### Events và gửi lại

`GET /v1/events?invoice_id=inv_…` (hoặc `deposit_id=dep_…`) liệt kê các webhook của một hóa đơn hoặc khoản nạp cùng `state`, `attempts`, `last_status` và `last_error`. `POST /v1/events/ev_…/redeliver` xếp hàng gửi lại ngay, ví dụ sau khi bạn đã sửa endpoint. “Nhà phát triển › Nhật ký webhook” trong trang quản trị hiển thị mọi lần thử kèm yêu cầu và phản hồi.

### Chế độ thử nghiệm

Hóa đơn tạo bằng khóa `bw_test_` chạy trên testnet và không bao giờ động đến tiền thật. Trang thanh toán hiển thị nút “Mô phỏng thanh toán” (đủ / một nửa / trả dư), để bạn xem trạng thái và webhook mà không cần có coin testnet.
