FINORA Pago para Windows
FINORA Pago es una aplicación de Windows que cobra por el Merchant en nombre de tu sistema de caja. Tu sistema no integra rieles ni terminales: le pide a FINORA Pago «cobra 227,00 por el pedido 8841» y recibe «APROBADA, autorización 123456», con ventana visible para el cajero y el cliente o sin ella.
El instalador FINORA-Pago-<versión>-win-x64.exe se publica en el Marketplace.
Cuándo usarlo
| Tu situación | Usa |
|---|---|
| Tienda web o app propia | El SDK de checkout o la página alojada. |
| Un sistema de caja en Windows que puede llamar HTTP a localhost | FINORA Pago por API local. |
| Una caja que solo puede abrir una URL | FINORA Pago por URI finora-pago://. |
| Una caja que solo puede ejecutar un proceso (VB6, .bat) | FINORA Pago por CLI. |
| Un ERP en la nube que empuja ventas a terminales de motorizados | Cobro empujado, sin app local. |
Por dentro, cada cobro es una sesión de checkout creada con la clave de tu comercio guardada cifrada en el equipo (DPAPI). Lo que se corrija en el checkout se corrige aquí.
Instalación y configuración
- Descarga el instalador
FINORA-Pago-<versión>-win-x64.exedel Marketplace e instálalo (registra el esquemafinora-pago://y deja la app en la bandeja). - En Configuración (PIN de administrador,
0000al inicio: cámbialo): URL del Merchanthttps://pos.finoracore.com, RIFJ-00000000-0, clave API (cifrada con DPAPI, no vuelve a la pantalla; «Probar conexión»), terminal para tarjeta (por ejemploTERM0042), token de la API local, puerto (9810), modo de la ventana (Inmersivo para la pantalla del cliente, Formal para la del cajero), monitor, ventana «Estado del cobro» para el cajero, sonidos, color del comercio, cierre automático del veredicto (8 s), sandbox por defecto, arranque con Windows. - Comprueba que está viva:
GET http://127.0.0.1:9810/v1/salud→{version, comercio{rif,nombre,merchantCode}, terminal, merchantAlcanzable, ventanaAbierta, cobroEnCurso, puerto, sandboxPorDefecto}.
Todo se puede fijar por variables de entorno para imágenes de instalación y MDM: PAGO_MERCHANT_URL, PAGO_RIF, PAGO_API_KEY, PAGO_TERMINAL_ID, PAGO_PUERTO, PAGO_TOKEN_LOCAL, PAGO_MOSTRAR, PAGO_MODO_VENTANA, PAGO_MONITOR, PAGO_VENTANA_CAJERO, PAGO_SONIDOS, PAGO_COLOR_COMERCIO, PAGO_CIERRE_SEGUNDOS, PAGO_ARRANCAR_CON_WINDOWS, PAGO_SANDBOX, PAGO_CORS_ORIGENES.
El puerto 9800 lo usa finora-merchant-agent (agente local para pinpad USB). FINORA Pago usa 9810 para no chocar. La API es solo loopback y sin CORS: una caja que corre en el navegador de la misma máquina lo habilita con PAGO_CORS_ORIGENES=http://localhost:5173 (nunca *).
API local (http://127.0.0.1:9810)
Solo loopback. Si configuraste un token local, todas las llamadas llevan X-Pago-Token (o Authorization: Bearer); sin él, 401 TOKEN_LOCAL_INVALIDO. Los errores son siempre {"codigo", "mensaje"}.
| Método | Ruta | Qué hace |
|---|---|---|
GET | /v1/salud | {version, comercio, terminal, merchantAlcanzable, ventanaAbierta} |
POST | /v1/cobros | Crea el cobro. 201 {cobroId, sesionId, estado: "EN_CURSO", urlPago} o, con esperar: true, 200 con el veredicto final. |
GET | /v1/cobros/{cobroId} | Veredicto o estado. |
POST | /v1/cobros/{cobroId}/cancelar | Cancela en el Merchant y responde CANCELADA (codigo: CANCELADA_POR_COMERCIO); 409 COBRO_EN_CURSO si ya no se puede. |
GET | /v1/cobros?desde&hasta&max | Historial local. |
POST | /v1/ventana/mostrar · /v1/ventana/ocultar | Controla la ventana de pago. |
Cuerpo de POST /v1/cobros:
{
"monto": 227.00, "moneda": "VES", "referencia": "8841", "descripcion": "Pedido 8841",
"items": [{ "nombre": "Kit", "cantidad": 1, "precio": 227.00 }],
"pagador": { "tipoDocumento": "V", "documento": "12345678" },
"riel": "TARJETA", "terminalId": "TERM0042",
"campos": {},
"mostrar": true, "modo": "INMERSIVO", "esperar": true, "ttlSegundos": 300,
"callbackUrl": "http://127.0.0.1:5000/pago", "sandbox": true
}
mostrar: trueabre la ventana de pago (el cliente elige medio);mostrar: falsefijarielycamposy la app solo orquesta (tarjeta empujada al terminal, pago móvil con los datos tecleados en tu caja).modofuerzaFORMAL(ventana compacta en la pantalla del cajero) oINMERSIVO(monitor entero del cliente) para ese cobro.esperar: trueretiene la llamada hasta el veredicto (hasta el TTL);falsedevuelve201 {cobroId, sesionId, estado: "EN_CURSO", urlPago}de inmediato y consultas o esperas el callback.callbackUrl: al terminar, la app hacePOSTcon el mismo JSON del cobro y las cabecerasX-Pago-Evento: pago.cobro,X-Pago-Cobro: <cobroId>yX-Pago-Firma: sha256=hex(HMAC-SHA256(tokenLocal, cuerpo crudo)); 3 reintentos (0, 2 y 8 s). Sin token configurado, la clave del HMAC es la cadena vacía.- Rieles y campos (
GET /api/v1/pagos/rieles):TARJETA(cedula, necesitaterminalId),PAGO_MOVIL(cedula,telefono,banco,claveC2P),TRANSFERENCIA(cedula,banco,cuentade 20 dígitos),QR(cedula,telefono),CRIPTO(cedula,wallet). La cédula puede ir enpagador.documentoo encampos.cedula.
Veredicto:
{ "cobroId": "c_01J…", "sesionId": "CS…", "estado": "APROBADA", "codigo": "00", "mensaje": "Aprobada",
"riel": "TARJETA", "autorizacion": { "codigo": "00", "referenciaBanco": "BK-…", "autorizador": "BANCO_SIM" },
"recibo": { "referencia": "FNR…", "referenciaComercio": "8841", "monto": 227.00, "moneda": "VES", "referenciaPagador": "455555******1234" } }
estado ∈ APROBADA · RECHAZADA · EN_DUDA · CANCELADA · EXPIRADA; codigo es el DE 39 del banco («00», «51», «05»…) o un motivo técnico (MERCHANT_INALCANZABLE). Con EN_DUDA no vuelvas a cobrar ni entregues: el Merchant la resuelve en minutos (consulta GET /v1/cobros/{id}).
| HTTP | codigo | Cuándo |
|---|---|---|
| 400 | PETICION_INVALIDA | monto, referencia, riel, campos, modo, callbackUrl… (mensaje dice cuál) |
| 400 | DATOS_INCOMPLETOS, MEDIO_NO_DISPONIBLE, TERMINAL_NO_ENCONTRADO | el Merchant rechazó el medio fijado; se cancela la sesión y te llega su motivo |
| 401 | TOKEN_LOCAL_INVALIDO | falta o no coincide X-Pago-Token |
| 404 | COBRO_NO_ENCONTRADO, SIN_VENTANA | |
| 409 | COBRO_EN_CURSO | ya hay una ventana de cobro abierta (trae cobroId), o el cobro no se puede cancelar aún |
| 503 | MERCHANT_INALCANZABLE, SIN_CONFIGURAR | no se llegó al Merchant en 15 s / falta RIF o clave |
Ejemplos por lenguaje
// C# / .NET: cobro síncrono con ventana
using var http = new HttpClient { BaseAddress = new Uri("http://127.0.0.1:9810") };
http.DefaultRequestHeaders.Add("X-Pago-Token", tokenLocal);
var res = await http.PostAsJsonAsync("/v1/cobros", new { monto = 227.00m, referencia = "8841", mostrar = true, esperar = true, sandbox = true });
var v = await res.Content.ReadFromJsonAsync<JsonElement>();
if (v.GetProperty("estado").GetString() == "APROBADA") MarcarPagado("8841", v.GetProperty("autorizacion").GetProperty("codigo").GetString());
var req = HttpRequest.newBuilder(URI.create("http://127.0.0.1:9810/v1/cobros"))
.header("Content-Type", "application/json").header("X-Pago-Token", tokenLocal)
.POST(HttpRequest.BodyPublishers.ofString("{\"monto\":227.00,\"referencia\":\"8841\",\"mostrar\":true,\"esperar\":true,\"sandbox\":true}"))
.timeout(Duration.ofSeconds(330)).build();
var res = HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString());
JsonNode v = new ObjectMapper().readTree(res.body());
boolean aprobada = "APROBADA".equals(v.get("estado").asText());
const v = await fetch("http://127.0.0.1:9810/v1/cobros", {
method: "POST",
headers: { "Content-Type": "application/json", "X-Pago-Token": tokenLocal },
body: JSON.stringify({ monto: 227.0, referencia: "8841", mostrar: true, esperar: true, sandbox: true }),
}).then((r) => r.json());
console.log(v.estado, v.codigo); // APROBADA 00
import requests
v = requests.post("http://127.0.0.1:9810/v1/cobros", headers={"X-Pago-Token": token_local},
json={"monto": 227.00, "referencia": "8841", "mostrar": True, "esperar": True, "sandbox": True},
timeout=330).json()
print(v["estado"], v["codigo"])
$v = Invoke-RestMethod -Method Post -Uri http://127.0.0.1:9810/v1/cobros -Headers @{ "X-Pago-Token" = $tokenLocal } `
-ContentType "application/json" -Body '{"monto":227.00,"referencia":"8841","mostrar":true,"esperar":true,"sandbox":true}' -TimeoutSec 330
$v.estado
Dim http As Object: Set http = CreateObject("WinHttp.WinHttpRequest.5.1")
http.Open "POST", "http://127.0.0.1:9810/v1/cobros", False
http.SetRequestHeader "Content-Type", "application/json"
http.SetRequestHeader "X-Pago-Token", tokenLocal
http.SetTimeouts 5000, 5000, 5000, 330000
http.Send "{""monto"":227.00,""referencia"":""8841"",""mostrar"":true,""esperar"":true,""sandbox"":true}"
If InStr(http.ResponseText, """estado"":""APROBADA""") > 0 Then MarcarPagado "8841"
URI y CLI
Para cajas que solo abren una URL (ShellExecute, start, un <a> en una intranet). Mismos parámetros que POST /v1/cobros; los campos del riel como campo.<code>=, el pagador como pagador.<campo>=, callback = callbackUrl, ttl = ttlSegundos. No hay respuesta por la URI: el veredicto llega por el callback o se consulta por GET /v1/cobros.
finora-pago://cobrar?monto=227.00&referencia=8841&callback=http%3A%2F%2F127.0.0.1%3A5000%2Fpago
finora-pago://cobrar?monto=227.00&referencia=8841&mostrar=false&riel=PAGO_MOVIL&campo.telefono=04141234567&campo.banco=0102&campo.clave=1234&pagador.documento=12345678&pagador.tipoDocumento=V&sandbox=true
finora-pago://cobrar?monto=64.90&referencia=8842&modo=formal&callback=http%3A%2F%2F127.0.0.1%3A5000%2Fpago
Para cajas que ejecutan procesos: la CLI siempre espera el veredicto e imprime el JSON del cobro (o {"error":{codigo,mensaje}}). Códigos de salida: 0 aprobada · 1 rechazada, cancelada o expirada · 2 en duda · 3 error. Si FINORA Pago no está abierta, la lanza y espera hasta 20 s.
finora-pago.exe cobrar --monto 227.00 --referencia 8841 --json
finora-pago.exe cobrar --monto 64.90 --referencia 8842 --modo formal --json
finora-pago.exe cobrar --monto 15.00 --referencia 8843 --mostrar false --riel TARJETA --terminalId TERM0042 --json
finora-pago.exe consultar CBf0Eu90y93C78 --esperar --json
finora-pago.exe cancelar CBf0Eu90y93C78 --json
finora-pago.exe salud --json
Un .exe de Electron es gráfico: su salida llega intacta cuando lo lanzas con tuberías (Process.Start + RedirectStandardOutput, execFile, subprocess.run). Tecleado a mano en cmd.exe puede no verse: redirige a un archivo (finora-pago.exe salud --json > salida.json).
Callback firmado
POST /pago HTTP/1.1
Content-Type: application/json
User-Agent: FINORA-Pago-Callback/1.0
X-Pago-Evento: pago.cobro
X-Pago-Cobro: CBf0Eu90y93C78
X-Pago-Firma: sha256=f44ebc9873f965313dadb3804003d9cbccec91f515342aee3fa20bf4ad33b396
{"evento":"pago.cobro","cobroId":"CBf0Eu90y93C78","estado":"APROBADA", …el mismo JSON del cobro… }
// Verificar X-Pago-Firma en tu receptor local (Node, express.raw)
import crypto from "node:crypto";
const esperada = "sha256=" + crypto.createHmac("sha256", tokenLocal).update(cuerpoCrudo).digest("hex");
const recibida = req.get("X-Pago-Firma") ?? "";
const valida = esperada.length === recibida.length && crypto.timingSafeEqual(Buffer.from(esperada), Buffer.from(recibida));
Vector de prueba: token secreto, cuerpo {"a":1}; si tu HMAC no coincide con sha256= + hex, estás firmando otra cosa (espacios, JSON reparseado). Responde 2xx rápido; si tu caja estaba caída, el veredicto sigue en GET /v1/cobros/{id}.
Sandbox
"sandbox": true en la petición. Mismas reglas que el checkout: los céntimos deciden (.51 rechaza, .96 en duda) y las tarjetas de prueba del banco simulado valen en el terminal. Tabla en Sandbox.
Diagnóstico
Bitácora con códigos en %APPDATA%\finora-pago\bitacora.log, pantalla «Estado» en la bandeja y GET /v1/salud para que tu caja compruebe que la app está viva antes de cobrar.