Integra Finanxis en tus cajas

De cero a tu primer cobro en 10 pasos. Prueba todo en sandbox antes de cobrar de verdad.

Cómo funciona

Cada caja de tu comercio tiene un QR fijo impreso. Cuando vas a cobrar, tu sistema crea un cobro; el cliente escanea el QR con la app de Finanxis, ve tu logo y el monto, y confirma. El dinero llega a tu wallet al instante y te avisamos por webhook para que imprimas el ticket.

1. Tu caja

POST /cobros

monto + moneda

2. Finanxis

Cobro PENDIENTE

el QR muestra el cobro

3. Cliente

Escanea y confirma

PIN · Face ID

4. Finanxis

Cobro PAGADO

dinero en tu wallet

5. Tu servidor

webhook cobro.pagado

imprime el ticket

Si el cliente no tiene la moneda que cobras, Finanxis la convierte al momento desde otra de sus wallets. Tú siempre recibes el monto exacto en tu moneda.

1. Obtén tus llaves

En el dashboard (Llaves de API) crea una llave de pruebas. Recibirás dos valores:

ValorPara quéDónde va
pk_test_…Identifica a tu comercioHeader X-Finanxis-Key en cada petición
sk_test_…Firma tus peticiones. Se muestra una sola vezSolo en tu servidor (variable de entorno). Nunca se envía
Nunca pongas el secreto en una app, una web o un repositorio. Si se filtra, revoca la llave en el dashboard y crea otra.
AmbienteURLLlaves
Pruebashttps://sandbox.finanxis.net/business/v0pk_test_ / sk_test_
Producciónhttps://api.finanxis.net/business/v0pk_live_ / sk_live_

2. Firma tus peticiones

Cada petición lleva tres headers. La firma es un HMAC-SHA256 (en hex) con tu secreto sobre este texto:

${timestamp}.${METODO}.${ruta_con_query}.${body}

Ej: 1760106045.POST./business/v0/cobros.{"caja_id":"caja_8x2k1m","monto":21000.5,"moneda":"VES"}
HeaderValor
X-Finanxis-Keypk_test_…
X-Finanxis-TimestampSegundos Unix actuales (UTC). Se aceptan ±5 min
X-Finanxis-SignatureLa firma en hex
Idempotency-KeySolo al crear algo. Único por operación

Copia esta función en tu proyecto: firma y envía cualquier petición.

import crypto from "node:crypto";

const KEY = process.env.FINANXIS_KEY;       // pk_test_…
const SECRET = process.env.FINANXIS_SECRET; // sk_test_…
const BASE = "https://sandbox.finanxis.net";

export async function finanxis(metodo, ruta, bodyObj, idempotencyKey) {
  const body = bodyObj ? JSON.stringify(bodyObj) : "";
  const ts = Math.floor(Date.now() / 1000).toString();
  const firma = crypto
    .createHmac("sha256", SECRET)
    .update(`${ts}.${metodo}.${ruta}.${body}`)
    .digest("hex");

  const res = await fetch(BASE + ruta, {
    method: metodo,
    headers: {
      "Content-Type": "application/json",
      "X-Finanxis-Key": KEY,
      "X-Finanxis-Timestamp": ts,
      "X-Finanxis-Signature": firma,
      ...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}),
    },
    body: body || undefined,
  });
  return res.json(); // { success, message, data }
}
Errores más comunes: firmar un body distinto al que envías (otro orden o espacios: firma exactamente el mismo texto), usar milisegundos en vez de segundos, olvidar la query en la ruta, o el reloj del servidor desfasado. Si la firma falla, el error te muestra el texto que nosotros firmamos.

3. Prueba tu integración

const r = await finanxis("GET", "/business/v0/ping");
console.log(r);
{
  "success": true,
  "message": "Autenticación correcta. Tu integración está lista.",
  "data": { "comercio": { "id": "cm9x1…", "nombre_fantasia": "Farmacia Central" }, "ambiente": "sandbox", "hora_servidor": "2026-10-10T14:20:45.000Z" }
}
Si ves success: true, tu llave y tu firma están listas. Si no, data.error.como_resolver te dice exactamente qué corregir.

4. Configura tus cajas en el dashboard

