# API compatible con NOWPayments

> Pasa de NOWPayments a blockwallets cambiando la URL base y las claves: endpoints admitidos, callbacks y firmas IPN, tickers de monedas y diferencias.
>
> HTML: https://blockwallets.io/docs/nowpayments?lang=es · English: https://blockwallets.io/docs/nowpayments.md?lang=en

## 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](https://blockwallets.io/docs/migrate?lang=es).

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](https://blockwallets.io/?lang=es#join)) 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

```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();
```

#### 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.

```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 | 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_currency` es solo `usd` (más `usdt` / `usdc` a 1:1); cualquier otra moneda fiat devuelve 400.

- `pay_amount` (un importe fijo en cripto), `payout_address` / `payout_currency` / `payout_extra_id` y `customer_email` no se admiten y devuelven 400.

- `is_fixed_rate` se acepta pero no tiene efecto: siempre bloqueamos la tasa (BTC / LTC, etc., a la mediana de varios exchanges, durante 30 minutos). Con `is_fee_paid_by_user` en true, la comisión de plataforma se suma a `pay_amount` (la comisión de plataforma es 1%, menor con más volumen; consulta los [precios](https://blockwallets.io/pricing?lang=es)).

- Los pagos son válidos hasta 12 horas (NOWPayments: 7 días). `partially_paid_url` se guarda y se devuelve tal cual, pero la caja no redirige a ella.

- `payment_id` es un número de 10 dígitos; en las facturas (invoices) creadas con `/invoice`, el `payment_id` del pago es igual al `id` de la factura, y una factura tiene exactamente un pago (sin redepósitos con `parent_payment_id`).

- Solo se envían los IPN de estado final y de `partially_paid` (tabla de arriba), ninguno de `waiting` / `confirming`; `fee.serviceFee` es 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](https://blockwallets.io/docs/cryptomus?lang=es).
