Skip to content

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: listaMesajePaginatieFactura returned 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 prelucrareok + id_descarcare) → descarcare (ZIP with signed invoice + semnatura XML) → paginated list with results — all matching the shapes below. The same session confirmed that validare and transformare answer HTTP 404 on test (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 prodtest 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 (stareMesajok/nok).

Provenance: PDF pp. 1–5 (each operation lists both webserviceapl cert mode and api.anaf.ro oauth2 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/nok are terminal; in prelucrare drives the upload_and_wait poll loop. nok is a typed business outcome (not an exception); an Errors-only response raises AnafResponseError.

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_messages validates 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. MessageListItem keeps sender_cif/receiver_cif as optional aliases for the PDF-documented keys (wire wins if ANAF ever sends them); when absent it extracts them (best-effort) from the first two detalii wordings above, leaving None for wordings that carry no CIFs.

Provenance: PDF pp. 2–4; envelope + eroare catalog + Mesaj schema from the lista swagger (listamesaje.html); cif_emitent/cif_beneficiar absence 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 the DownloadedMessage — 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
Both files come from the 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/plain with the XML as the raw body. The PDF mandates text/plain for validare/transformare; for upload it says nothing and the swagger declares the request body */*, so text/plain is a safe uniform choice, not a requirement.
  • Listing: list_messages is a single async iterator that pages listaMesajePaginatieFactura under the hood (days is converted to a [now-days, now] millisecond window; the non-paginated listaMesajeFactura (3a) is not used). It stops on the first empty page and honours the numar_total_pagini envelope field (confirmed by the lista swagger). ANAF returns the same eroare field for both "no messages in interval" and genuine errors — the former yields an empty iterator, the latter raises AnafResponseError (matched by wording; see is_empty_result_message; the official wordings are catalogued in §3).
  • download returns a ZIP (binary) — handle as bytes, unzip to the two XML members.
  • validare/transformare are stateless, public no-auth, prod-only (see §5/§6), so they live on PublicClient (validate_invoice/render_invoice_pdf) — no Bearer header, no environment, no OAuth credentials needed.