# NOWPayments 호환 API

> 기본 URL과 키만 바꿔 NOWPayments에서 blockwallets로 옮기세요: 지원하는 엔드포인트, IPN 콜백과 서명, 통화 티커, 차이점.
>
> HTML: https://blockwallets.io/docs/nowpayments?lang=ko · English: https://blockwallets.io/docs/nowpayments.md?lang=en

## NOWPayments 호환 API

경로, `x-api-key` 헤더, 요청과 응답 필드, 상태, `x-nowpayments-sig` IPN 서명이 NOWPayments 문서를 따르므로 기존 코드는 대개 새 기본 URL, API key, IPN secret만 있으면 됩니다. 다른 곳에서 오셨나요? [이전 개요](https://blockwallets.io/docs/migrate?lang=ko)를 참고하세요.

내부적으로는 일반 blockwallets 청구서입니다. 수신 주소는 당신의 것이고, 결제 페이지는 저희가 호스팅하는 결제 페이지이며, 결제는 체인에서 컨펌되고 두 번째 노드와 교차 확인된 뒤에야 인정됩니다. 아래에서 세 단계를 설명하고, 이어서 각 API가 지원하는 범위와 **원래 서비스와 어떻게 다른지**를 설명합니다. 구현하지 않은 매개변수는 조용히 무시하지 않고 오류를 돌려줍니다.

### 세 단계

- 가맹점 계정을 만들고([홈페이지에서 신청](https://blockwallets.io/?lang=ko#join)) 관리 화면 “개발자 › 연동 및 키”의 “웹사이트 도메인”에 도메인을 추가합니다. 콜백 URL(`ipn_callback_url`)과 리디렉션 URL(`success_url` / `cancel_url` / `redirect_url`)은 이 도메인이나 콜백 URL의 도메인에 속해야 합니다. 키가 유출되어도 웹훅을 다른 곳으로 보낼 수 없습니다.

- 가맹점 관리 화면의 “개발자 › 연동 및 키”에서 호환 자격 증명을 만듭니다. 실서비스용 한 세트와 테스트용 한 세트입니다. NOWPayments는 **API key + IPN secret**, Coinbase Commerce는 **API key + 웹훅 공유 시크릿**을 받습니다. 한 번만 표시되며 저희는 해시 / 암호문만 보관합니다.

- 코드가 아래 기본 URL을 가리키게 하고 새 키를 사용합니다. 먼저 테스트 자격 증명으로 테스트넷에서 실행해 본 다음 실서비스로 전환하세요.

| 이전 | 이후 | 인증 헤더 | 웹훅 서명 |
| --- | --- | --- | --- |
| `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` | 지금 받을 수 있는 코인(아래 티커). `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는 필요 없습니다. 이 엔드포인트는 검증하지 않는 자리표시자 토큰을 돌려주어 “로그인 후 목록 조회” 코드가 계속 동작하게 합니다(이메일 / 비밀번호는 검사하지 않음) |

제공하지 않음: 지급(payout), 환전, 수탁 하위 파트너, 구독, `update-merchant-estimate`, 법정화폐 결제. 이 경로는 404를 돌려줍니다.

#### IPN 콜백

결제가 최종 상태에 도달하면 생성할 때 지정한 `ipn_callback_url`로 POST하며, `x-nowpayments-sig` = HMAC-SHA512(IPN secret, 키 기준으로 재귀 정렬한 JSON)입니다. NOWPayments의 검증 코드와 똑같습니다. 저희가 보내는 본문은 이미 정렬된 compact JSON이므로 원문 본문에 대한 HMAC도 통합니다(아주 작은 소수를 다시 인코딩하면 JS와 결과가 다를 수 있는 PHP / Python에서는 이쪽이 가장 안전합니다). 실패한 콜백은 1분, 5분, 30분, … 뒤에 재시도하며 총 8번 시도합니다.

```nodejs
import crypto from "node:crypto";

// Same as the NOWPayments docs (our body is already sorted, so hashing the raw body works too)
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);  // the recursive ksort from the NOWPayments docs
$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`가 되어 잔액에 입금되며 다른 곳으로 전달하지 않습니다.

#### 통화 티커

| 코인 | 네트워크 | 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`만 가능합니다(`usdt` / `usdc`는 1:1로 추가 허용). 다른 법정화폐는 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=ko) 참고).

- 결제 유효 시간은 최대 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입니다.

### 테스트 모드

테스트 자격 증명으로 만든 charge와 결제는 각 체인의 테스트넷에서 동작하며 웹훅은 테스트 IPN secret / 공유 시크릿으로 서명됩니다. 실서비스와 완전히 분리되어 있어 테스트 키로는 실서비스 결제를 볼 수 없습니다.

나머지(결제 페이지, 컨펌, 네트워크와 최소 금액, 환불)는 [메인 문서](https://blockwallets.io/docs/cryptomus?lang=ko)와 같습니다.
