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
| Necesitas | Dó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 webhook | Centro de desarrolladores → Webhooks → Regenerar. También una sola vez. |
| Opcional: el TID de un terminal | Si quieres ofrecer tarjeta. Tus terminales: los terminales de prueba que FINORA asigne a tu comercio. Sin terminal, el checkout ofrece los demás medios. |
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 -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 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 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();
// .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()!;
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
$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).
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.