Saltar al contenido principal

Empezar en 10 minutos

Cuando tu cliente pulsa «Pagar», FINORA toma el control: muestra los medios que tu comercio tiene habilitados (tarjeta en tu terminal, pago móvil C2P, transferencia), guía la captura, autoriza contra el banco y te devuelve un veredicto firmado. Tú nunca ves una tarjeta ni una clave de pago.

Son cuatro pasos. Los dos primeros ocurren en tu servidor; el tercero, en la página del cliente; el cuarto, otra vez en tu servidor.

tu servidor ──(1) POST /api/v1/checkout/sesiones + X-API-Key──▶ FINORA ──▶ { sesionId, token, urlPago }
tu página ──(2) redirige a urlPago o incrusta <finora-checkout>──▶ el cliente paga
FINORA ──(3) POST webhook firmado ──▶ tu servidor marca el pedido como pagado
tu página ──(4) returnUrl / onResultado ──▶ «gracias»

Antes de empezar​

NecesitasDónde
Una clave API de tu comercio (X-API-Key)Portal → Claves API con alcance INTEGRACION, o el botón «Crear clave sandbox» en la portada del Centro de desarrolladores. Se muestra una sola vez. Si lo que quieres es instalar una caja o un terminal, no es una clave: es Enrolar equipo.
El secreto de webhookCentro de desarrolladores → Webhooks → Regenerar. También una sola vez.
Opcional: el TID de un terminalSi quieres ofrecer tarjeta. Tus terminales: los terminales de prueba que FINORA asigne a tu comercio. Sin terminal, el checkout ofrece los demás medios.
nota

La clave API vive solo en tu servidor. El navegador recibe un token de sesión que solo sirve para esa sesión, no puede crear otras ni cambiar el monto, y vence con ella.

Paso 1: crea la sesión desde tu servidor​

Tu servidor crea la sesión con el monto y la referencia del pedido. FINORA devuelve el sesionId, un token de cliente y urlPago, la página de pago alojada por FINORA. Marca "sandbox": true mientras pruebas: mismo API, banco simulado, sin dinero real.

cURL
curl -s -X POST https://pos.finoracore.com/api/v1/checkout/sesiones \
-H "X-API-Key: $FINORA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"monto": 227.00,
"moneda": "VES",
"referencia": "PED-1042",
"descripcion": "Pedido PED-1042",
"terminalId": "TERM0042",
"pagador": { "tipoDocumento": "V", "documento": "12345678" },
"returnUrl": "https://mitienda.example/gracias",
"webhookUrl": "https://mitienda.example/finora/webhook",
"ttlSegundos": 900,
"sandbox": true
}'
Node.js
// Node 18+ (fetch nativo). Express: app.post("/api/pagos", …)
const r = await fetch("https://pos.finoracore.com/api/v1/checkout/sesiones", {
method: "POST",
headers: { "Content-Type": "application/json", "X-API-Key": process.env.FINORA_API_KEY },
body: JSON.stringify({
monto: 227.0, moneda: "VES", referencia: "PED-1042",
terminalId: "TERM0042",
returnUrl: "https://mitienda.example/gracias",
webhookUrl: "https://mitienda.example/finora/webhook",
sandbox: true,
}),
});
if (!r.ok) throw new Error((await r.json()).codigo); // PETICION_INVALIDA, CLAVE_INVALIDA…
const { sesionId, token, urlPago } = await r.json();
// Al navegador solo va esto. Guarda sesionId junto al pedido.
Java
// Java 11+ (java.net.http). Jackson para el JSON.
var cuerpo = """
{"monto": 227.00, "moneda": "VES", "referencia": "PED-1042",
"terminalId": "TERM0042",
"returnUrl": "https://mitienda.example/gracias",
"webhookUrl": "https://mitienda.example/finora/webhook", "sandbox": true}
""";
var req = HttpRequest.newBuilder(URI.create("https://pos.finoracore.com/api/v1/checkout/sesiones"))
.header("Content-Type", "application/json")
.header("X-API-Key", System.getenv("FINORA_API_KEY"))
.POST(HttpRequest.BodyPublishers.ofString(cuerpo)).build();
var res = HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString());
if (res.statusCode() != 201) throw new IllegalStateException(res.body()); // ProblemDetail con "codigo"
JsonNode sesion = new ObjectMapper().readTree(res.body());
String sesionId = sesion.get("sesionId").asText(), urlPago = sesion.get("urlPago").asText();
C#
// .NET 6+
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("X-API-Key", Environment.GetEnvironmentVariable("FINORA_API_KEY"));
var res = await http.PostAsJsonAsync("https://pos.finoracore.com/api/v1/checkout/sesiones", new {
monto = 227.00m, moneda = "VES", referencia = "PED-1042",
terminalId = "TERM0042",
returnUrl = "https://mitienda.example/gracias",
webhookUrl = "https://mitienda.example/finora/webhook",
sandbox = true,
});
if (!res.IsSuccessStatusCode) throw new Exception(await res.Content.ReadAsStringAsync());
var sesion = await res.Content.ReadFromJsonAsync<JsonElement>();
string sesionId = sesion.GetProperty("sesionId").GetString()!, urlPago = sesion.GetProperty("urlPago").GetString()!;
Python
import os, requests

