API compatible con NOWPayments
Las rutas, la cabecera x-api-key, los campos de petición y respuesta, los estados y la firma IPN x-nowpayments-sig siguen la documentación de NOWPayments, así que el código existente normalmente solo necesita una URL base, una API key y un IPN secret nuevos. ¿Vienes de otra pasarela? Consulta el resumen de migración.
Por debajo es una factura normal de blockwallets: las direcciones de cobro son tuyas, la página de pago es nuestra caja alojada, y un pago solo cuenta cuando está confirmado en la cadena y contrastado con un segundo nodo. A continuación: tres pasos y después qué admite cada API y en qué se diferencia de la original: los parámetros que no implementamos devuelven un error en lugar de ignorarse en silencio.
Tres pasos
- Consigue una cuenta de comercio (solicítala en la página de inicio) y añade tus dominios en “Dominios del sitio web”, en “Desarrolladores › Integración y claves” del panel. Las URL de callback (
ipn_callback_url) y de redirección (success_url/cancel_url/redirect_url) deben estar en esos dominios o en el dominio de tu URL de callback: una clave filtrada no puede apuntar los webhooks a otro sitio. - Genera credenciales compatibles en el panel del comercio, en “Desarrolladores › Integración y claves”, un juego para live y otro para test: NOWPayments recibe una API key + IPN secret, Coinbase Commerce una API key + webhook shared secret. Se muestran una sola vez; nosotros solo guardamos hashes / texto cifrado.
- Apunta tu código a la URL base de abajo y usa las nuevas claves. Pruébalo primero en testnets con las credenciales de test y después pasa a live.
| Antes | Después | Cabecera de autenticación | Firma del webhook |
|---|---|---|---|
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();Endpoints admitidos
| Endpoint | Notas |
|---|---|
GET /status | {"message":"OK"}, no necesita clave |
GET /currencies · /merchant/coins · /full-currencies | Monedas que puedes aceptar ahora mismo (tickers abajo); fixed_rate se ignora |
GET /min-amount · GET /estimate | Mínimos y estimaciones; solo precios en USD (USDT / USDC cuentan 1:1) |
POST /payment | Crea un pago; devuelve pay_address / pay_amount al instante (marca blanca) |
POST /invoice | Crea una factura (invoice); invoice_url es nuestra caja alojada, donde el cliente elige moneda y red |
POST /invoice-payment | Elige la moneda de una factura (invoice) y devuelve el pago |
GET /payment/:id | Estado del pago |
GET /payment/ | Listado: limit (1–500) / page / orderBy / dateFrom / dateTo / invoiceId; sortBy solo created_at |
POST /auth | El listado solo necesita x-api-key, sin JWT; este devuelve un token de relleno sin comprobar para que el código de “iniciar sesión y luego listar” siga funcionando (email / contraseña no se comprueban) |
No disponibles: payouts, conversiones, sub-partners de custodia, suscripciones, update-merchant-estimate, pagos en moneda fiat. Esas rutas devuelven 404.
Callbacks IPN
Cuando un pago llega a un estado final enviamos un POST a la ipn_callback_url indicada al crearlo, con x-nowpayments-sig = HMAC-SHA512(IPN secret, el JSON ordenado recursivamente por clave), exactamente el código de verificación de NOWPayments. El cuerpo que enviamos ya es JSON compacto y ordenado, así que un HMAC sobre el cuerpo bruto también funciona (y es la opción más segura en PHP / Python, cuya recodificación de decimales muy pequeños puede diferir de JS). Los callbacks fallidos se reintentan tras 1 min, 5 min, 30 min, …: 8 intentos en 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 | Cuándo | IPN enviado |
|---|---|---|
waiting | Pendiente de pago (también una factura (invoice) sin moneda elegida todavía) | — |
confirming | Pago visto en la cadena, esperando confirmaciones | — (visible al consultar) |
finished | Pagado (incluido con pago de más; actually_paid es lo que llegó) | ✓ |
partially_paid | Insuficiente: el cliente aún puede completar el pago en la misma dirección antes de que caduque (se envía una vez en el primer pago insuficiente), y otra vez si sigue faltando al caducar | ✓ |
expired | Caducado sin pagar | ✓ |
refunded | Reembolsaste el pago completo desde el panel y se envió | — (visible al consultar) |
confirmed, sending y failed nunca aparecen: una vez confirmado pasa a finished y se acredita en tu saldo; no se reenvía nada.
Tickers de monedas
| Moneda | Red | pay_currency | network | Mínimo |
|---|---|---|---|---|
| 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 |
Diferencias con NOWPayments
price_currencyes solousd(másusdt/usdca 1:1); cualquier otra moneda fiat devuelve 400.pay_amount(un importe fijo en cripto),payout_address/payout_currency/payout_extra_idycustomer_emailno se admiten y devuelven 400.is_fixed_ratese acepta pero no tiene efecto: siempre bloqueamos la tasa (BTC / LTC, etc., a la mediana de varios exchanges, durante 30 minutos). Conis_fee_paid_by_useren true, la comisión de plataforma se suma apay_amount(la comisión de plataforma es 1%, menor con más volumen; consulta los precios).- Los pagos son válidos hasta 12 horas (NOWPayments: 7 días).
partially_paid_urlse guarda y se devuelve tal cual, pero la caja no redirige a ella. payment_ides un número de 10 dígitos; en las facturas (invoices) creadas con/invoice, elpayment_iddel pago es igual alidde la factura, y una factura tiene exactamente un pago (sin redepósitos conparent_payment_id).- Solo se envían los IPN de estado final y de
partially_paid(tabla de arriba), ninguno dewaiting/confirming;fee.serviceFeees la comisión de plataforma (el importe recibido × la tarifa cuando se creó la factura, 1% estándar), las demás comisiones son 0.
Modo de prueba
Los cargos y pagos creados con credenciales de test funcionan en la testnet de cada cadena y sus webhooks se firman con el IPN secret / shared secret de test, totalmente separados de live: una clave de test no puede ver pagos de live.
Todo lo demás (caja, confirmaciones, redes y mínimos, reembolsos) funciona como en la documentación principal.