Skip to content
FINORA Academy
SistemaFINORA Core

FINORA Core · How-to guide

Integrate the bank's satellite systems with FINORA Core as the system of record

ImplementedImplementedIntermediate35 min

A bank has many systems around its core: the branch teller, the ERP for corporate accounting, internet banking and mobile banking, the reconciliation system, a corporate customer’s payroll system. This guide explains how to connect them with FINORA Core so that the core is the system of record: the only place where an account balance lives, where every transaction is recorded and where every journal entry is generated. By the end you will know which door each system uses, which credential and scopes to request, which routes to call and how to learn what happens in the core without polling it all the time.

1. Split the work correctly: what belongs to the core and what belongs to the satellite

Section titled “1. Split the work correctly: what belongs to the core and what belongs to the satellite”

There is only one rule: the satellite never keeps its own balance and never posts accounting on its own. It captures the operation, asks the core to perform it and shows what the core answers.

FINORA Core doesThe satellite system does
Keeps the balance of each account: ledger, held and availableShows the balance the core returns at that moment
Validates funds, account status, holds and blocksRequests the operation and honors a rejection
Records the transaction with its stable ledger referenceStores the core’s reference next to its own voucher
Generates the journal entry from the product’s and channel’s accounting conceptNever builds a journal entry by hand for a customer movement
Closes the day and produces the trial balanceReads the general ledger to reconcile or report
Notifies by webhook or through the event streamReacts to the notice instead of polling in a loop
flowchart LR
  T["Teller<br/>(FINORA Sucursal or another)"] -->|"accounts, checks, payments"| K
  E["Bank ERP"] -->|"apply-concept, ledger queries"| K
  C["Channels<br/>(internet, mobile)"] -->|"/v1 through the gateway"| G["API gateway"]
  G --> K["FINORA Core<br/>system of record"]
  K -->|"signed webhooks<br/>and events"| E
  K -->|"signed webhooks<br/>and events"| C

2. Choose the door: the gateway or the core API

Section titled “2. Choose the door: the gateway or the core API”

FINORA Core has two doors for external systems. They are not mutually exclusive: each system uses the one that fits it.

API gateway (/v1)Core API (/api/v1)
For whomSystems outside the core’s network: channels, partners, fintechsThe bank’s internal systems: teller, ERP, reconciliation
IdentityA gateway credential per system (client_id and client_secret, by OAuth token or apiKey)A core API credential per system (OAuth Bearer or X-Api-Key header)
ScopesBy domain: clients:*, deposits:*, loans:*, gl:*, webhooks:*, events:read, config:*, license:read, fx:*Fine-grained: account:read, payment:write, credit:payment, gl:read, accounting:write…
What it addsInstitution and environment by subdomain or X-Finora-Tenant, per-client limits, circuit breaker, uniform errorsAccess to routes the gateway does not publish: checks, apply-concept, checkbooks
Where it is describedThe core API gateway (in Spanish)This guide and the accounting API (in Spanish)

A practical rule: if the system faces the internet or is operated by a third party, it goes through the gateway. If it lives inside the bank and needs checks or posting by concept, it uses the core API with its own credential.

3. One credential per system, with just the scopes it needs

Section titled “3. One credential per system, with just the scopes it needs”

Each satellite gets its own credential. Never share one between the teller and the ERP: if one leaks, it is revoked without shutting down the other, and the core’s audit trail records which system performed each operation (apikey:<id> in the created-by field).

SystemScopes in the core APIScopes in the gateway
Telleraccount:read, payment:writeNot applicable: checks are not published on /v1
Bank ERPgl:read, accounting:write (if it posts by concept)gl:read
Internet or mobile bankingaccount:read, payment:write, credit:read, credit:paymentclients:read, deposits:read, deposits:write, loans:read, loans:write
Notice receiverwebhook:read, events:readwebhooks:read, events:read

