# Recibir pagos con criptomonedas en Node.js

> Recibe Bitcoin y USDT en una app Node.js: firma solicitudes a la API con node:crypto, crea un pago, redirige a la página de pago y verifica el webhook en Express.

Source: https://cryptolike.net/es/guides/accept-crypto-payments-nodejs
Published: 2026-09-25 · Updated: 2026-10-04 · Author: CryptoLike team

Una aplicación Node.js recibe cripto con CryptoLike en tres pasos: firma cada solicitud a la API con `node:crypto`, crea un pago para el pedido y verifica un webhook firmado antes de marcar el pedido como pagado. Basta con Node.js 18 o superior: `fetch` y `crypto` vienen incluidos, así que la integración no tiene dependencias aparte de tu framework web.

Para una tienda en un CMS no hay código que escribir: revisa el [plugin de WooCommerce](https://cryptolike.net/es/integrations/woocommerce) y las demás [integraciones](https://cryptolike.net/integrations). Esta guía es para tu propio backend: un SaaS, un marketplace o una app.

## Qué necesitas

Una cuenta de CryptoLike, un servidor con Node.js 18+ y una URL pública `https://` para los webhooks. Guarda los secretos en el entorno: los ejemplos leen `CRYPTOLIKE_API_KEY_ID`, `CRYPTOLIKE_API_SECRET` y `CRYPTOLIKE_WEBHOOK_SECRET`, además de `CRYPTOLIKE_API_URL` para la URL de la API de prueba (si no está definida, se usa producción). Desarrolla y prueba primero con claves de prueba y redes de prueba.

## 1. Crea una clave API de prueba

En el panel abre **Desarrolladores → Claves API → Crear clave** con los permisos `create_invoice` y `read`. Una clave `ck_test_` funciona con redes de prueba, donde las monedas no tienen valor. Las claves de prueba se crean en el entorno de prueba: el soporte de CryptoLike te da acceso a su panel, donde creas la misma clave, y la URL de su API (`CRYPTOLIKE_API_URL` en los ejemplos). El panel de producción emite solo claves reales, y en la red principal una clave de prueba se rechaza con `401 auth.test_key`. El secreto se muestra una sola vez; guárdalo en tu gestor de secretos o en el entorno, nunca en el código del frontend.

## 2. Firma las solicitudes a la API con node:crypto

La firma es un HMAC-SHA256 de cuatro líneas —la marca de tiempo Unix en segundos, el método, la ruta con su query y el SHA-256 del cuerpo— con el secreto de la API como clave. Esta función firma y envía una solicitud:

```ts
import { createHash, createHmac } from "node:crypto";

export async function cryptolike(method: string, path: string, payload?: unknown, idempotencyKey?: string) {
  const body = payload === undefined ? "" : JSON.stringify(payload);
  const ts = Math.floor(Date.now() / 1000).toString();
  const canonical = [ts, method.toUpperCase(), path, createHash("sha256").update(body).digest("hex")].join("\n");
  const res = await fetch(`${process.env.CRYPTOLIKE_API_URL ?? "https://api.cryptolike.net"}${path}`, {
    method,
    headers: {
      "X-API-Key": process.env.CRYPTOLIKE_API_KEY_ID!,
      "X-Timestamp": ts,
      "X-Signature": createHmac("sha256", process.env.CRYPTOLIKE_API_SECRET!).update(canonical).digest("hex"),
      "Content-Type": "application/json",
      ...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}),
    },
    body: body || undefined,
  });
  const data = await res.json();
  if (!res.ok) throw new Error(`${data.error?.code ?? res.status}: ${data.error?.message ?? ""}`);
  return data;
}
```

Serializa el cuerpo una sola vez y envía esa misma cadena: calcular el hash de una cadena y enviar otra es la causa más común de `401 auth.invalid_signature`. Compara la función con el vector de referencia de [autenticación de la API](https://cryptolike.net/docs/auth).

## 3. Crea el pago de un pedido

Crea el pago en el servidor cuando el cliente hace el pedido. Ponle precio en USD o EUR y deja que el cliente elija la moneda cripto en la página, o pasa un `coin_id` de `GET /v1/currencies`. Sin `coin_id`, la página muestra las monedas disponibles para facturas, cada una con un monto aproximado marcado con ≈; `allowed_coin_ids` (id con `invoice_enabled: true` en `GET /v1/currencies` firmado) limita la lista para este pedido. Con el ID del pedido como `Idempotency-Key`, un reintento devuelve el mismo pago:

```ts
const invoice = await cryptolike("POST", "/v1/invoices", {
  amount: "49.90",
  currency: "USD",
  order_id: order.id,
  idempotency_by_order_id: true,
  return_url: `https://example.com/orders/${order.id}`,
}, `order-${order.id}`);

