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.
POST /cobros
monto + moneda
Cobro PENDIENTE
el QR muestra el cobro
Escanea y confirma
PIN · Face ID
Cobro PAGADO
dinero en tu wallet
webhook cobro.pagado
imprime el ticket
1. Obtén tus llaves
En el dashboard (Llaves de API) crea una llave de pruebas. Recibirás dos valores:
| Valor | Para qué | Dónde va |
|---|---|---|
| pk_test_… | Identifica a tu comercio | Header X-Finanxis-Key en cada petición |
| sk_test_… | Firma tus peticiones. Se muestra una sola vez | Solo en tu servidor (variable de entorno). Nunca se envía |
| Ambiente | URL | Llaves |
|---|---|---|
| Pruebas | https://sandbox.finanxis.net/business/v0 | pk_test_ / sk_test_ |
| Producción | https://api.finanxis.net/business/v0 | pk_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"}| Header | Valor |
|---|---|
| X-Finanxis-Key | pk_test_… |
| X-Finanxis-Timestamp | Segundos Unix actuales (UTC). Se aceptan ±5 min |
| X-Finanxis-Signature | La firma en hex |
| Idempotency-Key | Solo 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 }
}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" }
}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.
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.
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 */ }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ódigo | Qué hacer |
|---|---|
| AUTH_FIRMA_INVALIDA | Compara tu texto firmado con el que muestra detalle. |
| AUTH_TIMESTAMP_INVALIDO | Usa segundos Unix y sincroniza el reloj (NTP). |
| VALIDACION | Corrige cada elemento de data.error.campos. |
| CONFLICTO_ESTADO | Consulta el recurso: quizá ya cambió de estado. |
| LIMITE_PETICIONES | Espera Retry-After segundos. |
| ERROR_INTERNO | Reintenta con la misma Idempotency-Key; si persiste, envíanos el request_id. |
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.