Some core scopes are critical (gl:write, credit:payment, credit:approve, credit:disburse, account:status, account:authorize, events:read, config:write, license:write): a credential with general access does not receive them unless they are granted explicitly. Request only the scopes your system uses and review them every month.

4. Accounts: find the account and read its balance

Section titled “4. Accounts: find the account and read its balance”

The teller knows the account by its 20-digit number; the rest of the API asks for its UUID. The first step is always to resolve one into the other.

Method and route in the coreScopeWhat it does
GET /api/v1/ext/accounts/by-code/{accountCode}account:readResolves the account number: UUID, holder, product, currency, status
GET /api/v1/ext/accounts/{accountId}account:readAccount data, status and balances
GET /api/v1/ext/accounts/{accountId}/balanceaccount:readLedger, held and available balance, with the balances by phase
GET /api/v1/ext/accounts/{accountId}/transactionsaccount:readMovements from newest to oldest, by cursor (size 1-100, from, to)
GET /api/v1/ext/accounts?customerId=…account:readAll the accounts of a customer
POST /api/v1/ext/accountsaccount:writeOpens an account for an existing customer, with Idempotency-Key
GET, POST /api/v1/ext/accounts/{accountId}/blocksaccount:read, account:statusBlocks, with four-eyes approval (account:authorize)
GET /api/v1/ext/accounts/by-code/00010001010000000002
X-Api-Key: <teller credential>
{
"accountId": "3d0c4f0e-9c7f-4b8e-8c1a-0a5b5f1d2e11",
"accountCode": "00010001010000000002",
"customerId": "b4c6c1d8-bedd-4d05-ae70-a3152d4e1417",
"holderName": "Cliente Uno",
"productCode": "PRD-CC-VES",
"currency": "VES",
"statusCode": "CUENTA ACTIVA",
"closed": false
}

Each movement carries ledgerRef, the stable ledger reference, and voucher, the accounting voucher, which is null until the core posts it. Store the ledgerRef in your system: it is what ties your voucher to the core’s journal entry.

5. Payments: deposits, withdrawals and transfers

Section titled “5. Payments: deposits, withdrawals and transfers”

Money enters and leaves an account through three routes. All three require the Idempotency-Key header and the payment:write scope, and the core generates the journal entry from the product’s accounting concept.

Method and route in the coreIn the gatewayWhat it does
POST /api/v1/payments/depositPOST /v1/deposits/{depositId}/deposit-transactionsCredits from an external source (cash at the teller, incoming transfer)
POST /api/v1/payments/withdrawalPOST /v1/deposits/{depositId}/withdrawal-transactionsDebits to pay out of the bank; validates funds first
POST /api/v1/payments/transferPOST /v1/deposits/{depositId}/transfer-transactionsTransfers between two accounts of the same bank
POST /api/v1/payments/deposit
X-Api-Key: <teller credential>
Idempotency-Key: 6f1c2b8e-0d4a-4c1e-9a77-2f0b9d3e5a10
Content-Type: application/json
{
"destinationAccountId": "3d0c4f0e-9c7f-4b8e-8c1a-0a5b5f1d2e11",
"amount": "1000.00",
"currency": "VES",
"externalReference": "CAJA-20261024-0042",
"description": "Depósito en efectivo por taquilla",
"channel": "AGENCIA",
"channelCode": "CAJA"
}

The first time it answers 201; if you repeat the request with the same key, 200 with the same response and without moving the balance again. channelCode is optional: if the bank defined that transaction channel, the counterpart of the journal entry uses the channel’s account (for example, the branch cash drawer). See Configure a transaction channel (in Spanish).

6. The bank’s own checks at the teller: validate and pay

Section titled “6. The bank’s own checks at the teller: validate and pay”

Checks are only available in the core API (not in the gateway). There are two ways to pay them; choose one and do not mix them.

