# blockwallets 네이티브 REST API

> blockwallets 네이티브 REST API: Bearer 키, 센트 단위 청구서와 멱등한 ref, 입금 계정, HMAC-SHA256 서명 웹훅, 이벤트와 재전송.
>
> HTML: https://blockwallets.io/docs/api?lang=ko · English: https://blockwallets.io/docs/api.md?lang=en

## 네이티브 REST API

네이티브 API는 blockwallets 고유의 REST 인터페이스입니다. 미국 센트 단위 청구서, 주문마다 멱등한 `ref`, 모든 네트워크를 아우르는 고정 입금 계정, HMAC-SHA256으로 서명된 웹훅을 제공합니다. [Cryptomus 호환 API](https://blockwallets.io/docs/cryptomus?lang=ko)와 같은 호스팅 결제 페이지에 같은 청구서를 만들며 수수료와 잔액도 같습니다.

키는 관리 화면의 “개발자 › 연동 및 키”에서 직접 생성합니다: Live 키와 Test 키, 그리고 각각의 웹훅 서명 시크릿. 모두 한 번만 표시되며 목록에는 접두사만 보입니다. 계정에서 2단계 인증을 켜 두었다면 Live 키를 생성하거나 교체할 때 코드를 입력해야 합니다. 아직 가맹점 계정이 없다면 [이용을 신청](https://blockwallets.io/?lang=ko#join)하세요. 이미 Cryptomus를 쓰고 있다면 [Cryptomus 호환 API](https://blockwallets.io/docs/cryptomus?lang=ko)를 보세요([고르는 방법](https://blockwallets.io/docs?lang=ko#which)).

### 키와 요청

기본 URL은 `https://blockwallets.io`입니다. 요청과 응답은 JSON이며, 모든 요청에는 다음이 들어갑니다.

- `Authorization: Bearer bw_live_…` 또는 `Bearer bw_test_…`: 접두사가 모드를 정하므로 Test 키는 Test 청구서만 볼 수 있고 테스트넷에서 동작합니다

- `POST` 요청에는 `Content-Type: application/json`

- 키는 서버에서만 보관하고 웹페이지나 앱에는 절대 넣지 마세요. 저희는 키의 해시만 저장합니다

오류는 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?lang=ko#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` | 아니요 | 2 KB 이하의 임의의 JSON 객체. 조회와 웹훅에서 그대로 돌려줍니다 |
| `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`(코인 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`(주소, QR 코드, 최근 입금 내역을 보여 주는 호스팅 페이지)과 `networks`(네트워크마다 `id`, `address`, `min_amount`, `confirmations`)가 들어갑니다

- 입금 id는 `dep_`로 시작합니다. `amount`는 미국 센트, `amount_token`은 코인 수량이며, 시세가 필요한 코인에는 적용한 `rate`가 들어갑니다

최소 금액(보통 $1, BTC $5) 미만의 송금은 `below_min`으로 기록되며 자동으로 입금 처리되지 않습니다. 입금 처리가 필요하면 문의해 주세요.

### 웹훅

관리 화면의 웹훅 URL(“개발자 › 연동 및 키”, Live와 Test는 각각 자체 URL과 시크릿이 있음)로 JSON을 POST합니다. 본문이 곧 이벤트입니다.

```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(해당 모드의 웹훅 시크릿, `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회).

### 이벤트와 재전송

`GET /v1/events?invoice_id=inv_…`(또는 `deposit_id=dep_…`)는 청구서나 입금 하나의 웹훅을 `state`, `attempts`, `last_status`, `last_error`와 함께 나열합니다. `POST /v1/events/ev_…/redeliver`는 즉시 다시 대기열에 넣습니다(예: 엔드포인트를 고친 직후). 관리 화면의 “개발자 › 웹훅 로그”에서는 모든 시도를 요청과 응답까지 볼 수 있습니다.

### 테스트 모드

`bw_test_` 키로 만든 청구서는 테스트넷에서 동작하며 실제 돈이 오가지 않습니다. 호스팅 결제 페이지에 “결제 시뮬레이션” 버튼(전액 / 절반 / 초과)이 표시되므로 테스트넷 코인 없이도 상태 변화와 웹훅을 확인할 수 있습니다.
