Ir al contenido
FINORA Academy
SistemaFINORA Core

FINORA Core · Conceptos

El gateway de API del core: una puerta pública para sistemas externos

ImplementadoImplementado

El problema: cada sistema quiere entrar por su propia puerta

Sección titulada «El problema: cada sistema quiere entrar por su propia puerta»

Un banco no vive solo de su core. El ERP de contabilidad corporativa quiere leer el plan de cuentas, el conciliador quiere los asientos del día, una fintech aliada quiere abrir cuentas y una app quiere saldos. Si cada uno entra directo al core, con su propio usuario y sus propias reglas, el banco pierde el control: no sabe quién llamó, no puede limitar cuánto llama y un reintento mal hecho duplica un asiento.

El gateway de API del core es esa puerta. Expone una API pública y estable bajo /v1, separada de la API interna del core, y en cada petición hace el trabajo del portero: identifica al sistema, comprueba qué puede hacer, decide a qué institución va, frena los excesos, evita los duplicados y traduce los errores a un formato único.

flowchart LR
  C["Sistema del banco<br/>(ERP, app, conciliador)"] -->|"HTTPS + token o apiKey"| G
  subgraph G["Gateway de API del core"]
    direction TB
    G1["Correlación<br/>X-Request-Id"] --> G2["Versión de la API"]
    G2 --> G3["Credencial<br/>(token o apiKey)"]
    G3 --> G4["Institución y entorno<br/>subdominio o X-Finora-Tenant"]
    G4 --> G5["Límite de peticiones<br/>por cliente e institución"]
    G5 --> G6["Alcance por dominio"]
    G6 --> G7["Idempotencia<br/>en las escrituras"]
    G7 --> G8["Circuit breaker<br/>por institución"]
  end
  G8 -->|"credencial propia de la institución"| K["FINORA Core<br/>de esa institución"]

El sistema externo nunca conoce la credencial con la que el gateway entra al core: esa la guarda el operador del gateway, una por institución. Así el banco puede revocar a un consumidor sin tocar a los demás.

Contamos los puntos de entrada en el código del gateway: 86 operaciones. Una es el token, que acepta el cuerpo en dos formatos (formulario y JSON). De las restantes, 18 responden 501 a propósito: son las que una API de core bancario suele tener pero el core no ofrece por API (modificar o borrar un cliente, castigar un crédito, crear una cuenta contable…). En lugar de fallar con un 500, dicen qué hacer en su lugar.

DominioRutasQué cubre
Autenticación/v1/oauth/tokenEl token del sistema consumidor
Clientes/v1/clientsBúsqueda por texto, consulta, alta, sus cuentas y sus créditos
Captaciones/v1/deposits, /v1/deposit-productsProductos, apertura, saldo, movimientos, bloqueos, cierre, depósitos, retiros y transferencias
Créditos/v1/loans, /v1/loan-productsSimulación, solicitud, aprobación, desembolso, pagos, cuota de cancelación y cancelación
Contabilidad/v1/glaccounts, /v1/gljournalentries, /v1/gltrialbalance, /v1/transactionchannels, /v1/accounting/reportsPlan de cuentas con saldos, asientos manuales, búsqueda, reverso, balance de comprobación y reportes asíncronos. Detalle en la referencia de la API contable
Divisas/v1/fxCotización, operación y liquidación
Licencia/v1/license/modulesQué módulos tiene activos la institución
Webhooks/v1/webhooksSuscripciones, entregas y reenvío. Ver Webhooks firmados del core
Eventos/v1/events, /v1/subscriptionsLectura ordenada por cursor. Ver Consumir el streaming de eventos
Configuración/v1/configuration/accounting.yamlLa configuración contable como código. Ver la guía

Los cuerpos de las respuestas son los del core, sin renombrar campos.

Cada sistema consumidor recibe un client_id y un client_secret. Con ellos tiene dos formas de identificarse:

  • Token OAuth 2.0 (recomendada). POST /v1/oauth/token con grant_type=client_credentials. La respuesta trae access_token, token_type: Bearer, expires_in: 900 (15 minutos), scope, tenant y environment. El secreto viaja una vez cada 15 minutos, no en cada petición.
  • Cabecera apiKey. apiKey: <client_id>.<client_secret> en cada petición, sin token. Es más cómoda, pero el secreto viaja siempre: úsala solo si tu cliente no puede manejar tokens.

Con los valores por defecto, tras cinco fallos seguidos la credencial queda bloqueada 15 minutos (429 GW_LOGIN_BLOQUEADO), y tras diez credenciales inválidas desde una misma dirección en 15 minutos se bloquea esa dirección para todos (429 GW_IP_BLOQUEADA), incluso con una credencial válida. Cómo cambiar un secreto sin cortar el servicio está en Rotar una credencial de API con periodo de gracia.

Cada consumidor tiene una lista de alcances: clients:read, deposits:write, gl:read… Un GET exige el alcance :read del dominio y un POST, PUT, PATCH o DELETE el :write. Hay excepciones de sentido común: una búsqueda (gljournalentries:search), una simulación de crédito o un reporte contable son lectura aunque viajen en POST. Sin el alcance, 403 GW_ALCANCE_INSUFICIENTE.