This is the contract used by FINORA Sucursal: the teller shows the cashier whether the check is payable, the cashier confirms and then the withdrawal is made.

  1. Validate with POST /api/v1/checks/validate (scope account:read):

    { "accountCode": "00010001010000000002", "checkNumber": "000101", "amount": "250.00" }

    It always answers 200. If payable is false, reason says why (CHEQUE_ANULADO, FONDOS_INSUFICIENTES, CUENTA_BLOQUEADA, CHEQUE_EN_PROCESO…). If it is true, the check is reserved for your credential until reservedUntil (15 minutes by default) and another validation of the same check answers CHEQUE_EN_PROCESO. Validating does not move the balance.

  2. Withdraw with POST /api/v1/payments/withdrawal, with the same credential, the same account, the same amount and a check-payment reference that starts with PCH-. The core marks the check as paid (CLEARED) in the same transaction as the debit.

Checkbooks and the life cycle of each check (issue, delivery, voiding, stop payment, return) are under /api/v1/ext/accounts/{accountId}/checkbooks and /api/v1/ext/accounts/{accountId}/checks.

Method and route in the coreScopeWhat it does
GET /api/v1/ext/credit-productscredit:readLoan products
POST /api/v1/ext/credits/simulationscredit:readSimulates the amortization schedule
POST /api/v1/ext/creditscredit:writeCreates the application
POST /api/v1/ext/credits/{creditId}/approvalcredit:approveApproves or rejects, with four eyes
POST /api/v1/ext/credits/{creditId}/disbursementcredit:disburseDisburses the approved loan
GET /api/v1/ext/credits/{creditId}, …/schedulecredit:readStatus and payment schedule
POST /api/v1/ext/credits/{creditId}/paymentscredit:paymentCollects installments
GET /api/v1/ext/credits/{creditId}/payoff-quotecredit:readQuotes the full payoff
POST /api/v1/ext/credits/{creditId}/payoffcredit:paymentPays off the loan

A payment is applied in the legal order: fees, late interest, interest and principal, from the oldest installment to the newest. Without amount, it collects what remains of the next installment. A larger amount is an advance payment: it covers the following installments in order, without recalculating the schedule. If the last installment is paid, the loan moves to CLOSED.

{ "amount": "1185.60", "originAccountId": "3d0c4f0e-9c7f-4b8e-8c1a-0a5b5f1d2e11", "channelCode": "CAJA" }

8. Accounting: post by concept and read the general ledger

Section titled “8. Accounting: post by concept and read the general ledger”

A satellite that needs to record an accounting event that is not an account movement (a fee collected in the ERP, an expense, an adjustment) does not build the journal entry: it tells the core which accounting concept to apply and for what amount. The concept is defined by the bank’s accountant and fixes the accounts, the legs and the currency.

POST /api/v1/accounting/apply-concept (scope accounting:write):

{
"cia": "001",
"conceptCode": "COMISION-SERVICIO",
"baseAmount": 100.00,
"currency": "VES",
"description": "Comisión por servicio",
"reference": "ERP-000123",
"idempotencyKey": "erp-000123"
}

It answers { "ok": true, "status": "success", "anchorId": "…" }. If the concept does not exist or validation fails, 400 with ok: false and the reason in status. The idempotency key goes in the body: always use it, with your document number, so that a retry does not duplicate the entry. customerAccount (UUID) is optional and ties the entry to a customer’s account.

The general-ledger queries are under /api/v1/ext/gl (scope gl:read) and the gateway publishes them on /v1:

In the coreIn the gatewayWhat it returns
GET /api/v1/ext/gl/accountsGET /v1/glaccountsChart of accounts, filterable by type, currency and whether it is postable
GET /api/v1/ext/gl/accounts/{glCode}?balanceDate=…GET /v1/glaccounts/{glCode}One account with its balance by currency at a date
POST /api/v1/ext/gl/journal-entries:searchPOST /v1/gljournalentries:searchLedger lines by criteria (date, account, reference)
GET /api/v1/ext/gl/journal-entries/{voucher}GET /v1/gljournalentries/{voucher}A complete journal entry by voucher
GET /api/v1/ext/gl/trial-balanceGET /v1/gltrialbalanceTrial balance
POST /api/v1/ext/gl/reportsPOST /v1/accounting/reportsQueues an asynchronous accounting report