Sucursales, cajas, llaves y webhooks se administran solo desde el dashboard; la API es únicamente para cobrar. Crea una sucursal por local y una caja por punto de cobro.

  • •Copia el id de cada caja (caja_…) y guárdalo en la configuración de ese punto de venta: es el caja_id que envías al crear un cobro.
  • •Descarga el QR de la caja, imprímelo y pégalo en la caja. No cambia nunca.

5. Crea un cobro

Cuando el cajero cierra la venta, crea el cobro en esa caja. El QR de la caja pasa a mostrar este cobro.

const cobro = await finanxis("POST", "/business/v0/cobros", {
  caja_id: "caja_8x2k1m",
  monto: 21000.5,          // número, máx. 2 decimales
  moneda: "VES",
  referencia_externa: "TICKET-55821",
  expira_en_segundos: 180,
}, "TICKET-55821");        // Idempotency-Key = tu número de ticket

if (!cobro.success) console.error(cobro.data.error);
  • •Usa tu número de ticket como Idempotency-Key: si se corta la conexión y reintentas, no se crea un cobro duplicado.
  • •Una caja tiene un solo cobro pendiente a la vez. Si ya hay uno, cancélalo (POST /cobros/{id}/cancelar) o espera a que venza.
  • •Si el cliente no paga antes de expira_en, el cobro pasa a EXPIRADO y te llega cobro.expirado.

6. El cliente paga

No tienes que hacer nada en este paso. El cliente escanea el QR con la app de Finanxis (o su cámara), ve tu logo, la sucursal y el monto, elige la wallet y confirma con PIN, Face ID o huella. Se descuenta de su wallet y se acredita en la tuya en una sola operación.

Un cobro se paga una sola vez: es imposible que se cobre dos veces.

7. Recibe el webhook

Registra una URL https en el dashboard (Webhooks) y elige los eventos. Guarda el secreto whsec_ que se muestra una sola vez. Cuando el cliente paga, te enviamos cobro.pagado:

POST https://tu-servidor.com/webhooks/finanxis
Finanxis-Signature: t=1760106090,v1=9a7f3c…
Finanxis-Event-Id: evt_7d2c…
Finanxis-Event-Type: cobro.pagado

{
  "evento_id": "evt_7d2c…",
  "evento": "cobro.pagado",
  "ambiente": "live",
  "creado_en": "2026-10-10T14:21:30Z",
  "data": { "cobro": { "id": "cob_7d2c9a", "estado": "PAGADO", "monto": 21000.5, "moneda": "VES", "referencia_externa": "TICKET-55821" } }
}

Verifica siempre la firma antes de dar la venta por pagada. La firma se calcula sobre los bytes exactos que enviamos, así que el orden importa:

  • •1. Recibir: Toma el body como bytes originales (Buffer / texto crudo). No lo parsees todavía.
  • •2. Verificar: Calcula el HMAC-SHA256 de ${t}.${body_crudo} con tu secreto whsec_ y compáralo con cada v1 del header.
  • •3. Parsear: Solo si la firma es válida, convierte el JSON en objeto.
  • •4. Procesar: Ignora el evento si ya procesaste su evento_id, responde 2xx rápido y ejecuta tu lógica.
Si usas express.json() (o cualquier parser) antes de verificar, la firma nunca coincidirá: JSON.stringify(req.body) no reproduce los bytes originales. Guarda el body crudo (como en el ejemplo) y verifica sobre él.
import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRETO = process.env.FINANXIS_WEBHOOK_SECRET; // whsec_…

// 1) RECIBIR: guarda el body crudo ANTES de que express.json() lo parsee.
//    (Si express.json() es global, este "verify" lo captura para todas las rutas.)
app.use(express.json({
  verify: (req, _res, buf) => { req.rawBody = buf.toString("utf8"); },
}));

