NOWPayments-compatible API
Paths, the x-api-key header, request and response fields, statuses and the x-nowpayments-sig IPN signature follow NOWPayments’ docs, so existing code usually needs only a new base URL, API key and IPN secret. Coming from somewhere else? See the migration overview.
Underneath it is a regular blockwallets bill: the receiving addresses are yours, the payment page is our hosted checkout, and a payment only counts once it is confirmed on chain and cross-checked against a second node. Below: three steps, then what each API supports and how it differs from the original — parameters we don't implement return an error instead of being silently ignored.
Three steps
- Get a merchant account (apply on the home page) and add your domains under “Website domains” on “Developers › Integration & keys” in the dashboard. Callback URLs (
ipn_callback_url) and redirect URLs (success_url/cancel_url/redirect_url) must be on those domains or your callback URL's domain — a leaked key can't point webhooks elsewhere. - Generate compatible credentials in the merchant dashboard under “Developers › Integration & keys”, one set for live and one for test: NOWPayments gets an API key + IPN secret, Coinbase Commerce an API key + webhook shared secret. They are shown once; we only keep hashes / ciphertext.
- Point your code at the base URL below and use the new keys. Run it on testnets with the test credentials first, then switch to live.
| Before | After | Auth header | Webhook signature |
|---|---|---|---|
https://api.nowpayments.io/v1 | https://blockwallets.io/nowpayments/v1 | x-api-key | x-nowpayments-sig (HMAC-SHA512) |
NOWPayments
- 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();Supported endpoints
| Endpoint | Notes |
|---|---|
GET /status | {"message":"OK"}, no key needed |
GET /currencies · /merchant/coins · /full-currencies | Coins you can accept right now (tickers below); fixed_rate is ignored |
GET /min-amount · GET /estimate | Minimums and estimates; USD pricing only (USDT / USDC count 1:1) |
POST /payment | Create a payment; returns pay_address / pay_amount right away (white-label) |
POST /invoice | Create an invoice; invoice_url is our hosted checkout where the customer picks coin and network |
POST /invoice-payment | Pick the coin for an invoice, returns the payment |
GET /payment/:id | Payment status |
GET /payment/ | List: limit (1–500) / page / orderBy / dateFrom / dateTo / invoiceId; sortBy only created_at |
POST /auth | The list only needs x-api-key, no JWT; this returns an unchecked placeholder token so “log in, then list” code keeps working (email / password are not checked) |
Not available: payouts, conversions, custody sub-partners, subscriptions, update-merchant-estimate, fiat payments. Those paths return 404.
IPN callbacks
When a payment reaches a final status we POST to the ipn_callback_url given at creation, with x-nowpayments-sig = HMAC-SHA512(IPN secret, the JSON sorted by key recursively) — exactly NOWPayments' verification code. The body we send is already sorted, compact JSON, so an HMAC over the raw body works too (and is the safest choice in PHP / Python, whose re-encoding of very small decimals can differ from JS). Failed callbacks are retried after 1 min, 5 min, 30 min, … — 8 attempts in total.
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)$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 | When | IPN sent |
|---|---|---|
waiting | Waiting for payment (also an invoice with no coin picked yet) | — |
confirming | Payment seen on chain, waiting for confirmations | — (visible when you query) |
finished | Paid (including overpaid; actually_paid is what arrived) | ✓ |
partially_paid | Short: the customer can still top up to the same address before it expires (sent once at the first short payment), and again if still short at expiry | ✓ |
expired | Expired unpaid | ✓ |
refunded | You refunded the whole payment from the dashboard and it was sent | — (visible when you query) |
confirmed, sending and failed never appear: once confirmed it is finished and credited to your balance; nothing is forwarded.
Currency tickers
| Coin | Network | pay_currency | network | Minimum |
|---|---|---|---|---|
| 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 |
Differences from NOWPayments
price_currencyisusdonly (plususdt/usdcat 1:1); other fiat returns 400.pay_amount(a fixed crypto amount),payout_address/payout_currency/payout_extra_idandcustomer_emailare not supported and return 400.is_fixed_rateis accepted but has no effect: we always lock the rate (BTC / LTC etc. at the median of several exchanges, for 30 minutes). Withis_fee_paid_by_usertrue the platform fee is added topay_amount(the platform fee is 1%, lower at higher volume; see pricing).- Payments are valid for up to 12 hours (NOWPayments: 7 days).
partially_paid_urlis stored and echoed back, but the checkout does not redirect to it. payment_idis a 10-digit number; for invoices created with/invoicethe payment'spayment_idequals the invoiceid, and an invoice has exactly one payment (noparent_payment_idre-deposits).- Only final-status and
partially_paidIPNs are sent (table above), nowaiting/confirmingones;fee.serviceFeeis the platform fee (the amount received × the rate when the bill was created, 1% standard), other fees are 0.
Test mode
Charges and payments created with test credentials run on each chain's testnet and their webhooks are signed with the test IPN secret / shared secret — fully separate from live: a test key can't see live payments.
Everything else (checkout, confirmations, networks and minimums, refunds) works as in the main docs.