Field details, criteria and errors are in the accounting API reference (in Spanish). The balance of a general-ledger account includes what is pending in the close; the trial balance only counts what has already been posted.

POST /api/v1/ext/gl/journal-entries (critical scope gl:write, Idempotency-Key) posts a balanced entry with several legs and branches. It works only if the installation configured the manual journal-entry concept; without it, it answers 422 manual-concept-unavailable.

9. Learn what happens: signed webhooks and events

Section titled “9. Learn what happens: signed webhooks and events”

Instead of asking the core every minute whether a balance changed, subscribe to its notices.

  • Signed webhooks. The core sends a POST to your URL for each event (a posted journal entry, an account change). Each notice carries X-Finora-Signature (t=<seconds>,v1=<hex>, an HMAC of the raw body) and X-Finora-Webhook-Id, which is the same across all retries: verify the signature and deduplicate by that id. Registration, deliveries and redelivery are under /api/v1/ext/webhooks (core) or /v1/webhooks (gateway). See Receive and verify the core webhooks (in Spanish).
  • Event stream. If you need every event in order and the ability to resume where you left off, read /api/v1/ext/events or create a subscription with a committed cursor at /api/v1/ext/event-subscriptions (in the gateway, /v1/events and /v1/subscriptions). See Consume the event stream (in Spanish).

The usual pattern: the ERP listens to AccEntryPosted to reconcile, and the channels listen to AccountChanged to refresh the balance they show.

10. Module licensing: what your system may use

Section titled “10. Module licensing: what your system may use”

FINORA is licensed by module. Before bringing a satellite online, check that the modules it uses are enabled in that installation:

GET /api/v1/ext/license/modules
X-Api-Key: <credential with license:read>

Each module carries code, enabled, licensed, enforced and the API prefixes it covers. The ones that matter for this guide are ACCOUNTS, TRANSACTIONS, CREDITS, CHECKBOOKS, ACCOUNTING, API (the external API in general), GL_API (the accounting API) and GATEWAY. If the core has a registered license and a module is switched off, every route of that module answers:

HTTP/1.1 403 Forbidden
{ "error": "MODULE_DISABLED", "module": "GL_API" }

A 403 MODULE_DISABLED is not fixed with more scopes: it is resolved by whoever administers the subscription. See Subscription and module licenses (in Spanish).

  • Each satellite has its own credential, with only the scopes it uses.
  • Every write carries an idempotency key generated once per operation and stored before calling.
  • The satellite stores the ledgerRef or the voucher returned by the core next to its own document.
  • No balance is calculated in the satellite: it is always read from the core.
  • The webhook receiver verifies the signature over the raw body and deduplicates by X-Finora-Webhook-Id.
  • The modules the system uses appear as enabled in /license/modules.
  • Testing was done against the institution’s sandbox, never against real data.
What happensWhyWhat to do
A deposit appears twiceThe retry carried a new Idempotency-KeyReuse the same key on every retry of the same operation
CHEQUE_EN_PROCESO when validatingAnother credential or an earlier validation reserved the checkWait until reservedUntil or pay with the credential that reserved it
The withdrawal does not mark the check as paidThe credential, account or amount changed, or the reference does not start with PCH-Use the same data as the validation
403 MODULE_DISABLEDThe module is not licensed in that installationAsk whoever administers the subscription to enable it
422 manual-concept-unavailableThe installation did not configure the manual journal-entry conceptPost through apply-concept or ask for the concept to be configured
apply-concept answers 400The concept does not exist in that company or the amount does not validateCheck conceptCode and cia with the bank’s accountant
The channel shows a stale balanceIt stored the balance in its own database and does not listen to noticesRead the balance from the core or subscribe to AccountChanged