# REST API ของ blockwallets

> REST API ของ blockwallets: คีย์ Bearer ใบแจ้งหนี้เป็นเซนต์สหรัฐพร้อม ref แบบ idempotent บัญชีฝากเงิน Webhook ที่เซ็นด้วย HMAC-SHA256 events และการส่งซ้ำ
>
> HTML: https://blockwallets.io/docs/api?lang=th · English: https://blockwallets.io/docs/api.md?lang=en

## REST API ของเรา

API ของเราคืออินเทอร์เฟซ REST ของ blockwallets เอง: ใบแจ้งหนี้กำหนดราคาเป็นเซนต์สหรัฐ `ref` แบบ idempotent ต่อหนึ่งคำสั่งซื้อ บัญชีฝากเงินคงที่ครอบคลุมทุกเครือข่าย และ Webhook ที่เซ็นด้วย HMAC-SHA256 สร้างใบแจ้งหนี้แบบเดียวกันบนหน้าชำระเงินเดียวกันกับ[API ที่เข้ากันได้กับ Cryptomus](https://blockwallets.io/docs/cryptomus?lang=th) ด้วยค่าธรรมเนียมและยอดคงเหลือเหมือนกัน

สร้างคีย์เองได้ในแดชบอร์ดที่ “นักพัฒนา › การเชื่อมต่อและคีย์”: คีย์ Live และ Test อย่างละหนึ่งชุดพร้อม webhook signing secret ของคีย์เหล่านั้น แต่ละรายการแสดงเพียงครั้งเดียว ในรายการจะเห็นเฉพาะคำนำหน้า หากบัญชีเปิดการยืนยันสองขั้นตอน การสร้างหรือเปลี่ยนคีย์ Live จะต้องกรอกรหัส ยังไม่มีบัญชีร้านค้า? [ขอใช้งาน](https://blockwallets.io/?lang=th#join) ใช้ Cryptomus อยู่แล้ว? ดู [API ที่เข้ากันได้กับ Cryptomus](https://blockwallets.io/docs/cryptomus?lang=th) ([วิธีเลือก](https://blockwallets.io/docs?lang=th#which))

### คีย์และคำขอ

Base URL `https://blockwallets.io` คำขอและการตอบกลับเป็น JSON ทุกคำขอมี:

- `Authorization: Bearer bw_live_…` หรือ `Bearer bw_test_…`: คำนำหน้ากำหนดโหมด คีย์ Test จึงเห็นเฉพาะใบแจ้งหนี้ Test และรันบนเทสต์เน็ต

- `Content-Type: application/json` ในคำขอ `POST`

- เก็บคีย์ไว้ที่เซิร์ฟเวอร์ของคุณเท่านั้น ห้ามใส่ในหน้าเว็บหรือแอป เราเก็บเฉพาะค่าแฮชของคีย์

ข้อผิดพลาดตอบกลับเป็นสถานะ HTTP และ `{"error": {"code": "…", "message": "…"}}`: 400 `invalid_request` (ฟิลด์หายไปหรือรูปแบบผิด), 401 `unauthorized` (ไม่มีคีย์ ไม่รู้จักคีย์ หรือคีย์ถูกปิด), 404 `not_found`, 409 `conflict` (คำขอขัดกับสถานะปัจจุบันของใบแจ้งหนี้), 422 `currency_unsupported`

### Endpoint

| Endpoint | ทำอะไร |
| --- | --- |
| `GET /v1/networks` | เครือข่ายที่รับได้ในขณะนี้ตามโหมดของคีย์นี้: ขั้นต่ำ การยืนยัน เวลาที่มักใช้ และค่าธรรมเนียมโดยประมาณ เหรียญที่มีราคาจะมีอัตราปัจจุบันมาด้วย |
| `POST /v1/invoices` | สร้างใบแจ้งหนี้ (201) หรือคืนใบเดิมของ `ref` เดียวกัน (200) |
| `GET /v1/invoices?ref=…` | ค้นหาใบแจ้งหนี้ด้วย `ref` ของคุณ |
| `GET /v1/invoices/:id` | ค้นหาใบแจ้งหนี้ด้วย id |
| `POST /v1/invoices/:id/network` | เลือกเครือข่ายของใบแจ้งหนี้เอง (หน้าชำระเงินแบบ white-label): คืน `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` | การส่ง webhook ของใบแจ้งหนี้หรือรายการฝากหนึ่งรายการ (`invoice_id` หรือ `deposit_id`) |
| `POST /v1/events/:id/redeliver` | ส่ง webhook อีกครั้ง (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` | ไม่ | เลือกเครือข่ายล่วงหน้าด้วย id จาก[ตารางเครือข่าย](https://blockwallets.io/docs/api?lang=th#networks) เว้นว่างเพื่อให้ลูกค้าเลือกบนหน้าชำระเงิน |
| `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 คืนกลับตามที่ส่งมาในการค้นหาและ webhook |
| `currency` | ไม่ | เฉพาะ `USD` (ค่าเริ่มต้น) อย่างอื่นได้ 422 |

ทุก URL ต้องอยู่ในโดเมนเว็บไซต์ของคุณโดเมนใดโดเมนหนึ่ง (แดชบอร์ด → “นักพัฒนา › การเชื่อมต่อและคีย์”) การตอบกลับคืออ็อบเจกต์ใบแจ้งหนี้ด้านล่าง `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` (ดอลลาร์สหรัฐต่อเหรียญ ล็อก 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 ทั้งหมดใช้ที่อยู่เดียวกัน) สิ่งที่ลูกค้าส่งมาจะถูกบันทึกเป็นดอลลาร์สหรัฐ พร้อม 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 ตัวอักษร เป็นตัวอักษร ตัวเลข และ `_ . : @ -` ค่าเดิมได้บัญชีเดิมเสมอ `return_url` ไม่บังคับ ต้องอยู่ในโดเมนของคุณ

- การตอบกลับมี `deposit_url` (หน้าที่เราโฮสต์ แสดงที่อยู่ คิวอาร์โค้ด และรายการฝากล่าสุด) และ `networks` (แต่ละเครือข่ายมี `id`, `address`, `min_amount`, `confirmations`)

- id รายการฝากขึ้นต้นด้วย `dep_` `amount` เป็นเซนต์สหรัฐ `amount_token` คือจำนวนเหรียญ และเหรียญที่มีราคาจะมี `rate` ที่ใช้มาด้วย

การโอนที่ต่ำกว่าขั้นต่ำ (ปกติ $1, BTC $5) จะถูกบันทึกเป็น `below_min` และไม่ถูกบันทึกยอดให้อัตโนมัติ ติดต่อเราเพื่อให้บันทึกยอดให้

### Webhook

เรา POST JSON ไปที่ URL webhook ในแดชบอร์ด (“นักพัฒนา › การเชื่อมต่อและคีย์” Live และ Test มี URL และ secret ของตัวเอง) เนื้อหาคืออีเวนต์:

```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>` โดย `v1` = HMAC-SHA256(webhook secret ของโหมดนั้น, `t` + `.` + เนื้อหาดิบ)

- `X-BW-Event`: id ของอีเวนต์ (`ev_…`) ใช้ตัดรายการซ้ำ

- `X-BW-Delivery`: ครั้งที่ส่ง เริ่มที่ 1

| อีเวนต์ | เมื่อไร |
| --- | --- |
| `invoice.paid` | ใบแจ้งหนี้เป็น `paid` หรือ `paid_over` (รวมถึงเงินที่เข้ามาหลังหมดอายุ) |
| `invoice.wrong_amount` | ใบแจ้งหนี้จบเป็น `wrong_amount` |
| `invoice.closed` | ใบแจ้งหนี้หมดอายุโดยไม่ได้รับเงินเลย หรือคุณปิดไปแล้ว |
| `deposit.credited` | การโอนเข้าบัญชีฝากเงินถูกบันทึกยอดแล้ว |

ตรวจกับเนื้อหาดิบก่อนแยกวิเคราะห์ ปฏิเสธ `t` ที่ห่างจากนาฬิกาของคุณเกิน 300 วินาที และเปรียบเทียบด้วยเวลาคงที่ `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 ครั้ง)

### Events และการส่งซ้ำ

`GET /v1/events?invoice_id=inv_…` (หรือ `deposit_id=dep_…`) แสดงรายการ webhook ของใบแจ้งหนี้หรือรายการฝากหนึ่งรายการ พร้อม `state`, `attempts`, `last_status` และ `last_error` `POST /v1/events/ev_…/redeliver` จัดคิวส่งอีกครั้งทันที เช่นหลังจากคุณแก้ endpoint แล้ว “นักพัฒนา › บันทึก Webhook” ในแดชบอร์ดแสดงทุกครั้งที่ลองส่ง พร้อมคำขอและการตอบกลับ

### โหมดทดสอบ

ใบแจ้งหนี้ที่สร้างด้วยคีย์ `bw_test_` รันบนเทสต์เน็ตและไม่แตะเงินจริง หน้าชำระเงินแสดงปุ่ม “จำลองการชำระเงิน” (เต็มจำนวน / ครึ่งหนึ่ง / จ่ายเกิน) คุณจึงดูสถานะและ webhook ได้โดยไม่ต้องมีเหรียญเทสต์เน็ต