r = requests.post("https://pos.finoracore.com/api/v1/checkout/sesiones",
headers={"X-API-Key": os.environ["FINORA_API_KEY"]},
json={"monto": 227.00, "moneda": "VES", "referencia": "PED-1042",
"terminalId": "TERM0042",
"returnUrl": "https://mitienda.example/gracias",
"webhookUrl": "https://mitienda.example/finora/webhook", "sandbox": True},
timeout=15)
r.raise_for_status() # 4xx → ProblemDetail con "codigo" en r.json()
sesion = r.json()
sesion_id, url_pago = sesion["sesionId"], sesion["urlPago"]
PHP
<?php
$ch = curl_init("https://pos.finoracore.com/api/v1/checkout/sesiones");
curl_setopt_array($ch, [
CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Content-Type: application/json", "X-API-Key: " . getenv("FINORA_API_KEY")],
CURLOPT_POSTFIELDS => json_encode([
"monto" => 227.00, "moneda" => "VES", "referencia" => "PED-1042",
"terminalId" => "TERM0042",
"returnUrl" => "https://mitienda.example/gracias",
"webhookUrl" => "https://mitienda.example/finora/webhook", "sandbox" => true,
]),
]);
$sesion = json_decode(curl_exec($ch), true);
if (curl_getinfo($ch, CURLINFO_HTTP_CODE) !== 201) { throw new RuntimeException($sesion["codigo"]); }
[$sesionId, $urlPago] = [$sesion["sesionId"], $sesion["urlPago"]];

Respuesta 201:

{
"sesionId": "CSAgkpH6tSLSO-58tneFeBaw",
"token": "3Y1kq0…",
"expiraEn": "2026-09-13T15:20:00.000+00:00",
"urlPago": "https://pago.finoracore.com/s/CSAgkpH6tSLSO-58tneFeBaw?t=3Y1kq0…",
"estado": "CREADA",
"medios": [ { "code": "TARJETA", "name": "Tarjeta", "requiereTerminal": true, "disponible": true, "campos": [] }, { "code": "PAGO_MOVIL", "name": "Pago móvil", "campos": [ "…" ] } ]
}

Solo monto es obligatorio. referencia es tu número de pedido o factura (hasta 64 caracteres) y vuelve en el webhook y en el recibo. ttlSegundos va de 60 a 3600 (900 por defecto).

aviso

El token solo viaja en esta respuesta: FINORA guarda su SHA-256 y nadie puede volver a verlo. Devuelve al navegador solo sesionId, token y urlPago; nunca la clave.

Paso 2: deja que el cliente pague​

Tres formas, de menos a más código. Las tres muestran la misma experiencia.

A. Redirige a urlPago. Sin escribir una línea de interfaz. Al terminar, el cliente vuelve a tu returnUrl con ?sesion=…&estado=APROBADA.