Hay un segundo filtro, detrás: la credencial del gateway ante el core tiene sus propios permisos, y algunos son críticos (postear asientos, aprobar créditos, leer el stream de eventos). Si el gateway te deja pasar pero el core no, recibes 403 CORE_ACCESO_DENEGADO.

Un gateway puede atender a varias instituciones, cada una con su propio core. La institución se nombra de dos formas:

  • por el subdominio: https://<institucion>.<su-gateway>/v1/...;
  • por la cabecera X-Finora-Tenant: <institucion> contra el host genérico.

Si no nombras ninguna, vale la del token. Si nombras una distinta de la de tu credencial, 403 TENANT_CRUZADO: un consumidor de un banco nunca ve los datos de otro. Una institución suspendida responde 423 y un dominio que no contrató, 403 MODULO_NO_HABILITADO.

Cada institución puede tener un sandbox: otra institución en el registro, con otro core y otra base, sus propias credenciales y sus propios límites. Se pide por el subdominio <institucion>.sandbox.<su-gateway> o con la cabecera X-Finora-Environment: sandbox. El token lleva su entorno: uno de producción usado contra el sandbox, o al revés, responde 403 ENTORNO_CRUZADO. Toda respuesta autenticada trae la cabecera X-Finora-Environment para que sepas siempre dónde estás.

Toda escritura exige la cabecera Idempotency-Key con un UUID (v4 recomendado). La clave vale 6 horas:

  • La primera petición la reserva. Otra con la misma clave mientras la primera sigue en curso: 409 IDEMPOTENCIA_EN_CURSO.
  • Repetida con el mismo cuerpo, devuelve la respuesta original con Idempotent-Replayed: true, sin llamar al core. Es lo que te salva cuando se cayó la red justo después de enviar un asiento.
  • Misma clave con otro cuerpo: 422 IDEMPOTENCIA_CUERPO_DISTINTO.
  • Un rechazo (4xx) o un fallo (5xx) no se guarda: corrige y reintenta con la misma clave.
sequenceDiagram
  participant S as Tu sistema
  participant G as Gateway
  participant K as Core
  S->>G: POST asiento · Idempotency-Key: K1
  G->>K: POST (clave derivada de K1)
  K-->>G: 201 asiento
  G--xS: la respuesta se pierde en la red
  S->>G: POST mismo asiento · Idempotency-Key: K1
  G-->>S: 201 el mismo asiento · Idempotent-Replayed: true
  Note over G,K: el core no recibe la segunda petición

Las listas usan offset (desde 0) y limit (50 por defecto, hasta 1000) y la respuesta es un arreglo JSON. Con paginationDetails=ON llegan además las cabeceras Items-Total, Items-Offset e Items-Limit. Los movimientos de una cuenta y los eventos se paginan por cursor: la cabecera Items-Next-Cursor trae la posición siguiente. detailsLevel=FULL agrega datos que cuestan otra consulta al core (el saldo de una cuenta, el cronograma de un crédito).

Lo que no es crear, leer o modificar se expresa como una acción pegada al recurso con dos puntos: POST /v1/gljournalentries:search, POST /v1/gljournalentries/{voucher}:reverse, POST /v1/loans/{id}:disburse, POST /v1/webhooks/{id}:reset-circuit. Un asiento contabilizado no se borra ni se modifica: se reversa.

Todo error es application/problem+json (RFC 7807), con un codigo estable, el requestId de la petición y, para clientes que ya esperan ese esquema, errorCode (el estado HTTP), errorReason (la categoría) y errorSource (el campo o la cabecera culpable cuando se conoce):

{
"type": "urn:finora:core-gateway:solicitud-invalida",
"title": "Solicitud inválida",
"status": 400,
"detail": "La búsqueda de asientos exige un criterio {\"field\":\"bookingDate\",\"operator\":\"BETWEEN\"} con value y secondValue.",
"codigo": "RANGO_DE_FECHAS_REQUERIDO",
"requestId": "3f2c9a71e8b44d0f9b6c1a2e5d7f8a90",
"timestamp": "2026-10-19T14:05:12.481-04:00",
"errorCode": 400,
"errorReason": "INVALID_PARAMETERS",
"errorSource": "filterCriteria"
}

Los rechazos del core llegan como CORE_<TIPO> y conservan sus datos de negocio. La lista completa está en Códigos de error del gateway de API.

  • X-Request-Id. Mándalo tú (8 a 64 caracteres [A-Za-z0-9._-]) o el gateway genera uno. Vuelve en la respuesta, viaja al core y aparece en cada error: es lo que le das a soporte para encontrar tu petición.
  • Límites. Una cubeta de fichas por consumidor y otra por institución. Las cabeceras X-RateLimit-Limit, X-RateLimit-Remaining y X-RateLimit-Reset muestran la más cercana a agotarse. Al pasarte, 429 LIMITE_EXCEDIDO con Retry-After y alcanceLimite (CLIENTE o TENANT).
  • Circuit breaker por institución. Si el core de una institución empieza a fallar (errores 5xx, red o tiempos agotados, nunca un rechazo de negocio), el gateway deja de llamarlo un rato y responde 503 CORE_CIRCUITO_ABIERTO. El core caído de un banco no arrastra a los demás.