# API REST nativa de blockwallets

> La API REST nativa de blockwallets: claves Bearer, facturas en centavos de dólar con refs idempotentes, cuentas de depósito, webhooks firmados con HMAC-SHA256, eventos y reenvío.
>
> HTML: https://blockwallets.io/docs/api?lang=es · English: https://blockwallets.io/docs/api.md?lang=en

## API REST nativa

La API nativa es la interfaz REST propia de blockwallets: facturas con precio en centavos de dólar, un `ref` idempotente por pedido, cuentas de depósito estáticas que cubren todas las redes y webhooks firmados con HMAC-SHA256. Crea las mismas facturas en la misma página de pago alojada que la [API compatible con Cryptomus](https://blockwallets.io/docs/cryptomus?lang=es), con las mismas comisiones y el mismo saldo.

Genera tus claves tú mismo en el panel, en “Desarrolladores › Integración y claves”: una clave Live y una Test con sus claves de firma de webhooks. Cada una se muestra una sola vez; la lista solo muestra su prefijo. Si tu cuenta tiene la verificación en dos pasos activada, generar o rotar una clave Live pide un código. ¿Aún no tienes cuenta de comercio? [Solicita acceso](https://blockwallets.io/?lang=es#join). ¿Ya usas Cryptomus? Consulta la [API compatible con Cryptomus](https://blockwallets.io/docs/cryptomus?lang=es) ([cómo elegir](https://blockwallets.io/docs?lang=es#which)).

### Claves y peticiones

URL base `https://blockwallets.io`. Las peticiones y respuestas son JSON. Cada petición lleva:

- `Authorization: Bearer bw_live_…` o `Bearer bw_test_…`: el prefijo decide el modo, así que una clave Test solo ve facturas Test y funciona en testnets

- `Content-Type: application/json` en las peticiones `POST`

- Guarda las claves solo en tu servidor, nunca en una página web ni en una app; nosotros solo almacenamos su hash

Los errores devuelven un estado HTTP y `{"error": {"code": "…", "message": "…"}}`: 400 `invalid_request` (un campo ausente o mal formado), 401 `unauthorized` (clave ausente, desconocida o desactivada), 404 `not_found`, 409 `conflict` (la petición choca con el estado actual de la factura), 422 `currency_unsupported`.

### Endpoints

| Endpoint | Qué hace |
| --- | --- |
| `GET /v1/networks` | Redes que puedes aceptar ahora mismo en el modo de esta clave: mínimo, confirmaciones, tiempo de llegada habitual y comisión estimada; las monedas con precio incluyen la tasa actual |
| `POST /v1/invoices` | Crea una factura (201), o devuelve la existente para el mismo `ref` (200) |
| `GET /v1/invoices?ref=…` | Consulta una factura por tu `ref` |
| `GET /v1/invoices/:id` | Consulta una factura por id |
| `POST /v1/invoices/:id/network` | Elige tú mismo la red de una factura (página de pago de marca blanca): devuelve la `address` y el `pay_amount` que debes mostrar |
| `POST /v1/invoices/:id/close` | Cierra una factura pendiente que no ha recibido ninguna transferencia |
| `POST /v1/deposit-accounts` | Abre (201) u obtiene (200) la cuenta de depósito de un cliente: una dirección permanente en cada red |
| `GET /v1/deposit-accounts?customer_ref=…` | Consulta una cuenta de depósito por `customer_ref` |
| `GET /v1/deposit-accounts/:id` | Consulta una cuenta de depósito por id |
| `GET /v1/deposits` | Lista los depósitos de una cuenta (`account_id` o `customer_ref`, `limit` 1–200, por defecto 50) |
| `GET /v1/deposits/:id` | Consulta un depósito |
| `GET /v1/events` | Envíos de webhook de una factura o un depósito (`invoice_id` o `deposit_id`) |
| `POST /v1/events/:id/redeliver` | Vuelve a enviar un webhook (202) |

Añade `?probe=1` a la consulta de una factura o de una cuenta de depósito para comprobar la cadena en el momento (como máximo una vez cada 10 segundos); si no, obtienes el último estado de nuestro escáner, que revisa cada minuto.

### Crear una factura POST /v1/invoices

Llama a `POST /v1/invoices` cuando el cliente haga el pedido y redirígelo a la `pay_url` de la respuesta.

```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}}'
```

| Campo | Obligatorio | Descripción |
| --- | --- | --- |
| `ref` | Sí | Tu número de pedido: 1–64 caracteres entre letras, dígitos y `_ . : -`. Único por modo; si envías el mismo `ref` con los mismos campos se devuelve la misma factura, y con campos distintos devuelve 409 |
| `amount` | Sí | Importe en centavos de dólar como entero: `4200` = $42.00. No inferior al mínimo de la red una vez elegida |
| `title` | Sí | Lo que ve el cliente en la página de pago, hasta 64 caracteres |
| `return_url` | Sí | A dónde lleva “Volver” en la página de pago; debe estar en uno de los dominios de tu sitio web |
| `success_url` | No | A dónde va el cliente después de pagar; por defecto, `return_url` |
| `network` | No | Preselecciona un id de red de la [tabla de redes](https://blockwallets.io/docs/api?lang=es#networks); omítelo para que el cliente elija en la página de pago |
| `customer_ref` | No | El id de tu cliente, hasta 64 caracteres, devuelto tal cual |
| `lifetime` | No | Duración en segundos, 600 – 43200, por defecto 3600 |
| `amount_rule` | No | `"any"` = acredita lo que llegue (recargas): `paid` en cuanto se confirma. Omítelo para usar la “Regla de importe” de “Ajustes › Reglas de cobro y tarifas” en el panel |
| `accuracy_percent` | No | Cuánto pago de menos se sigue considerando pagado, 0 – 5 (por ciento), por defecto 0 |
| `subtract_percent` | No | Parte de la comisión de la plataforma que paga el cliente, 0 – 100 (por ciento), añadida al `pay_amount`; omítelo para usar el valor por defecto del panel |
| `payment_multiple` | No | Si un pago insuficiente se puede completar en la misma dirección; por defecto, el valor del panel (activado salvo que lo cambies); `false`: la primera transferencia insuficiente termina como `wrong_amount` |
| `meta` | No | Cualquier objeto JSON de hasta 2 KB, devuelto tal cual en las consultas y los webhooks |
| `currency` | No | Solo `USD` (el valor por defecto); cualquier otro devuelve 422 |

Cada URL debe estar en uno de los dominios de tu sitio web (panel → “Desarrolladores › Integración y claves”). La respuesta es el objeto factura de abajo; `network`, `address` y `pay_amount` se rellenan cuando se elige una red.

### El objeto factura

Los ids empiezan por `inv_`. `amount` está en centavos de dólar; `pay_amount` y `paid_amount` son importes de la moneda, como cadenas decimales, en la red elegida.

```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 | Significado | Qué haces tú |
| --- | --- | --- |
| `pending` | Pendiente de pago | — |
| `confirming` | Se detectó una transferencia y se está confirmando, o llegó incompleta y aún se puede completar | — |
| `paid` | Pagada | Envía / acredita el pedido |
| `paid_over` | Pagada, con pago de más | Igual; el extra está en tu saldo |
| `wrong_amount` | Terminó con menos del importe total (al caducar, o en la primera transferencia insuficiente cuando `payment_multiple` es false) | Los negocios de recargas pueden acreditar lo que llegó (`paid_amount`; las monedas con tasa también traen `paid_cents`); las tiendas piden al cliente que pague el resto |
| `closed` | Caducó sin recibir nada, o la cerraste tú | — (el dinero que llegue más tarde se acredita y se notifica igualmente) |

Otros campos: `pay_url` (la página de pago alojada), `payments` (cada transferencia con `txid`, `amount`, `confirmations` y `confirmed`), `expires_at`, `paid_at`, `closed_at`; BTC, LTC, DOGE, BCH y las monedas nativas también incluyen `rate` (dólares por moneda, bloqueada durante 30 minutos), `rate_locked_until` y `paid_cents` (lo recibido hasta ahora, en centavos de dólar).

### Redes

Pasa el id como `network`. `GET /v1/networks` devuelve las abiertas ahora mismo en tu modo; una red puede estar en pausa, así que es mejor consultarla que fijarla en el código.

| network | Moneda | Cadena | Mínimo |
| --- | --- | --- | --- |
| `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 |

### Cuentas de depósito (direcciones estáticas) POST /v1/deposit-accounts

`POST /v1/deposit-accounts` con `{"customer_ref": "user_42"}` da a ese cliente una dirección permanente en cada red (todas las cadenas EVM comparten una dirección). Todo lo que envíe se acredita en dólares estadounidenses, con un webhook `deposit.credited` por transferencia.

```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 caracteres entre letras, dígitos y `_ . : @ -`; el mismo valor devuelve siempre la misma cuenta. `return_url` opcional en tus dominios

- La respuesta incluye `deposit_url` (una página alojada con las direcciones, los códigos QR y los depósitos recientes) y `networks` (`id`, `address`, `min_amount` y `confirmations` de cada una)

- Los ids de depósito empiezan por `dep_`; `amount` está en centavos de dólar, `amount_token` es el importe en la moneda y las monedas con precio incluyen la `rate` usada

Las transferencias por debajo del mínimo (normalmente $1, BTC $5) se registran como `below_min` y no se acreditan automáticamente; contáctanos para que las acreditemos.

### Webhooks

Enviamos un POST con JSON a la URL de webhook del panel (“Desarrolladores › Integración y claves”; Live y Test tienen cada uno su propia URL y su propio secret). El cuerpo es el evento:

```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>`, donde `v1` = HMAC-SHA256(webhook secret de ese modo, `t` + `.` + cuerpo sin procesar)

- `X-BW-Event`: el id del evento (`ev_…`); deduplica por él

- `X-BW-Delivery`: el número de intento, empezando en 1

| Evento | Cuándo |
| --- | --- |
| `invoice.paid` | La factura pasó a `paid` o `paid_over` (incluido el dinero que llegó tras caducar) |
| `invoice.wrong_amount` | La factura terminó como `wrong_amount` |
| `invoice.closed` | La factura caducó sin recibir nada, o la cerraste tú |
| `deposit.credited` | Se acreditó una transferencia a una cuenta de depósito |

Verifica contra el cuerpo sin procesar antes de analizarlo, rechaza una `t` que difiera más de 300 segundos de tu reloj y compara en tiempo constante. `invoice` / `deposit` en el cuerpo es el mismo objeto que devuelve una consulta.

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

Una respuesta 2xx cuenta como entregada; si no, reintentamos tras 1 minuto, 5 minutos, 30 minutos, 2 horas, 12 horas, 24 horas y 24 horas (8 intentos en total).

### Eventos y reenvío

`GET /v1/events?invoice_id=inv_…` (o `deposit_id=dep_…`) lista los webhooks de una factura o un depósito con su `state`, `attempts`, `last_status` y `last_error`. `POST /v1/events/ev_…/redeliver` lo pone de nuevo en cola al instante, por ejemplo después de corregir tu endpoint. La página “Desarrolladores › Registro de webhooks” del panel muestra cada intento con su petición y su respuesta.

### Modo de prueba

Las facturas creadas con una clave `bw_test_` funcionan en testnets y nunca tocan dinero real. La página de pago alojada muestra un botón “Simular pago” (completo / mitad / de más), así que puedes ver los estados y los webhooks sin monedas de testnet.
