API

Integrating with the FincoLedger API

How to authenticate, what you can read and write, and how to make retries safe — for the systems that plug into the accounting engine.

Two Ways to Authenticate

Pick a personal access token for a quick, single-tenant integration, or OAuth2 when you need a standard authorization flow or a multi-client integration.

Personal Access Tokens (Quick Start)

The fastest way in. A user with the api_token.manage permission (owner or admin) creates a token directly in Console, choosing which abilities it carries — for example events:read and events:write. The token is shown once at creation and never stored in plain text, so save it immediately.

List accounts with a personal access token

curl https://console.fincoledger.com/api/v1/accounts \ -H "Authorization: Bearer <your-personal-access-token>"
OAuth2 (Standard Flows)

Portal is the OAuth2 authorization server for the platform. It supports two grant types: Authorization Code with PKCE for integrations acting on behalf of a logged-in user, and Client Credentials for server-to-server integrations acting as a service account. Either way, the resulting access token is used against Console exactly like a personal access token.


Authorization Code + PKCE

Use this when a human is present to approve access — for example, a third-party app requesting scoped access to one tenant. Redirect the user to GET /oauth/authorize with response_type=code, your client_id, redirect_uri, requested scope, and a PKCE code_challenge using code_challenge_method=S256 (plain challenges are rejected). After the user approves, Portal redirects back with an authorization code, which you exchange for a token:

Exchange an authorization code for an access token

curl -X POST https://portal.fincoledger.com/api/oauth/token \ -d grant_type=authorization_code \ -d client_id="<your-client-id>" \ -d client_secret="<your-client-secret>" \ -d redirect_uri="<your-redirect-uri>" \ -d code="<code-from-the-redirect>" \ -d code_verifier="<your-pkce-code-verifier>"
Client Credentials

Use this for unattended, service-account integrations — no user is present at request time. Exchange your client ID and secret directly for a token, scoped to whichever tenant the client was registered under:

Get a token with the client credentials grant

curl -X POST https://portal.fincoledger.com/api/oauth/token \ -d grant_type=client_credentials \ -d client_id="<your-client-id>" \ -d client_secret="<your-client-secret>"
Both grants return a bearer access token you send to Console the same way as a personal access token — Authorization: Bearer <token>. Console verifies OAuth2 tokens by asking Portal to introspect them; that check happens server-side and needs no extra step on your part.

Every Token Is Bound to One Tenant

A token can never see or touch data outside the tenant it was issued for, and it can never do more than the person who issued it is allowed to do.

Tenant-Scoped

A personal access token is bound to the tenant it was created in; an OAuth2 token is bound to the tenant the client was registered against. There is no way to pass a different tenant at request time — every call runs against the token's own tenant only.

Abilities ∩ RBAC

Every endpoint checks two things at once: the ability the token was granted (e.g. events:write) and the RBAC permission of the user who issued it (e.g. event.create). A token can never exceed its issuing user's access — if that user's permissions are later reduced or revoked, the token's effective access shrinks with them.

Idempotent Retries

Networks fail and retries happen — the Idempotency-Key header makes it safe to retry a write without creating a duplicate.

Send a client-generated UUID as the Idempotency-Key header on any POST, PATCH, or DELETE request. Reuse the exact same key for every retry of that same logical request. If the retry matches the original request, you get back the original response instead of a second write; if the same key arrives with a different request body, the server rejects it with 409 Conflict rather than guessing which one you meant.

Post a business event with a retry-safe key

curl -X POST https://console.fincoledger.com/api/v1/events \ -H "Authorization: Bearer <your-token>" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 3fa85f64-5717-4562-b3fc-2c963f66afa6" \ -d '{ "event_type": "receive_money", "event_date": "2026-07-01", "amount": 1500.00, "currency_code": "TWD", "account_id": 101, "contra_account_id": 205, "description": "Customer deposit" }' # Retrying with the SAME Idempotency-Key returns the same 201 response # instead of creating a second event — safe to run again after a timeout.
Keys are remembered for 24 hours and pruned automatically after that. Requests without the header are executed normally every time — idempotency protection is opt-in, not forced on every write.

What You Can Read and Write

The write surface is deliberately narrow: everything you create goes in through business events, and the accounting engine handles the double-entry bookkeeping from there.

Write: Business Events Only

Create, edit, or delete a draft business event, then post or void it — POST /api/v1/events and its /events/{id} actions are the only write surface. Submit what happened in plain terms (receive money, spend money, transfer money, or an accrual) and the engine produces the balanced journal entry, the same way the New Transaction screen does inside the app.

Read: Everything Else

Chart of accounts, journals, trial balance, income statement, balance sheet, inventory valuation, AR customers/invoices/receipts, AP vendors/bills/payments, inventory items/warehouses/movements, and bank transactions are all available as read endpoints under /api/v1 — enough for an integrator to reconcile their own records against the ledger without ever writing to it directly.

This split is intentional: it keeps every ledger entry flowing through one code path (business events), so the same posting rules and validations apply whether the entry came from the UI or from your integration.

Ready to Start Building?

Explore the API surface yourself in the demo environment, or get in touch for integration credentials.