B. Una etiqueta en tu página.

<script src="https://pago.finoracore.com/sdk/finora-checkout.js"></script>

<finora-checkout sesion="CSAgkpH6tSLSO-58tneFeBaw" token="3Y1kq0…"
base-url="https://pos.finoracore.com" idioma="es" tema="claro"
style="height: 40rem"></finora-checkout>

<script>
document.querySelector("finora-checkout").addEventListener("finora-resultado", (e) => {
console.log(e.detail.estado); // APROBADA | RECHAZADA | EN_DUDA | CANCELADA | EXPIRADA
});
</script>

C. Un modal desde JavaScript (npm i @finora/checkout).

import { FinoraCheckout } from "@finora/checkout";

const { sesionId, token } = await fetch("/api/pagos", { method: "POST" }).then((r) => r.json());
FinoraCheckout.abrir({
sesionId, token, baseUrl: "https://pos.finoracore.com",
onResultado: (r) => { if (r.estado === "APROBADA") location.href = "/gracias"; },
onCancelar: () => console.log("el cliente cerró el pago"),
});

La guía Checkout detalla atributos, eventos y tema.

Paso 3: escucha el webhook (la fuente de verdad)​

onResultado y returnUrl sirven para la experiencia («gracias», vaciar el carrito). La entrega del pedido se decide con el webhook: el JavaScript del navegador lo puede alterar cualquiera; la firma HMAC no.

FINORA hace POST a tu webhookUrl al cerrar la sesión, con reintentos a 1, 5, 25 y 125 minutos si no respondes 2xx:

POST /finora/webhook HTTP/1.1
Content-Type: application/json
User-Agent: FINORA-Checkout-Webhook/1.0
X-Finora-Evento: checkout.sesion
X-Finora-Entrega: 4812
X-Finora-Signature: t=1757775912,v1=8f3c…,v0=2a71…

{"evento":"checkout.sesion","sesionId":"CSAgkpH6tSLSO-58tneFeBaw","estado":"APROBADA",
"referencia":"PED-1042","monto":227.00,"moneda":"VES","riel":"PAGO_MOVIL","codigo":"00",
"mensaje":"Aprobada","autorizacion":{"estado":"APROBADO","codigo":"00","referenciaBanco":"BK-FNR1K2X9-1","autorizador":"BANCO_SIM"},
"recibo":{"referencia":"FNR1K2X9…","referenciaComercio":"PED-1042","monto":227.00,"moneda":"VES","comercio":"tu comercio","riel":"PAGO_MOVIL","referenciaPagador":"04141234567","fecha":"…"},
"sandbox":true,"ocurridoEn":"2026-09-13T15:05:12.341Z"}

Verifica v1 = hex(HMAC-SHA256(secreto, t + "." + cuerpo)) sobre el cuerpo crudo, en tiempo constante, y rechaza si |ahora − t| > 300 s. Snippets en Node, Java, C#, Python y PHP en la guía Webhooks. Prueba tu endpoint sin pagar desde la consola de pruebas.

Paso 4: cierra el círculo​

Antes de mostrar «pedido confirmado», consulta a tu servidor (que ya recibió el webhook) o GET /api/v1/checkout/sesiones/{id} con tu clave. onResultado recibe { sesionId, estado, riel, codigo, mensaje, autorizacion, recibo }.

Prueba cada camino sin dinero​

En sandbox los céntimos del monto deciden el veredicto: 227.00 aprueba, 227.51 rechaza con 51, 227.96 queda EN_DUDA y se resuelve después con un segundo webhook. Tabla completa en Sandbox. La consola de pruebas crea la sesión por ti y te muestra los webhooks entregados con su firma y su cuerpo.

Lo que sigue​

  • Checkout web y app: atributos, eventos, modal, tema.
  • FINORA Pago: cobrar desde una caja de escritorio sin integrar rieles.
  • Cobro empujado y TMS: empujar una venta a un terminal desde tu ERP.
  • Referencia de la API: todos los endpoints, con «Probar» que copia un curl con tu clave.