app.post("/webhooks/finanxis", (req, res) => {
  try {
    const rawBody = req.rawBody;
    if (typeof rawBody !== "string") return res.status(500).json({ message: "Raw body no disponible" });

    // Header: "t=1760106090,v1=<firma>[,v1=<firma_anterior>]"
    const partes = (req.get("Finanxis-Signature") ?? "").split(",").map((p) => {
      const i = p.indexOf("=");
      return [p.slice(0, i).trim(), p.slice(i + 1).trim()];
    });
    const t = partes.find(([k]) => k === "t")?.[1];
    const firmas = partes.filter(([k]) => k === "v1").map(([, v]) => v);

    // Anti-reenvío: el timestamp debe tener menos de 5 minutos.
    if (!t || !/^\d+$/.test(t)) return res.sendStatus(400);
    const timestamp = Number(t);
    if (!Number.isSafeInteger(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > 300) return res.sendStatus(400);

    // 2) VERIFICAR sobre los bytes exactos recibidos.
    const esperada = crypto.createHmac("sha256", SECRETO).update(`${t}.${rawBody}`, "utf8").digest("hex");
    const valida = firmas.some((firma) =>
      /^[a-f0-9]{64}$/i.test(firma) &&
      crypto.timingSafeEqual(Buffer.from(firma, "hex"), Buffer.from(esperada, "hex")),
    );
    if (!valida) return res.status(401).json({ message: "Firma HMAC inválida" });

    // 3) PARSEAR: ya verificado, usa el objeto (express.json() ya lo dejó en req.body).
    const evento = req.body;

    // 4) PROCESAR: idempotencia por evento.evento_id y responde rápido.
    // if (await yaProcesado(evento.evento_id)) return res.sendStatus(200);
    // if (evento.evento === "cobro.pagado") { marcar venta pagada + imprimir ticket }
    return res.status(200).json({ success: true });
  } catch (error) {
    console.error("Error verificando webhook:", error);
    return res.sendStatus(500);
  }
});
  • •Responde 2xx en menos de 10 s. Si no, reintentamos a 1 min, 5 min, 30 min, 2 h y 12 h.
  • •Puede llegarte el mismo evento más de una vez: guarda evento_id y no lo proceses dos veces.
  • •Puedes tener varias URLs, cada una con su secreto y sus eventos.

Errores frecuentes:

  • •Verificar con el body ya parseado (req.body de express.json()) y volverlo a serializar con JSON.stringify: cambian espacios y orden, y la firma nunca coincide.
  • •Quedarse con un solo v1: durante una rotación de secreto enviamos dos (el nuevo y el anterior). Acepta si cualquiera coincide.
  • •Comparar firmas con === en vez de una comparación en tiempo constante (timingSafeEqual / hmac.compare_digest / hash_equals).

8. Si el webhook no llega

Como respaldo, consulta el cobro. Útil si tu servidor estuvo caído o para confirmar antes de entregar la compra.

const r = await finanxis("GET", "/business/v0/cobros/cob_7d2c9a");
if (r.data.estado === "PAGADO") { /* entrega la compra */ }
No consultes en un bucle cada segundo: usa el webhook como vía principal. Límite: 20 peticiones por segundo por llave.

9. Maneja los errores

Todas las respuestas tienen la misma forma. En los errores, programa contra data.error.code (estable):

{
  "success": false,
  "message": "Hay campos inválidos.",
  "data": {
    "error": {
      "code": "VALIDACION",
      "detalle": "Revisa 1 campo(s) en body.",
      "como_resolver": "Revisa data.error.campos…",
      "campos": [{ "campo": "monto", "mensaje": "monto debe ser un número (no texto)", "recibido": "21000", "ejemplo": 21000.5 }],
      "docs": "https://business.finanxis.net/docs#errores"
    },
    "request_id": "f324556f-…"
  }
}
CódigoQué hacer
AUTH_FIRMA_INVALIDACompara tu texto firmado con el que muestra detalle.
AUTH_TIMESTAMP_INVALIDOUsa segundos Unix y sincroniza el reloj (NTP).
VALIDACIONCorrige cada elemento de data.error.campos.
CONFLICTO_ESTADOConsulta el recurso: quizá ya cambió de estado.
LIMITE_PETICIONESEspera Retry-After segundos.
ERROR_INTERNOReintenta con la misma Idempotency-Key; si persiste, envíanos el request_id.
El catálogo completo de códigos está en la referencia de la API.

10. Sal a producción

  • •Probaste en sandbox: ping, cobro pagado, cobro expirado, cobro cancelado y reintento con la misma Idempotency-Key.
  • •Tu webhook verifica la firma, responde 2xx rápido e ignora eventos repetidos.
  • •Creaste una llave pk_live_ y la guardaste como variable de entorno (opcional: restringida a la IP de tu servidor).
  • •Cambiaste la URL base a https://api.finanxis.net y registraste tu webhook de producción.
  • •Imprimiste los QR de producción de cada caja.
¿Dudas? Escríbenos a contacto@finanxis.net con el request_id de la petición.