fiscalm API
openapi.json is the public HTTP contract: routes, fields, request
schemas and refusal codes. The same schema is available without authentication
at GET /api/v1/openapi.json. It is generated from finrec.api.schema.openapi();
change descriptions there and regenerate the JSON, rather than editing it by hand:
uv run python -c 'import json; from finrec.api.schema import openapi; print(json.dumps(openapi(), ensure_ascii=False, indent=2))' > docs/openapi.json
Authentication and transport
Use HTTPS through the installation's reverse proxy. Create a scoped credential:
finrec token create --user admin --name 'Reporting script' --scope read
finrec token list
finrec token revoke <id>
Creation reveals the secret once; only its digest is stored. A token covers every
company in the installation; write includes read. Every token belongs to an enabled
account. Its effective scope is intersected with that account's current role: a write
token becomes read-only immediately when its owner is demoted to Doar citire (writes
return 403 insufficient_scope). A disabled or missing owner returns 401 unauthorized;
enabling the owner restores access without reissuing the token. Listing adds
user_id and owner: {id, display_name}. Invoicing, commerce and settlement writes
recheck the token and owner together under the write lock. No ownerless token can
authenticate, and CLI creation requires an explicit --user. Send it in
Authorization: Bearer <token> to /api/v1/. Never put it in a URL, source control
or logs. Revocation is checked again under the write lock on each engine transaction,
including generic HTTP/MCP actions and multi-stage imports. A transaction already
authorized under that lock may finish; parsing a request does not reserve future
write authority. Invoicing also rechecks token authority at its post-commit SPV claim.
App links, cookies and Tailscale identity do not authenticate the public API.
The browser lists owned keys in Setări › Acces and Setări › Cont, including
before the first company. Token creation sends {name, scope} and revocation
sends {id}, with no company or claimed owner. Administrators see all owners;
other normal accounts see only their own keys, and Doar citire offers only read.
POST requests require application/json, one Content-Length, no transfer encoding,
and at most 4 KB (order intake permits 256 KB). If Origin is present it must match the server origin; the server
also checks it for MCP. All routes, including API v1, upload capabilities and hooks, pin Host and port to
FINREC_PUBLIC_URL (only /healthz is exempt). Preserve Host through the proxy. Forwarded headers are
trusted only from FINREC_TRUST_PROXY; see self-hosting.
Reading state
Start with /api/v1/companies, then a company's state or resource lists. Treat
identifiers as opaque, including payment IDs. Entity links contain related IDs,
not URLs; null/empty means unavailable. Money is a two-decimal string, dates ISO.
ContaHub-API-Version: 1 is retained as a protocol identifier.
Lists use data and next_cursor: pass the returned cursor unchanged and
URL-encoded with the same filters. Default limit is 50, maximum 200. Pagination
is not a snapshot. The invoicing workspace's embedded lists are not paginated.
Files are served through guarded resource routes, never arbitrary storage paths.
Actions and safety
POST /api/v1/companies/{co}/actions/{action} with the fields described in the
schema. The engine action registry supplies the action names, argument fields and
app-only metadata to the generated contract. Every action refuses unknown keys;
API v1 clients must send only fields declared for that action. If co is also supplied in the body it must equal the URL company.
Invoicing has explicit /invoicing/{operation} routes with separate request
schemas. Refresh state after an action and use returned capability flags; a flag
is a preview, and the engine rechecks every precondition when writing.
Errors have {"error":{"code":"…","message":"…"}}; successful actions retain
the Romanian message. Use codes and schemas, not message matching, in clients.
A user marking an invoice paid is not bank proof. fiscalm prepares payment files
and documents to download; a human imports the file or sends the documents. The mail account is read-only.
Without <FINREC_HOME>/state/LIVE, all local work is available with the same evidence
and authorization guards. SPV uploads, nudges, webhooks, marketplace writes
and payment-file generation/downloads are refused or skipped; check runs a tick that respects Sandbox. Administrator-only
company, integration and automation settings use the app/CLI, not bearer access.
Receipt-upload links are separate upload-only capabilities, not API credentials.
Issue and storno support Idempotency-Key, scoped to token and company for 24 hours. Retry an uncertain result with the same key and body; a changed body conflicts. Other operations reject this header. After issuance, read SPV status separately; a committed invoice does not prove successful submission.
App and MCP
The browser uses /web/api/… with a __Host-session cookie (Secure, HttpOnly,
SameSite=Strict, Path=/). Configure an HTTPS FINREC_PUBLIC_URL before starting
the listener. Every host/port is checked against it, except /healthz. The app shell
at / and /setup is public; financial reads/actions require an enabled account's
session. URL-token links and Tailscale identity headers never authenticate.
GET /web/api/me returns safe account metadata, role capabilities and recovery state;
401 starts sign-in. POST /auth/password with username/password returns only a
five-minute ticket. POST /auth/totp with ticket/code sets a new cookie after TOTP
proof. /auth/recovery takes the password ticket and one recovery code and opens a
restricted session. /auth/recover/begin and /auth/recover/finish replace TOTP and
codes, then close every session; sign in again afterward. Setup links use
/auth/setup/inspect, /auth/setup/begin and /auth/setup/finish; link is only the
URL fragment, never a request path. The generated x-browser-auth contract lists
exact bodies and permitted session types. Password alone never signs in. Codes and
TOTP secrets appear only in the intended one-time enrollment/recovery responses.
Passkeys use the generated x-browser-auth.passkeys contract and the
/auth/passkey/…, /auth/step-up/passkey/…, /auth/setup/passkey/… and
/auth/me/passkey/… route families. Options return ticket and WebAuthn
publicKey JSON; binary proof fields use canonical unpadded base64url and
transports is a JSON-array string. A DNS HTTPS origin, discoverable credentials
and user verification are required. api/me includes safe owned-key metadata,
availability and the session's actual authentication method.
The app offers native passkey sign-in when advertised by the server and supported
by the browser. Same-account confirmation can also use an owned key. Leaving the
source cancels its native request; a lost finish response is never replayed automatically.
The setup page can create a discoverable passkey from the one-time setup link,
then shows recovery codes once and requires acknowledgement before entering the app.
An uncertain finish offers an explicit access check; restarting obtains fresh options.
For an account whose only complete sign-in method is a passkey,
/auth/passkey/recovery accepts username and one 80-bit recovery code with no
existing cookie. Its recovery_code session can only use
/auth/recover/passkey/… to verify a replacement, retire all old factors and
replace codes; it cannot perform financial actions or step-up. Password-and-code
recovery sessions stay confined to the existing TOTP flow. Both finalizations
close every session and require fresh sign-in. Own-key rename/removal requires
recent authentication and cannot remove the last complete sign-in method.
The app exposes code-only recovery separately from password/TOTP recovery and
selects the replacement form from the restricted session's actual method. Recovery
is never offered as sensitive-action confirmation. A lost recovery response can be
checked by reading the current identity; the code is not resubmitted automatically.
Account and administrator screens reflect configured key counts and actual session
methods. Password/TOTP change controls appear for an existing pair; administrator
reset applies to key-only accounts too and removes their keys.
Cont › Chei de acces lists owned key names, creation and last-use dates. Every
normal role can add, rename and remove its own keys after recent authentication;
native creation requires a compatible browser. Unreadable write responses refresh
metadata and retain an uncertainty warning without automatically repeating a mutation.
Every browser POST, including sign-in, requires one matching Origin, JSON up to
4 KB (explicit document/invoice upload limits are declared separately), and
Sec-Fetch-Site: same-origin if supplied. Plain HTTP cookies are refused even on
loopback. No CORS is enabled. Sensitive actions return 401 {ok:false, reauth:true};
POST /auth/step-up/password then /auth/step-up/totp confirms the cookie's own
identity and rotates it. Normal sign-in and setup also revoke the browser’s previously
presented session, including when switching accounts. Failure preserves that session,
and replacement commits atomically. Other devices remain signed in. The app opens a confirmation sheet without losing the
current form, verifies that the same account is still active, and repeats the
refused request once. Cancellation, failed proof or an account change never
repeats it. Network errors never trigger an automatic retry. POST /auth/signout with {} revokes it and clears it.
Administrator, Membru and Doar citire apply to every company.
Setări › Utilizatori uses the administrator account endpoints without a company
field; role changes refresh api/me, and one-time setup links remain only in
component memory. Resetting the current account hands its link directly to /setup. The generated
x-app-action-roles map publishes the same registry capability used by the server;
app controls combine it with api/me capabilities, never a separate role table. Read-only accounts
cannot mutate state or download payment files. Integrations require an administrator
(except member access when no enabled administrator exists). Bank files use
Origin-checked POST /web/api/download {co,v} and a CSV blob response; their former
GET route is refused. The app saves the returned CSV, then refreshes once; it never
repeats the POST automatically after an interrupted response. A changed file returns 409. The session and current role
are checked again under the writer lock before reading the snapshot or recording
a download; a late disable, demotion or revocation returns 403 without CSV bytes. Account IDs are recorded in audit
notes and action logs; no credential is logged.
Normal browser accounts can list their own tokens at /web/api/tokens; an
administrator sees all metadata, including revoked historical records. Create with
POST /web/api/token-create {name,scope} after step-up; the cookie determines the
owner. Administrator and Membru may choose read/write, Doar citire only read.
POST /web/api/token-revoke {id} revokes an own token; administrators may revoke
any. Ownership and live administrator status are rechecked under the writer lock.
No body owner or actor override is accepted, and bearer credentials cannot manage
tokens. The generated x-app-token-management describes these app operations.
Own-account reads use /web/api/me, including active sessions and the remaining
recovery-code count (never code hashes). Normal accounts of every role can POST
/auth/me/recovery-codes {}, /auth/me/session-close {id} and
/auth/me/sessions-close-others {} after step-up. The owner always comes from the
cookie; another user's session and any submitted owner override are refused.
Closing the current session clears its cookie. Codes are shown once and replace
the entire old set. Recovery-only sessions cannot use these routes.
Password replacement uses POST /auth/me/password-change {password} after step-up.
Hashing runs outside the write lock; live authority and the old credentials are
rechecked before saving. Other sessions and pending tickets close, while the current
session stays valid. TOTP replacement uses /auth/me/totp/begin {} followed by
/auth/me/totp/finish {ticket,code}. The ticket belongs to that user and session;
only a verified new seed replaces the old one. Success keeps the current session,
closes others and reveals a replacement set of recovery codes. Wrong/replayed codes
or stale/swapped tickets change no factor. No endpoint removes the last sign-in pair.
Administrator account management uses GET /web/api/users,
/web/api/user-sessions?id=<user id> and /web/api/auth-events (latest 100).
The generated x-app-user-administration lists exact POST fields for user-create,
user-update, user-disable, user-enable, user-unlock, user-setup-link, user-reset and
session-close. Every mutation requires fresh authentication, rechecked together
with the live session/role under the writer lock. Create and setup/reset return a
one-time done.url with the setup fragment; neither links nor secrets appear in
audit metadata. The last working administrator is protected. Session IDs in these
lists are one-way digests and cannot be used as cookies.
The app rechecks identity when a tab becomes visible or focused and after a session change in another tab. Only a random notification nonce is stored in localStorage. Private content and portalled sheets remain suspended until identity is confirmed; errors leave retry available. Same-account forms survive; a different owner clears them. Generation checks discard older successes and refusals. Setup/recovery code acknowledgement survives returning to the tab; an external session change clears it.
Search and custom financial controls combine the current identity capabilities with business-state flags. Role names or stale company metadata never grant access.
The schema's x-app-actions metadata records app-only action fields. App actions
return ok, message, next, done, and refreshed state (possibly null). A refused
engine action may return HTTP 200 with ok:false; authentication and transport
refusals use HTTP status codes. App fixtures are generated with npm run fixtures
in app/ and excluded from the production export.
MCP uses JSON-RPC 2.0 at POST /api/v1/mcp, or finrec mcp over stdio with
FINREC_MCP_TOKEN injected privately. HTTP uses the same bearer permissions and
guards as REST. HTTP requests with an id require Accept: application/json, text/event-stream.
For the legacy 2025-11-25 protocol, initialize with protocolVersion and
capabilities, then send MCP-Protocol-Version: 2025-11-25 on later requests.
The modern protocol and matching headers are specified in the generated schema.
Tool results are JSON text in content blocks, no resource capability is advertised,
and notifications return HTTP 202. See the schema for the MCP protocol surface.
Signed shop webhooks
Configure webhook_url and webhook_secret together in the shop integration. Events
are live-only and delivered at least once, so receivers must verify signatures and
freshness, then deduplicate by event ID. The generated contract's x-shop-webhooks
section specifies all header names, the exact signature calculation, payload fields,
event types and retry behavior. Orders cannot choose webhook destinations.
Accountant packs use local actions pack-prepare (month) and pack-sent
(month, exact version). Read the report's version after preparing, then download
GET /api/v1/companies/{co}/reports/{month}/pack?version=SHA256.
Confirmation marks only the frozen document set, never later arrivals. Issued invoice
PDF/XML downloads remain available. Reminder tasks contain subject and body for
a human to copy. No mail-draft action or outgoing IMAP route exists.
Product metadata: GET /api/v1/meta is public and reports the installed fiscalm version from the Python package metadata, shared with finrec --version and the app’s Setări footer.
Official AI CLI login is an app-only administrator configuration flow, including in Sandbox.
Its route, operations and sanitized responses are described by x-app-ai-cli in the generated OpenAPI
contract. POST requires the install's Origin; credentials and raw CLI output are never returned.
AI integration mutations are admin-only; other integrations retain their existing permissions.
api/integrations supplies can_manage_ai so the app shows AI settings read-only to other users.