Issue and track a Romanian company's invoices from your AI assistant, through
Oblio's API. It reads clients, products, series, VAT
rates and issued invoices; it issues invoices and proformas, records payments,
and cancels, restores, reverses or deletes invoices. Every tool that makes,
changes or unmakes a fiscal document is money: your client asks you first,
and shows the total computed the way ANAF checks it.
You need an Oblio account, its API secret, and the tax id (CIF) of the
company the server should act on. There is no sandbox: an invoice issued here
is real and takes the next number in its series.
What you can ask
- "Which invoices from September are still unpaid?"
- "Invoice Client SRL for 8 hours of consulting at 1,000 RON an hour plus 21%
VAT, due in 30 days."
- "Client SRL paid 1,000 EUR on invoice FCT 0042 by bank transfer today.
Record it."
- "Turn proforma PRF 0007 into an invoice."
- "Reverse invoice FCT 0042, and keep the payment recorded on it."
Install
[Install in VS Code] [Install in Cursor] [Download for Claude Desktop]
With Claude Code:
Claude Desktop (Settings, Developer, Edit Config) and most other clients read the same command as JSON:
Each runs the published package on Node 22.19 or newer. VS Code and the Claude Desktop bundle ask for OBLIO_CONFIG as they add the server. Cursor reads it from your environment instead (${env:OBLIO_CONFIG}), so set it before you start Cursor. Configuration says what it holds.
From a clone
In a JSON configuration, the command is node and its one argument the full path of servers/oblio/dist/index.js. To share one running copy between several clients over HTTP, see docs/deploying.md.
Configuration
app.json holds three values, all required:
company_cif has no default, so no call can act on a company nobody named.
api_base and token_url override Oblio's endpoints, for tests or a proxy.
The server never writes app.json, and its errors name a bad field, never a
value.
Getting the API secret
- Sign in to the Oblio web app.
- Open Settings, then Account Data (Setari, Date cont), and copy the API
secret.
- Put it in
app.json as client_secret, with the email you signed in with
as client_id.
Oblio makes a new secret every time the account's password is reset, so after
a reset app.json needs the new one.
The state folder holds access_token.json, the hour-long token Oblio issues
for the secret, written with mode 0600 and locked across processes. The
variables every server shares (transport, limits, confirmation) are in
docs/configuration.md.
First run
- Ask your assistant to run Oblio's
health check. It answers with the
account, the company CIF from app.json, and whether the account reaches
that company. A refused secret shows up here, before anything is issued.
- Ask for your series (
list_series) and VAT rates (list_vat_rates): an
invoice names its series and each line's rate by the names Oblio gives
them.
From a shell, node dist/index.js --health prints one OBLIO: line and exits
non-zero when the credentials do not work, which suits a timer.
Tools
read changes nothing; write changes something in your Oblio account;
money makes, changes or unmakes a fiscal document, and your client asks you
before each call.
Money
The seven money tools are marked so in tools/list, and your client asks you
to approve each call (Claude Code does, in every permission mode). With
MCP_CONFIRM=elicit the server asks you itself instead, in one line computed
from the arguments. For example:
The first line is the document in Document input, below.
The last is delete_invoice given expect_total; without it the line says
"total not given, so not checked". The server puts its name first, so you read
oblio: Issue invoice ....
What you approve is what is issued:
- The total you confirm is the total issued. It is computed by
documentTotal (the kit's invoiceTotals.ts), the same function whose
result create_invoice returns as computedTotal. After issuing, the
document is read back, and if Oblio's total differs the call fails with
reason: "total_mismatch", naming the document that was issued.
invoice_from_proforma reads the proforma first and holds the invoice to
its total.
- Cancel, restore and delete read the invoice first and answer with what it
totalled, the document Oblio acted on and the company's CIF. Given
expect_total, a number that names an invoice of another total changes
nothing. (Oblio's document read carries no buyer, so the buyer cannot be
checked the same way.)
- A payment is checked against the invoice: the server refuses a cancelled
invoice, one already paid in full, and an amount over what is still owed,
saying how much that is.
Troubleshooting
auth: "Oblio refused the client_id and secret". The secret changed,
usually after a password reset. Copy the new one from Settings, Account Data
into app.json.
api with reason: "outcome_unknown" after an issue or a delete. The
connection dropped, so nobody knows whether Oblio acted. Look the document up
first (get_invoice, or Oblio's web app). If it is not there, call again with
the idempotency_key the error reported: Oblio will not issue the same key
twice. A repeated collect_invoice is safe as it is: it finds the payment if
it landed and records nothing.
api with reason: "total_mismatch". The document was issued, but
Oblio's total is not the one you confirmed. Open it (get_invoice or
get_proforma) and cancel it if it is wrong; do not issue it again.
api with Oblio's own message, such as "Seria FCT nu exista". The message
names the field or series at fault; list_series and list_vat_rates give
the names Oblio expects.
api with status 401 or 403 on a call. The account cannot act on the
company in company_cif; health and list_companies show which companies
it reaches.
api with reason: "drift". Oblio's answer changed shape. The server
refuses to guess; please report it.
How it works
Document input
create_invoice and create_proforma take the document as a validated input
modelled on Oblio's "Parametri document" table, in snake_case. The company's
CIF is not part of it: it comes from app.json, so no pasted example can issue
against another company.
- Every amount is a decimal string (
"1000.00"), checked by a pattern and
sent to Oblio as that string, so it never passes through a floating-point
number. The arithmetic is the kit's exact decimal.ts.
vat_included is required on every line, because Oblio's default
("included") silently turns a net price into a gross one. vat_percentage
is required because the total you confirm is computed from it.
- The buyer's code and IBAN are checked by their check digits: a Romanian
buyer's
cif must be a CIF with a valid control digit or a valid CNP, so a
transposed digit cannot send the invoice to another company. A buyer whose
country is another country passes as written. With send_email, the
answer's emailedTo says where Oblio sent the invoice.
- Series and numbers are checked for their form: a series is letters and
digits as
list_series shows it; a number is digits only, as text
("0042").
- Everything else that would go wrong is refused before sending, all at
once (
src/domain/rules.ts): more decimals than precision (Oblio would
round silently), a zero quantity, a negative price, a rate outside 0-100, a
due date before the issue date, a Chitanta without its receipt series, any
other payment without its reference.
- A retry cannot issue twice. Oblio refuses a second document with the same
idempotency_key. When you give none, the key is derived from the document
(its kind, the company, its content and its issue date: src/domain/keys.ts).
To issue a deliberately identical document the same day, give a key of your
own.
The total
EN 16931's rule, which ANAF validates every e-Factura against and Oblio
follows: a line's amount is quantity times price, rounded to 2 decimals; VAT is
computed once per rate on the sum of those amounts (VAT-inclusive and
VAT-exclusive lines at one rate are two groups, each rounded once); the total
is amounts plus VAT. The VAT inside a VAT-inclusive gross is gross x rate /
(100 + rate). Rounding is half away from zero throughout, as PHP's round()
does. Because that rounding is symmetric, a storno's lines add up to the
original's total exactly, negated; the tests check it on the rounding cases.
Payments and reversals
collect_invoice takes the invoice's series and number, how it was paid
(Ordin de plata for a bank transfer, Card, Chitanta for cash with a
receipt, and Oblio's other types), the amount, the day, and the payment's
reference (a receipt takes its series instead, since Oblio numbers it). The
amount and the day are required even though Oblio would default them to the
whole total and today, because nobody should confirm those defaults unseen. The
invoice is read first, so the buyer and currency in the line you confirm are
the invoice's own, and the answer gives the balance before and after. A
repeated call finds the payment if it already landed and records nothing; to
record a second payment identical in type, reference, amount and date, give an
idempotency_key of your own. A payment is removed in Oblio's web app; this
server does not undo one.
reverse_invoice uses Oblio's own reference flow, so the storno and the
original are linked in Oblio and nothing is retyped. It reverses the whole
invoice; for a partial correction, issue a correcting invoice with
create_invoice. storno_series picks the series (the original's by
default), and remove_payments decides whether the payments recorded on the
original stay (the default, as when money is refunded separately) or go
(Oblio's refund). A cancelled invoice and one that is itself a reversal are
refused. The idempotency key comes from the invoice being reversed, so no
retry, on any day, issues a second storno; to reverse it again on purpose,
after cancelling the first, give a key of your own.
Listing invoices
list_invoices gives, for each invoice, its series and number, dates,
currency, total as Oblio wrote it, whether it is cancelled, the buyer, its
payments, its e-Factura state and its PDF link. To find a buyer by name, look
them up with list_clients and filter by their CIF.
unpaid and paid follow Oblio's "collected" flag and leave cancelled
invoices out; cancelled lists only those. Drafts are never listed.
- Oblio reports each invoice's e-Factura state (
not_sent, processing,
sent, errors) but cannot filter by it, so that filter is applied to each
page after Oblio answers. A filtered page says how many rows it read
(scanned), and it can hold none and still offer a next page: keep following
nextCursor.
- A page holds at most 100 rows. The cursor carries the last row it read, so an
invoice issued while you page does not repeat a row, and a cursor given with
different filters is refused.
What a failure proves
There is no sandbox, so what each failure proves decides whether a call is
repeated (src/infra/http.ts, on the kit's makeHttp):
- 429 proves nothing happened: every call repeats, honouring
Retry-After.
- 401 on a fresh token: the token is minted again once, and a second 401 is
believed.
- A dropped connection or a 5xx proves nothing either way. Reads, cancels
and restores repeat. Issues and deletes do not: they fail as
outcome_unknown (see Troubleshooting).
Oblio throttles at 30 requests per 100 seconds on documents and 30 per 10
seconds elsewhere. The server paces itself below both
(src/infra/http/throttle.ts: 10 at once, then one every 5 seconds for issuing
and two a second for the rest), so a burst waits instead of meeting a 429. Over
HTTP, the pacing covers every client of the process.
What comes back
Oblio publishes no OpenAPI or JSON Schema, and its SDKs carry no types, so the
wire types are modelled by hand in src/domain/schema.ts, citing
https://www.oblio.eu/api and the PHP SDK at OblioSoftware/OblioApi@027a5cc.
Every response is parsed against them at the boundary; a documented field that
goes missing fails the call as drift instead of travelling on, and fields
beyond the documented ones are dropped. A flag is a boolean whichever of
true, 1 or "1" Oblio sent; a decimal is the string Oblio wrote; a series
number stays text ("0051"). An api failure leads with Oblio's own
statusMessage, because "Seria FCT nu exista" is the whole answer.
npx tsx scripts/smoke.ts drives the built server with your credentials. It
is read-only unless given --write-proforma <series>, which issues one 0.01
proforma; nothing money is reachable from it.
Limitations
- One company per server: the CIF in
app.json. An account attached to
several companies needs one configuration per company.
- It does not send e-Factura. Sending an invoice to ANAF is a separate act;
the
anaf-efactura server does it.
- Delivery notes are out of scope, and so is an invoice made from one.
- Some documented Oblio fields are left out on purpose:
list_invoices does not take Oblio's withProducts (the shape of a listed
line is not documented, so it cannot be checked) or its id and number
filters (get_invoice reads one document by series and number).
Other servers for Oblio
Searched 2026-10-15. Oblio publishes none: its GitHub account has API clients
for PHP, Node, Python, Go, Java and C# and shop plugins, and its site does not
mention MCP. Two community servers cover more of Oblio's API than this one:
oblio-mcp on GitHub (not on npm): stdio only, the e-mail and API secret
in environment variables, the company chosen at runtime with set_cif;
every document type including delivery notes, a document list, payments and
e-Factura submission.
io.usefulapi/oblio in the MCP Registry: hosted over HTTP by a third
party that holds your Oblio credentials behind its own OAuth sign-in; similar
coverage plus webhooks; free for 100 calls a month, then paid.
This one exists for what none of them does: it computes an invoice's total by
the rule ANAF validates e-Factura against, puts that total in the line you
confirm, and checks the issued document against it; it derives an idempotency
key when the caller gives none, so a retry cannot issue twice; it checks a
Romanian buyer's CIF or CNP by its control digit; it acts only on the one
company its configuration names; and it can run as a service of its own, with
its secret in a file the client never reads. It does not submit e-Factura.