Integrate Finanxis into your checkouts

From zero to your first charge in 10 steps. Try everything in sandbox before charging for real.

How it works

Each of your checkouts has a fixed printed QR. When you charge, your system creates a charge; the customer scans the QR with the Finanxis app, sees your logo and the amount, and confirms. The money reaches your wallet instantly and we notify you by webhook so you can print the receipt.

1. Your checkout

POST /cobros

amount + currency

2. Finanxis

Charge PENDIENTE

the QR shows the charge

3. Customer

Scans & confirms

PIN · Face ID

4. Finanxis

Charge PAGADO

money in your wallet

5. Your server

webhook cobro.pagado

print the receipt

If the customer doesn't hold the currency you charge in, Finanxis converts it on the spot from another of their wallets. You always receive the exact amount in your currency.

1. Get your keys

In the dashboard (API keys) create a test key. You'll get two values:

ValueWhat forWhere it goes
pk_test_…Identifies your businessX-Finanxis-Key header on every request
sk_test_…Signs your requests. Shown only onceOnly on your server (env variable). Never sent
Never put the secret in an app, a website or a repository. If it leaks, revoke the key in the dashboard and create a new one.
EnvironmentURLKeys
Sandboxhttps://sandbox.finanxis.net/business/v0pk_test_ / sk_test_
Productionhttps://api.finanxis.net/business/v0pk_live_ / sk_live_

2. Sign your requests

Every request carries three headers. The signature is an HMAC-SHA256 (hex) with your secret over this text:

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

Ej: 1760106045.POST./business/v0/cobros.{"caja_id":"caja_8x2k1m","monto":21000.5,"moneda":"VES"}
HeaderValue
X-Finanxis-Keypk_test_…
X-Finanxis-TimestampCurrent Unix seconds (UTC). ±5 min accepted
X-Finanxis-SignatureThe signature in hex
Idempotency-KeyOnly when creating something. Unique per operation

Copy this helper into your project: it signs and sends any request.

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 }
}
Most common mistakes: signing a different body than the one you send (other order or spacing: sign the exact same text), using milliseconds instead of seconds, forgetting the query in the path, or a skewed server clock. If the signature fails, the error shows you the text we signed.

3. Test your integration

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" }
}
If you see success: true, your key and signature are ready. If not, data.error.como_resolver tells you exactly what to fix.

4. Set up your checkouts in the dashboard

Branches, checkouts, keys and webhooks are managed only from the dashboard; the API is only for charging. Create a branch per location and a checkout per point of sale.

  • •Copy each checkout id (caja_…) and store it in that POS's configuration: it's the caja_id you send when creating a charge.
  • •Download the checkout QR, print it and place it at the checkout. It never changes.

5. Create a charge

When the cashier closes the sale, create the charge on that checkout. The checkout's QR now shows this charge.

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);
  • •Use your ticket number as the Idempotency-Key: if the connection drops and you retry, no duplicate charge is created.
  • •A checkout has only one pending charge at a time. If there is one, cancel it (POST /cobros/{id}/cancelar) or let it expire.
  • •If the customer doesn't pay before expira_en, the charge becomes EXPIRADO and you get cobro.expirado.

6. The customer pays

Nothing to do on your side here. The customer scans the QR with the Finanxis app (or their camera), sees your logo, branch and amount, picks a wallet and confirms with PIN, Face ID or fingerprint. It's debited from their wallet and credited to yours in a single operation.

A charge is paid only once: it can never be charged twice.

7. Receive the webhook

Register an https URL in the dashboard (Webhooks) and pick the events. Store the whsec_ secret shown only once. When the customer pays, we send you 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" } }
}

Always verify the signature before marking the sale as paid. The signature is computed over the exact bytes we send, so order matters:

  • •1. Receive: Take the body as the original bytes (Buffer / raw text). Don't parse it yet.
  • •2. Verify: Compute HMAC-SHA256 of ${t}.${raw_body} with your whsec_ secret and compare it with each v1 in the header.
  • •3. Parse: Only if the signature is valid, turn the JSON into an object.
  • •4. Process: Skip the event if you already processed its evento_id, reply 2xx quickly and run your logic.
If you run express.json() (or any parser) before verifying, the signature will never match: JSON.stringify(req.body) doesn't reproduce the original bytes. Keep the raw body (as in the example) and verify against it.
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);
  }
});
  • •Reply 2xx within 10 s. Otherwise we retry at 1 min, 5 min, 30 min, 2 h and 12 h.
  • •You may get the same event more than once: store evento_id and don't process it twice.
  • •You can have several URLs, each with its own secret and events.

Common mistakes:

  • •Verifying against the already parsed body (req.body from express.json()) re-serialized with JSON.stringify: spacing and order change and the signature never matches.
  • •Keeping a single v1: during a secret rotation we send two (new and previous). Accept if any of them matches.
  • •Comparing signatures with === instead of a constant-time comparison (timingSafeEqual / hmac.compare_digest / hash_equals).

8. If the webhook doesn't arrive

As a fallback, fetch the charge. Useful if your server was down or to confirm before handing over the purchase.

const r = await finanxis("GET", "/business/v0/cobros/cob_7d2c9a");
if (r.data.estado === "PAGADO") { /* entrega la compra */ }
Don't poll every second in a loop: use the webhook as the main path. Limit: 20 requests per second per key.

9. Handle errors

Every response has the same shape. On errors, code against data.error.code (stable):

{
  "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-…"
  }
}
CodeWhat to do
AUTH_FIRMA_INVALIDACompare your signed text with the one shown in detalle.
AUTH_TIMESTAMP_INVALIDOUse Unix seconds and sync the clock (NTP).
VALIDACIONFix each item in data.error.campos.
CONFLICTO_ESTADOFetch the resource: its status may have changed.
LIMITE_PETICIONESWait Retry-After seconds.
ERROR_INTERNORetry with the same Idempotency-Key; if it persists, send us the request_id.
The full code catalog is in the API reference.

10. Go live

  • •You tested in sandbox: ping, paid charge, expired charge, canceled charge and retry with the same Idempotency-Key.
  • •Your webhook verifies the signature, replies 2xx quickly and ignores repeated events.
  • •You created a pk_live_ key and stored it as an env variable (optional: restricted to your server IP).
  • •You switched the base URL to https://api.finanxis.net and registered your production webhook.
  • •You printed the production QR for each checkout.
Questions? Email contacto@finanxis.net with the request's request_id.