await orders.saveInvoiceId(order.id, invoice.id);
```

Mantén los montos como cadenas de punta a punta: `"49.90"`, nunca un número de JavaScript.

## 4. Envía al cliente a la página de pago

Redirige el navegador a `invoice.checkout_url`, por ejemplo con `res.redirect(303, invoice.checkout_url)` en Express. La página de pago muestra el monto, la red, la dirección con código QR, el tiempo restante y el estado en vivo. Para que el cliente siga en tu propio flujo de compra, incrusta la página con el [widget de pago](https://cryptolike.net/docs/widget); el navegador solo ve el ID de la factura.

## 5. Verifica el webhook en Express

Agrega un endpoint de webhook en **Desarrolladores → Webhooks** con los eventos `invoice.*`. Lee el cuerpo sin procesar —`express.json()` lo volvería a serializar y rompería la firma—, verifícalo, descarta duplicados por `event_id` y responde rápido:

```ts
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";

app.post("/webhooks/cryptolike", express.raw({ type: "application/json" }), async (req, res) => {
  const ts = String(req.header("X-Timestamp") ?? "");
  const got = Buffer.from(String(req.header("X-Signature") ?? ""));
  const secret = process.env.CRYPTOLIKE_WEBHOOK_SECRET ?? "";
  const want = Buffer.from("sha256=" + createHmac("sha256", secret).update(`${ts}.`).update(req.body).digest("hex"));
  const fresh = /^\d+$/.test(ts) && Math.abs(Date.now() / 1000 - Number(ts)) <= 300;
  if (!secret || !fresh || got.length !== want.length || !timingSafeEqual(got, want)) return res.sendStatus(401);

  const event = JSON.parse(req.body.toString("utf8"));
  if (await events.firstTime(event.event_id)) {
    if (event.type === "invoice.paid") await orders.markPaid(event.data.invoice.order_id);
  }
  res.sendStatus(200);
});
```

Cada evento se entrega al menos una vez y puede llegar en desorden: confía en el estado dentro de `data`, no en el orden de llegada. El procedimiento completo está en [cómo verificar los webhooks de pagos cripto](https://cryptolike.net/guides/verify-crypto-payment-webhooks).

## 6. Prueba en una red de prueba y pasa a producción

Crea un pago con la clave de prueba y págalo con monedas de prueba en la red que indica la página. Deberías ver `invoice.pending` y después `invoice.paid`. Maneja `invoice.underpaid` (el cliente puede completar el pago a la misma dirección hasta el vencimiento), `invoice.expired` (ofrece un pago nuevo) e `invoice.reverted` (retén la entrega del pedido). Luego cambia a una clave real y sigue la lista de verificación para producción de la [guía para desarrolladores](https://cryptolike.net/docs).

## Errores comunes y cómo corregirlos

| Error | Qué pasó | Qué hacer |
|---|---|---|
| `401 auth.invalid_signature` | La cadena firmada no coincide con la solicitud | Calcula el hash de la cadena exacta del cuerpo que envías; usa segundos, no los milisegundos de `Date.now()` |
| `401 auth.replay` | Dos solicitudes idénticas en el mismo segundo | Espera al segundo siguiente y vuelve a firmar |
| `401` en tu webhook | El cuerpo se parseó antes de la verificación | Usa `express.raw()` solo en la ruta del webhook |
| `409 conflict` | El pedido ya tiene un pago por otro monto | Usa `details.invoice_id`; no crees un segundo pago |
| `422 no_allowed_coins` | Ninguna moneda de `allowed_coin_ids` está disponible para facturas en esta divisa | Envía otros id u omite el campo para ofrecer todas las monedas |
| `429 rate_limited` | Se superó el límite de solicitudes de la clave (600 por minuto por defecto) | Espera los segundos que indica `Retry-After` |

## ¿Prefieres un SDK?

El [SDK oficial de TypeScript](https://cryptolike.net/integrations/sdk-typescript) para Node.js 18+ envuelve estas llamadas con recursos tipados, reintentos ante `429` y `5xx`, un `Idempotency-Key` en cada `POST` y `verifyWebhook()`. No tiene dependencias en tiempo de ejecución y se niega a correr en un navegador, así que el secreto no puede terminar en el código del frontend.

## Preguntas frecuentes

### ¿Puedo crear pagos de CryptoLike desde el navegador?

No. El secreto de la API firma las solicitudes en nombre de tu cuenta, así que debe quedarse en el servidor. Tu frontend le pide a tu backend que cree el pago y recibe solo el ID de la factura o el `checkout_url`. Por eso el SDK oficial de TypeScript lanza un error cuando detecta un navegador, y el widget de pago solo lee datos públicos del pago.

### ¿Por qué falla la verificación de la firma del webhook en Express?

Casi siempre porque `express.json()` parseó el cuerpo antes de la verificación: la firma cubre los bytes originales, y el JSON vuelto a serializar es distinto. Monta `express.raw({ type: "application/json" })` en la ruta del webhook, verifica primero y parsea después. Revisa también que el secreto sea el de ese endpoint y que el reloj de tu servidor esté sincronizado.

### ¿Tengo que consultar el estado del pago periódicamente?

No. Los webhooks informan cada cambio de estado, firmados y con reintentos durante unas 31 horas si tu servidor no responde. Consultar `GET /v1/invoices/{id}` cada pocos segundos también funciona, por ejemplo en la página a la que vuelve el cliente, pero entrega los pedidos con el webhook `invoice.paid` verificado, no con un evento del navegador.
