Saltar al contenido principal

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.

nota

El instalador FINORA-Pago-<versión>-win-x64.exe se publica en el Marketplace.

Cuándo usarlo​

Tu situaciónUsa
Tienda web o app propiaEl SDK de checkout o la página alojada.
Un sistema de caja en Windows que puede llamar HTTP a localhostFINORA Pago por API local.
Una caja que solo puede abrir una URLFINORA 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 motorizadosCobro 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​

  1. Descarga el instalador FINORA-Pago-<versión>-win-x64.exe del Marketplace e instálalo (registra el esquema finora-pago:// y deja la app en la bandeja).
  2. En Configuración (PIN de administrador, 0000 al inicio: cámbialo): URL del Merchant https://pos.finoracore.com, RIF J-00000000-0, clave API (cifrada con DPAPI, no vuelve a la pantalla; «Probar conexión»), terminal para tarjeta (por ejemplo TERM0042), 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.
  3. 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.

nota

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étodoRutaQué hace
GET/v1/salud{version, comercio, terminal, merchantAlcanzable, ventanaAbierta}
POST/v1/cobrosCrea 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}/cancelarCancela en el Merchant y responde CANCELADA (codigo: CANCELADA_POR_COMERCIO); 409 COBRO_EN_CURSO si ya no se puede.
GET/v1/cobros?desde&hasta&maxHistorial local.
POST/v1/ventana/mostrar · /v1/ventana/ocultarControla 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: true abre la ventana de pago (el cliente elige medio); mostrar: false fija riel y campos y la app solo orquesta (tarjeta empujada al terminal, pago móvil con los datos tecleados en tu caja). modo fuerza FORMAL (ventana compacta en la pantalla del cajero) o INMERSIVO (monitor entero del cliente) para ese cobro.
  • esperar: true retiene la llamada hasta el veredicto (hasta el TTL); false devuelve 201 {cobroId, sesionId, estado: "EN_CURSO", urlPago} de inmediato y consultas o esperas el callback.
  • callbackUrl: al terminar, la app hace POST con el mismo JSON del cobro y las cabeceras X-Pago-Evento: pago.cobro, X-Pago-Cobro: <cobroId> y X-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, necesita terminalId), PAGO_MOVIL (cedula, telefono, banco, clave C2P), TRANSFERENCIA (cedula, banco, cuenta de 20 dígitos), QR (cedula, telefono), CRIPTO (cedula, wallet). La cédula puede ir en pagador.documento o en campos.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}).

HTTPcodigoCuándo
400PETICION_INVALIDAmonto, referencia, riel, campos, modo, callbackUrl… (mensaje dice cuál)
400DATOS_INCOMPLETOS, MEDIO_NO_DISPONIBLE, TERMINAL_NO_ENCONTRADOel Merchant rechazó el medio fijado; se cancela la sesión y te llega su motivo
401TOKEN_LOCAL_INVALIDOfalta o no coincide X-Pago-Token
404COBRO_NO_ENCONTRADO, SIN_VENTANA
409COBRO_EN_CURSOya hay una ventana de cobro abierta (trae cobroId), o el cobro no se puede cancelar aún
503MERCHANT_INALCANZABLE, SIN_CONFIGURARno se llegó al Merchant en 15 s / falta RIF o clave

Ejemplos por lenguaje​

C#
// 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());
Java
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());
Node.js
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
Python
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"])
PowerShell
$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
VB6 / COM
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
aviso

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.