e-Factura — Web Services API¶
Operations for the RO e-Factura system. Authentication is the shared
OAuth2 flow (Bearer token). Invoice format is UBL 2.1
/ CIUS-RO; validation is ANAF's server-side validare endpoint (§5) — anafpy
deliberately has no local rule engine (see /DESIGN.md §4). This doc is the
transport/API surface.
Status: draft, compiled from the official 5-page API PDF (current as linked from the e-Factura informații tehnice page on 2026-06-28) plus the official per-endpoint swagger presentations (vendored 2026-07-02 under
_sources/efactura-swagger/) — the PDF covers URLs/params, the swaggers are the authority on response schemas. Re-confirm endpoint behaviour with a live test-environment call during implementation. First live TEST confirmation 2026-07-02:listaMesajePaginatieFacturareturned HTTP 200{"eroare":"Nu exista mesaje in intervalul selectat","titlu":"Lista Mesaje"}for an empty window — exactly the documented no-results shape below. Full roundtrip confirmed 2026-07-02 (TEST, minimal CIUS-RO invoice):upload(index_incarcare) →stareMesaj(in prelucrare→ok+id_descarcare) →descarcare(ZIP with signed invoice +semnaturaXML) → paginated list with results — all matching the shapes below. The same session confirmed thatvalidareandtransformareanswer HTTP 404 ontest(see the caveat below).
Access modes & base URLs¶
Every endpoint has two access modes, differing only by host:
| Mode | Host | How anafpy uses it |
|---|---|---|
| OAuth2 (Bearer token) | https://api.anaf.ro/{prod\|test} |
This is what anafpy uses. |
| Certificate-at-call-time | https://webserviceapl.anaf.ro/{prod\|test} |
Not used (direct mTLS per call). |
| Public (no auth) — validate/PDF only | https://webservicesp.anaf.ro/prod |
Optional, for validare/transformare. |
All paths below are shown for the OAuth2 mode, e.g.
https://api.anaf.ro/prod/FCTEL/rest/.... Swap prod↔test for the environments —
with one caveat: the PDF documents validare, transformare, and
validate/signature for producție only (no test URLs shown), and for the first
two that is not an omission — live-confirmed 2026-07-02 that
https://api.anaf.ro/test/FCTEL/rest/{validare,transformare}/FACT1 answer HTTP
404: they exist only on prod (validate/signature lives at the host root with no
prod/test segment at all). Documents filed to TEST are validated by the upload
pipeline itself (stareMesaj → ok/nok).
Provenance: PDF pp. 1–5 (each operation lists both
webserviceaplcert mode andapi.anaf.rooauth2 mode).
webservicesp.anaf.ro also hosts ANAF's broader family of public no-auth lookup
services (VAT/taxpayer registry with statusRO_e_Factura, the RO e-Factura register
query, financial statements) — documented separately in
public/api.md.
1. Upload — POST /FCTEL/rest/upload¶
Submit an invoice/credit-note/message XML.
POST https://api.anaf.ro/prod/FCTEL/rest/upload?standard={std}&cif={cif}
[&extern=DA][&autofactura=DA][&executare=DA]
Content-Type: text/plain # XML as the raw body (accepted: swagger declares */*)
Authorization: Bearer <token>
| Param | Req | Values / meaning |
|---|---|---|
standard |
✔ | UBL (invoice), CN (Credit Note), CII, or RASP (buyer→issuer message). |
cif |
✔ | Numeric CIF that receives the error message if the seller can't be identified from the XML. You must have SPV rights for it. |
extern |
— | DA only — buyer is outside Romania (no CUI/NIF). |
autofactura |
— | DA only — self-billing (beneficiary issues on supplier's behalf). |
executare |
— | DA only — filed by an enforcement body on the debtor's behalf. |
Response (200, application/xml) — a <header> element with attributes
dateResponse, ExecutionStatus (0 = accepted, 1 = rejected), and on success
index_incarcare (the upload index used by stareMesaj); on rejection a nested
Errors list with errorMessage attributes. 400 returns a JSON CustomErrorMessage
(timestamp/status/error/message).
B2C variant: identical, at POST /FCTEL/rest/uploadb2c?.... Live on test + prod
since 01.01.2025; mandatory for B2C filings since 31.03.2025 (informații-tehnice).
Provenance: PDF pp. 1–2; response schema from the upload swagger (upload.html, actualizat 04.02.2025).
2. Message status — GET /FCTEL/rest/stareMesaj¶
GET https://api.anaf.ro/prod/FCTEL/rest/stareMesaj?id_incarcare={index}
id_incarcare (✔) = the numeric upload index. Response field stare:
stare |
Meaning |
|---|---|
ok |
Validated & processed. Download = original invoice + MF e-signature. Invoice reaches the buyer. |
nok |
Errors found, not processed. Download = errors file + MF e-signature. Invoice does not reach the buyer. |
XML cu erori nepreluat de sistem |
Rejected at upload; the error was already returned in the upload response. |
in prelucrare |
Still processing — keep polling. |
Response (200, application/xml) — a <header> element: stare, and on
ok/nok an id_descarcare attribute (feeds descarcare). Query failures
(unknown/invalid id_incarcare, missing SPV rights, daily limit reached) come back as
<Errors errorMessage="…"/> without stare — they are errors about the query,
not a state of the document.
anafpy:ok/nokare terminal;in prelucraredrives theupload_and_waitpoll loop.nokis a typed business outcome (not an exception); anErrors-only response raisesAnafResponseError.Provenance: PDF p. 2; response schema + error catalog from the stareMesaj swagger (staremesaj.html).
3. Message lists¶
3a. GET /FCTEL/rest/listaMesajeFactura (by days)¶
GET .../listaMesajeFactura?zile={1..60}&cif={cif}[&filtru={E|T|P|R}]
3b. GET /FCTEL/rest/listaMesajePaginatieFactura (paginated, by time range)¶
GET .../listaMesajePaginatieFactura?startTime={ms}&endTime={ms}&cif={cif}&pagina={n}[&filtru=...]
startTime/endTime = unix-timestamp milliseconds (e.g. 1646037374000).
Range rules (per the lista swagger's error catalog): endTime cannot be before
startTime ("endTime nu poate fi inainte de startTime"), cannot be in the future
("nu poate fi o data din viitor"), and startTime cannot be older than 60 days
from the request moment ("nu poate fi mai vechi de 60 de zile fata de momentul
requestului") — messages are retained for 60 days.
filtru (optional): E=ERORI FACTURA, T=FACTURA TRIMISĂ, P=FACTURA PRIMITĂ,
R=MESAJ CUMPĂRĂTOR PRIMIT / MESAJ CUMPĂRĂTOR TRANSMIS (both directions).
Response fields (per message): data_creare, cif, id_solicitare (the upload
index), detalii, tip
(FACTURA TRIMISA | FACTURA PRIMITA | ERORI FACTURA | MESAJ CUMPARATOR …), and
id (used by descarcare) — the six properties of the swagger Mesaj schema.
cif_emitent / cif_beneficiar are documented but never sent. The API PDF
(pp. 3–4) lists them among "Parametrii raspuns" for both list endpoints
(cif_emitent = seller, cif_beneficiar = buyer), but the swagger Mesaj schema
omits them, and a production pull of 522 messages (2026-07-06) contained no such
JSON keys on any message. Both online sources re-fetched 2026-07-07 and found
byte-identical to the vendored copies — the conflict is ANAF's, and per this doc's
source hierarchy the swagger wins. The CIFs ride only inside the free-text detalii,
in (at least) three wordings (first two live-observed in production, third from the
swagger examples):
Factura cu id_incarcare={id} emisa de cif_emitent={seller} pentru cif_beneficiar={buyer}Factura cu id_incarcare={id} transmisa de cif={buyer} ca autofactura in numele cif={seller}(self-billing: the buyer issues on the supplier's behalf)Erori de validare identificate la factura primita cu id_incarcare={id}(no CIFs)
Response envelope (paginated, 3b) — alongside mesaje[]:
numar_inregistrari_in_pagina, numar_total_inregistrari_per_pagina (500),
numar_total_inregistrari, numar_total_pagini, index_pagina_curenta,
serial, cui, titlu. The non-paginated list (3a) caps at 500 messages and
errors with "folositi endpoint-ul cu paginatie" beyond that.
Indexing lag (live-observed 2026-07-02, TEST): list entries are created
asynchronously after processing — a freshly filed invoice whose stareMesaj is
already ok (and whose ZIP downloads fine) may take anywhere from seconds to
~15 minutes to appear in the lists; data_creare is the indexing time, not the
upload time. A self-billed filing (seller CIF == buyer CIF) produces two entries
per invoice, FACTURA TRIMISA and FACTURA PRIMITA, sharing one id_solicitare.
200-with-eroare: both list endpoints return errors and the no-results note in
the same eroare field on HTTP 200. Known no-results wordings: "Nu exista mesaje in
intervalul selectat" (3b) / "Nu exista mesaje in ultimele {N} zile" (3a). Everything
else (CIF … nu este un numar, Nu aveti drept in SPV pentru CIF=…, invalid
startTime/endTime/pagina/filtru, page > total pages, daily call limit reached)
is a genuine error.
anafpy:list_messagesvalidates the window rules above client-side (AnafConfigError, raised eagerly before any request) — they are unconditional and documented, so failing locally with an English message beats spending a round-trip on a 200-with-eroare.MessageListItemkeepssender_cif/receiver_cifas optional aliases for the PDF-documented keys (wire wins if ANAF ever sends them); when absent it extracts them (best-effort) from the first twodetaliiwordings above, leavingNonefor wordings that carry no CIFs.Provenance: PDF pp. 2–4; envelope +
eroarecatalog +Mesajschema from the lista swagger (listamesaje.html);cif_emitent/cif_beneficiarabsence live-confirmed in production 2026-07-06.
4. Download — GET /FCTEL/rest/descarcare¶
GET https://api.anaf.ro/prod/FCTEL/rest/descarcare?id={id}
id (✔) = the id from a listaMesaje entry. Returns a ZIP with two XML files:
the original invoice or the identified errors (as applicable), and the MF
electronic signature.
anafpy: this is theDownloadedMessage— preserve both raw XML bytes (the signed original is the legally valid artifact).Provenance: PDF p. 4.
5. Validate XML — POST /FCTEL/rest/validare/{std}¶
POST https://api.anaf.ro/prod/FCTEL/rest/validare/{FACT1|FCN} # oauth2
POST https://webservicesp.anaf.ro/prod/FCTEL/rest/validare/{FACT1|FCN} # no auth
Content-Type: text/plain # XML in the body
std (✔): FACT1 (invoice) or FCN (credit note). Server-side validation.
Available without auth on webservicesp.anaf.ro. Prod-only — the test path
answers HTTP 404 (live-confirmed 2026-07-02). anafpy therefore always calls the
no-auth webservicesp.anaf.ro/prod variant, regardless of the client's environment
(both response shapes live-confirmed there 2026-07-02: ok, and nok with
Messages[] + trace_id).
Response (JSON): {"stare": "ok"|"nok", "Messages": [{"message": "..."}], "trace_id":
"..."} — Messages present on nok.
Provenance: PDF p. 4; response schema confirmed by the validare swagger (validare.html).
6. XML → PDF — POST /FCTEL/rest/transformare/{std}[/{novld}]¶
POST https://api.anaf.ro/prod/FCTEL/rest/transformare/{FACT1|FCN}[/DA]
Content-Type: text/plain # XML in the body
std (✔): FACT1 | FCN. Optional novld=DA skips validation (⚠️ ANAF does not
guarantee correctness of the PDF for unvalidated XML). Also available no-auth on
webservicesp.anaf.ro. Prod-only — the test path answers HTTP 404
(live-confirmed 2026-07-02). anafpy therefore always calls the no-auth
webservicesp.anaf.ro/prod variant, regardless of the client's environment
(live-confirmed 2026-07-02: returns %PDF bytes for a valid invoice).
Provenance: PDF p. 5.
7. Validate signature — POST /api/validate/signature¶
POST https://api.anaf.ro/api/validate/signature # oauth2
POST https://webservicesp.anaf.ro/api/validate/signature # no auth
multipart/form-data:
file = invoice XML
signature = signature XML
descarcare ZIP. Response (200, JSON): {"msg": "…"} for
both outcomes — valid and invalid are distinguished only by the wording ("…au fost
validate cu succes…" vs "…NU au putut fi validate cu succes…"); a technical error is
HTTP 400 with the same {msg} shape. Note the path sits at the host root — no
FCTEL/rest prefix and no test/prod segment.
Provenance: PDF p. 5; response shape from the signature swagger (validaresemnatura.html, 03.09.2024).
8. Rate limits (per method)¶
Global (api.anaf.ro): 1000 requests/minute (see the
OAuth doc §8). On top of that, per-method daily limits:
| Method | Limit |
|---|---|
upload |
max 1000 RASP (buyer-message) files/day/CUI; no limit for invoice files. |
stareMesaj |
max 100 queries per message/day; no limit on total queries/day/CUI. |
listaMesajeFactura (3a) |
max 1500 queries/day/CUI. |
listaMesajePaginatieFactura (3b) |
max 100 000 queries/day/CUI. |
descarcare |
max 10 downloads per message/day; no limit on total downloads/day/CUI. |
Limits can change; repeated over-limit calls can get the user (and, in serious cases, the application) blocked. (An older lista swagger example mentions a 1000/day list limit — the limits file is newer and is the authority.)
Provenance: limiteApeluriAPI.txt (retrieved 2026-07-02).
anafpy endpoint map¶
anafpy method |
HTTP |
|---|---|
upload(xml, standard, cif, *, extern, autofactura, executare) |
POST /FCTEL/rest/upload |
upload(..., b2c=True) |
POST /FCTEL/rest/uploadb2c |
get_status(index) |
GET /FCTEL/rest/stareMesaj |
list_messages(cif, *, days \| start+end, filter=None) → AsyncIterator[MessageListItem] |
GET /FCTEL/rest/listaMesajePaginatieFactura (paged internally) |
download(id) → DownloadedMessage |
GET /FCTEL/rest/descarcare |
PublicClient.validate_invoice(xml, standard) |
POST webservicesp.anaf.ro/prod/FCTEL/rest/validare/{std} (no auth, prod-only) |
PublicClient.render_invoice_pdf(xml, standard, validate=True) |
POST webservicesp.anaf.ro/prod/FCTEL/rest/transformare/{std} (no auth, prod-only) |
validate_signature(file, signature) |
POST /api/validate/signature |
Implementation notes
- Bodies are sent as
Content-Type: text/plainwith the XML as the raw body. The PDF mandatestext/plainforvalidare/transformare; foruploadit says nothing and the swagger declares the request body*/*, sotext/plainis a safe uniform choice, not a requirement. - Listing:
list_messagesis a single async iterator that pageslistaMesajePaginatieFacturaunder the hood (daysis converted to a[now-days, now]millisecond window; the non-paginatedlistaMesajeFactura(3a) is not used). It stops on the first empty page and honours thenumar_total_paginienvelope field (confirmed by the lista swagger). ANAF returns the sameeroarefield for both "no messages in interval" and genuine errors — the former yields an empty iterator, the latter raisesAnafResponseError(matched by wording; seeis_empty_result_message; the official wordings are catalogued in §3). downloadreturns a ZIP (binary) — handle as bytes, unzip to the two XML members.validare/transformareare stateless, public no-auth, prod-only (see §5/§6), so they live onPublicClient(validate_invoice/render_invoice_pdf) — no Bearer header, noenvironment, no OAuth credentials needed.