Typed Python clients for Romania's ANAF web services: e-Factura, e-Transport, and the public no-auth registries.
Project description
anafpy
Typed Python clients for Romania's ANAF tax-authority web services — e-Factura (electronic invoicing), e-Transport (goods transport), and the public no-auth registries (VAT/taxpayer lookups, financial statements) — plus a local MCP server that exposes them as Claude Cowork skills.
anafpy is a thin transport client, not invoicing software. For e-Factura you bring invoice XML that your own invoicing system produced, and anafpy validates it, files it with ANAF, tracks status, and pulls documents back — wrapping the XML you read back (your filings and invoices suppliers issued to you) in a friendly flat read view for easy display. (e-Factura is a filing endpoint; Romanian law presumes you already run an invoicing system.) e-Transport is different: there is usually no upstream software producing declaration XML, so anafpy translates ANAF's whole (small, fully enumerated) schema into friendly typed models — you author declarations, UIT deletions, confirmations, and vehicle changes from structured fields, no XML handling needed, and the same models render what you read back.
What can you do with this?
With the MCP server connected to a Claude client (Claude Desktop, Claude Code), an accountant in Romania can ask Claude to:
Check partners and public data — no login required (these ride ANAF's public, no-auth services):
- Verify a business partner by CUI/CIF — name, address, VAT status (plătitor de TVA), TVA la încasare, split-VAT, inactive flag — one call, in bulk if you like.
- Check whether a partner is enrolled in RO e-Factura.
- Look up the farmers' register (RegAgric) and religious-entities register (RegCult).
- Pull a company's filed financial statements (bilanț) for a given year.
- Validate an invoice XML against ANAF's authoritative server-side
validare(CIUS-RO / BR-RO rules) — no filing.
Work your e-Factura inbox — read-only (needs the certificate login):
- List received and sent invoice messages for a date window.
- Download an invoice as an easy-to-read view, and save the official signed ZIP
and/or a rendered PDF to disk — powering batch flows like "export last month's
invoices as
<date> - <partner>.pdf".
Issuing outbound invoices is deliberately not here — that comes from your invoicing software, which files with ANAF directly. The e-Factura surface is read-only.
Declare goods transport in e-Transport — with a confirmation step (needs the login):
- File a declaration and get a UIT code from transport data in any source — an email, a PDF invoice, a CMR, a spreadsheet — and correct, delete, confirm, or change the vehicle on an existing one.
- List recent notifications, check an upload's status, and look up active declarations / UIT codes.
- Filing is two-step gated: Claude shows you a preview, and nothing is submitted to ANAF until you explicitly confirm.
Setup caveats worth knowing: the e-Factura and e-Transport tools need a one-time login
with your qualified digital certificate (the same one you use on ANAF's SPV) — the
public checks above work without it. The server runs locally on your own machine,
so downloaded invoices and PDFs land on your own filesystem. See
INSTALL.md for the full Claude Desktop + ANAF setup walkthrough.
Status: early / alpha (
0.x), not yet published to PyPI. The OAuth2 auth layer, both async clients (with an easy-to-read flat view of downloaded documents), and the MCP server (structured e-Transport filing and a read-only e-Factura surface — inbox, download, validate) are implemented and tested. Validation is ANAF's own server-sidevalidareendpoint — there is no local rule engine. SeeDESIGN.mdfor the full design anddocs/anaf-reference/for a compiled local reference of ANAF's APIs.
Requires Python 3.12+. Built on httpx and Pydantic v2.
What works today
- OAuth2 auth layer — Authorization-Code bootstrap (browser + qualified
certificate), local token store, and headless refresh, exposed via the
anafpyCLI and anhttpx.Authintegration for the clients. EFacturaClient(async) —upload,get_status,download,validate_signature(checks the MF signature over a downloaded invoice), theupload_and_waitpoll-until-terminal helper, andlist_messages— a single async iterator that pages the message list under the hood (window bydaysorstart/end; empty window → empty iterator, real ANAF errors → raise).downloadexposes three read tiers: raw signed bytes, the full UBL model, and an easy-to-readFlatInvoiceview.ETransportClient(async) —upload,get_status,info,upload_and_wait,list_notifications(same async-iterator shape), andupload_document, which composes and files any of the four flat documents — aFlatTransportdeclaration/correction,FlatDeletion,FlatConfirmation, orFlatVehicleChange— without the caller touching XML.PublicClient(async) — ANAF's unauthenticated public services onwebservicesp.anaf.ro:lookup_taxpayers(VAT registration, VAT-on-collection, inactive, split-VAT, and RO e-Factura register membership in one call),lookup_efactura_register,lookup_farmers,lookup_cult_entities,get_financial_statement(public bilanț indicators) — plus the stateless e-Factura document services:validate_invoice(ANAF's authoritative server-side validation, no filing) andrender_invoice_pdf(the officialtransformarePDF rendering); both are prod-only on ANAF's side and need no login. No credentials, no test/prod split; requests are paced client-side at ANAF's stated 1 req/s rule.- Flat models —
read_flat_invoiceprojects UBL into a small, easy-to-readFlatInvoiceread view (lossy, with acompleteflag); anafpy never composes UBL from it. The e-Transport flat models are bidirectional:read_flat_transportviews a parsed document andbuild_etransport/render_etransportauthor one — full translation of ANAF's XSD, with enum-coded fields (counties, border points, customs offices, operation types...) accepted by name or code. - Generated models — UBL 2.1 / CIUS-RO (
from anafpy.efactura import Invoice, CreditNote) and the proprietary e-Transport XSD, generated from vendored schemas. - MCP server (
anafpy[mcp]) — a local stdio connector exposing the operations as Cowork skills, with read-first, two-step gated e-Transport filing and a read-only e-Factura surface (see below).
Not yet built: CI and PyPI publishing. (A sync facade was dropped as a goal — the clients are async-only.)
Install
Setting up a fresh machine end to end — ANAF app registration, the certificate
login, and the Claude / Cowork configuration, written for a non-developer — follow
INSTALL.md. The short version for developers:
Not on PyPI yet — install from source:
git clone https://github.com/robert-malai/anafpy && cd anafpy
uv sync --all-extras
The distribution offers one extra: anafpy[mcp] (the MCP server).
Authentication
ANAF uses OAuth2 (Authorization Code) gated by a qualified digital certificate. The one-time, interactive bootstrap runs on your machine (the cert lives there):
anafpy auth login --client-id <ID> --client-secret <SECRET> \
--redirect-uri https://localhost:9002/callback --paste
anafpy auth status # show stored token validity
anafpy auth logout # remove the stored tokens (signs this machine out)
Register the callback URL with the https:// scheme — ANAF's developer portal
rejects http:// callbacks (HTTP 400 at registration; verified 2026-07-02). The
callback still doesn't need a public server; pick how the code gets captured:
-
--paste(recommended default). No listener runs. After the certificate step the browser shows a connection error — expected — and you paste the redirect URL from the address bar into the CLI. Paste promptly: ANAF's code expires in ~60 seconds. -
--tls-cert/--tls-key. The local listener serves TLS directly with a certificate you supply — a self-signed one works. Generate it once (browsers require asubjectAltName, not just the CN):openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \ -keyout ~/.anafpy/callback-key.pem -out ~/.anafpy/callback-cert.pem \ -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
then pass
--tls-cert ~/.anafpy/callback-cert.pem --tls-key ~/.anafpy/callback-key.peminstead of--paste. The browser shows a one-time "proceed to localhost" warning (click through — it's your own cert on your own loopback); trust the cert in the OS keychain, or use mkcert, to remove the warning entirely. On Windows (no stock OpenSSL), prefer mkcert:choco install mkcert(orscoop install mkcert), thenmkcert -installandmkcert localhost 127.0.0.1— the emitted PEM pair plugs into--tls-cert/--tls-keyunchanged, with no browser warning at all. -
Neither flag. The listener speaks plain HTTP; put your own TLS terminator in front of it. If the listener can't start — or no callback arrives in time — the CLI falls back to paste mode.
This opens your browser for the certificate step, captures the authorization code,
exchanges it for tokens, and stores them in the OS credential store (macOS
Keychain, Windows Credential Manager, Linux Secret Service/KWallet — the default
backend). Tokens then refresh headlessly for ~a year (access token
90 days, refresh token 365 days), so the cert is needed only about once a year. See
docs/anaf-reference/oauth/authentication.md.
To sign out — e.g. when leaving a shared machine — run anafpy auth logout. It
deletes the stored tokens, which is what ends this machine's access: without the
refresh token it can no longer mint new access tokens. The logout is purely
local: ANAF documents a /revoke endpoint but it is not reachable headlessly
(live-verified 2026-07-05 — ANAF's gateway answers with its certificate login
wall, same as for a nonexistent path), so server-side the tokens simply expire
(access ~90 days, refresh ~365 days); to hard-revoke them use Renunțare
Oauth in ANAF's developer portal (this deletes the app registration).
On a headless host without a credential store (Docker, a bare Linux server),
switch to the JSON-file backend: pass --store-backend file to auth login /
auth status / auth logout — or set ANAFPY_TOKEN_STORE_BACKEND=file, which
the MCP server reads too; the file lives at ~/.anafpy/tokens.json (--store /
ANAFPY_TOKEN_STORE to move it). On Windows the credential store's token set is
transparently split across vault entries (Credential Manager caps one entry at
2560 bytes, smaller than an ANAF JWT).
Usage
The clients are async and used as context managers. Build a TokenProvider over your
stored tokens, then call discrete operations:
from anafpy.auth import KeyringTokenStore, TokenProvider
from anafpy.efactura import EFacturaClient
provider = TokenProvider(
client_id="<ID>",
client_secret="<SECRET>",
store=KeyringTokenStore(), # OS credential store (the default backend)
# or FileTokenStore("~/.anafpy/tokens.json") for headless/Docker hosts
)
async with EFacturaClient(provider) as efactura:
result = await efactura.upload(invoice_xml, cif="RO12345678")
status = await efactura.get_status(result.upload_id)
# or, in one call: status = await efactura.upload_and_wait(invoice_xml, cif=...)
Discrete methods make a single call (no transport retry). HTTP/auth problems raise
AnafError subclasses; business outcomes (a nok rejection, BR-RO findings) come
back as typed values, not exceptions. On HTTP 429 the client raises AnafRateLimitError
exposing retry_after rather than backing off itself.
e-Transport declarations are authored from typed models — no XML in sight:
import datetime as dt
from decimal import Decimal
from anafpy.etransport import (
ETransportClient, FlatDeletion, FlatTransport, FlatTransportAddress,
FlatTransportDocument, FlatTransportGood, FlatTransportLocation,
FlatTransportPartner, FlatTransportVehicle,
)
from anafpy.etransport.schema.schema_etr_v2_20230126 import (
CodJudetType, CodScopOperatiuneType, CodTaraType, CodTipOperatiuneType,
TipDocumentType,
)
declaration = FlatTransport(
operation_type=CodTipOperatiuneType.TTN, # domestic transport
partner=FlatTransportPartner(
name="Partener SRL", country=CodTaraType.ROMANIA, code="12345678",
),
vehicle=FlatTransportVehicle(
plate="CJ01ABC", carrier_name="Transport SRL", carrier_code="23456789",
carrier_country=CodTaraType.ROMANIA, transport_date=dt.date(2026, 7, 10),
),
start_location=FlatTransportLocation(address=FlatTransportAddress(
county=CodJudetType.CLUJ, locality="Cluj-Napoca", street="Memorandumului",
)),
end_location=FlatTransportLocation(address=FlatTransportAddress(
county=CodJudetType.MUNICIPIUL_BUCURESTI, locality="Bucuresti",
street="Calea Victoriei",
)),
goods=[FlatTransportGood(
operation_scope=CodScopOperatiuneType.COMERCIALIZARE,
name="Materiale constructii", quantity=Decimal("100"), unit_code="KGM",
gross_weight=Decimal("110"), net_weight=Decimal("100"),
value_ron=Decimal("2500"), tariff_code="6810",
)],
documents=[FlatTransportDocument(
doc_type=TipDocumentType.CMR, date=dt.date(2026, 7, 9), number="FAC-001",
)],
)
async with ETransportClient(provider) as etransport:
result = await etransport.upload_document(declaration, cif="12345678")
print(result.uit) # the UIT code, issued at upload time
# later: delete / confirm / change vehicle on that UIT the same way, e.g.
await etransport.upload_document(FlatDeletion(uit=result.uit), cif="12345678")
The public registries need no auth at all:
from anafpy.public import PublicClient
async with PublicClient() as public:
lookup = await public.lookup_taxpayers(["RO12345678"])
if lookup.found:
record = lookup.found[0]
print(record.name, record.vat_registered, record.efactura_registered)
MCP server
The anafpy[mcp] extra ships a local stdio MCP server that wraps the clients as
Cowork skills. The server is best-effort: configuring it — including registering
your own OAuth application on ANAF's portal — is your responsibility, and
INSTALL.md walks you through every step. Run it host-side, where the
token store written by anafpy auth login lives:
ANAFPY_CLIENT_ID=... ANAFPY_CLIENT_SECRET=... ANAFPY_CIF=... \
python -m anafpy.mcp # or the `anafpy-mcp` console script
It exposes freely callable read tools (auth_status; the e-Factura inbox via
efactura_list_messages / efactura_download — the latter
can also save artifacts for the user: save_zip_as writes the signed archive
ZIP and save_pdf_as writes ANAF's official PDF rendering (transformare,
best-effort) to caller-given paths, which powers flows like "export last month's
invoices as <date> - <partner>.pdf" (an existing file is never replaced unless
overwrite=true — a name collision is reported instead of losing a file); the PDF
is also exposed as the MCP resource
anafmsg://<message_id>/pdf; etransport_list
/ etransport_get_status / etransport_lookup / etransport_nomenclature;
efactura_validate, which runs
ANAF's authoritative server-side validator without filing; and the anaf_* public
lookups — taxpayer/VAT registry, RO e-Factura register, farmers/cult registers, and
financial statements. efactura_validate and the anaf_* lookups ride the public
no-auth services, so they need no login at all) plus two-step gated
filing for e-Transport: etransport_prepare* returns a preview + a confirmation
token bound to the exact document and the CIF being filed for; etransport_submit
files only when
given that token (same document, same CIF) and confirm=True, and each token is
single-use — filing again requires a fresh prepare. The MCP e-Factura surface is
read-only — there are no invoice filing tools (removed 2026-07-03: outbound UBL
comes from invoicing software that files with ANAF itself; EFacturaClient.upload
remains for library users). e-Transport filing is composed for you:
etransport_prepare_declaration (a new declaration or, via correction_of_uit, a
correction), etransport_prepare_deletion, etransport_prepare_confirmation, and
etransport_prepare_vehicle_change take structured fields (enum-coded values by ANAF
code or by name — TTN, CLUJ, NADLAC; discoverable via etransport_nomenclature),
compose the declaration XML, and return it alongside the preview and token;
etransport_prepare still accepts ready-made XML. Both the e-Factura inbox and the
e-Transport prepare
previews present documents as friendly flat models parsed from the XML (for
invoices, easy to read and lossy by design — the raw bytes stay authoritative). The
compiled ANAF reference is surfaced as read-only resources. Auth stays
the host-side CLI — the server only reads and refreshes the token store. Configuration is
environment-only; see CLAUDE.md.
Register the server with any MCP client — e.g. with Claude Code, from a source checkout (locked deps, no PyPI needed):
claude mcp add anafpy \
-e ANAFPY_CLIENT_ID=... -e ANAFPY_CLIENT_SECRET=... -e ANAFPY_CIF=... \
-- uv run --directory /path/to/anafpy --frozen --extra mcp anafpy-mcp
No credentials yet? The server still starts; the public anaf_* lookups
(registries, financial statements) and efactura_validate are fully usable. The
remaining e-Factura / e-Transport tools unlock once you set the credentials and run
the one-time anafpy auth login in a terminal.
The server also ships workflow playbooks as MCP prompts — a user-invoked entry
point (Claude Desktop's "+" menu, or /mcp__anafpy__etransport-declare in Claude
Code). etransport-declare walks Claude through filing an e-Transport declaration
from whatever source the data lives in (an email, a PDF invoice, a CMR, a
spreadsheet) — extract, map to the structured declaration, prepare, show the
preview for your approval, submit, then poll until ANAF issues a valid UIT. The
playbooks live under skills/ (SKILL.md files, the single source of
truth) and travel with any connection method.
Development
uv sync --all-extras
uv run pytest # respx-mocked, credential-free
uv run ruff check . && uv run ruff format --check .
uv run mypy # strict
ANAFPY_LIVE=1 uv run pytest -m live # opt-in: live smoke against real ANAF
The live marker re-confirms wire shapes against real ANAF endpoints and is skipped
by default (not a gate). It covers the public services (no credentials needed) plus,
with .env credentials and an anafpy auth login token store, the authenticated
TEST environment: read-only shape checks and two roundtrips that actually file a
test document — a minimal CIUS-RO invoice (e-Factura) and a domestic transport
declaration composed via the flat authoring models (e-Transport). The roundtrips
target TEST only, never production.
Models under efactura/ubl/ and etransport/schema/ are generated (via
scripts/generate_*.py from vendored XSDs in schemas/) and must not be hand-edited.
See CLAUDE.md for repository conventions.
License
Apache-2.0. Independent / unofficial — not affiliated with ANAF.
anafpy is free to use and provided as-is, with no warranty: it moves documents to and from ANAF, it does not give tax advice, and filing outcomes are your responsibility. The MCP server is best-effort — configuring it, provisioning your own OAuth application on ANAF's portal, and holding the qualified certificate are up to you (INSTALL.md walks through all of it).
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file anafpy-0.1.0.tar.gz.
File metadata
- Download URL: anafpy-0.1.0.tar.gz
- Upload date:
- Size: 3.4 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6776711da5c3a597436003ff0262d6edf98176386df19f17f33ecdfc2c8058ea
|
|
| MD5 |
b76c6156866e9a9d1f2d1a2214d675ac
|
|
| BLAKE2b-256 |
aed15e0560fdd96c83337ca8b46b53bacfaf4de1a21b4efb24c529ac09f3bef9
|
Provenance
The following attestation bundles were made for anafpy-0.1.0.tar.gz:
Publisher:
release.yml on robert-malai/anafpy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
anafpy-0.1.0.tar.gz -
Subject digest:
6776711da5c3a597436003ff0262d6edf98176386df19f17f33ecdfc2c8058ea - Sigstore transparency entry: 2097157725
- Sigstore integration time:
-
Permalink:
robert-malai/anafpy@779776e353a33eed2caf2554406c216612a810c3 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/robert-malai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@779776e353a33eed2caf2554406c216612a810c3 -
Trigger Event:
push
-
Statement type:
File details
Details for the file anafpy-0.1.0-py3-none-any.whl.
File metadata
- Download URL: anafpy-0.1.0-py3-none-any.whl
- Upload date:
- Size: 370.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
37e3f5ab3f9afdbec1c19fad8eaa0628328d8f08b98decf426d79b292d08ec33
|
|
| MD5 |
00a98269fc30933e75012bef3ddd67dc
|
|
| BLAKE2b-256 |
fa2ceb37fef1db9673bae78c6a36bb8999bb109e4728eda354d381db0a536247
|
Provenance
The following attestation bundles were made for anafpy-0.1.0-py3-none-any.whl:
Publisher:
release.yml on robert-malai/anafpy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
anafpy-0.1.0-py3-none-any.whl -
Subject digest:
37e3f5ab3f9afdbec1c19fad8eaa0628328d8f08b98decf426d79b292d08ec33 - Sigstore transparency entry: 2097158095
- Sigstore integration time:
-
Permalink:
robert-malai/anafpy@779776e353a33eed2caf2554406c216612a810c3 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/robert-malai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@779776e353a33eed2caf2554406c216612a810c3 -
Trigger Event:
push
-
Statement type: