Romania — RO e-Factura (ANAF)
Research doc for the Romania fiscalization adapter. Consumed by the implementation bead. No code in this bead.
Status: Research — no adapter code written yet.
Strategic context: Phase-1 Central/Adriatic cluster, alongside HU, SI, HR, GR, PL.
Chosen because: €0 cert (qualified cert is paid commercially but ANAF access via SPV
is free for taxpayers who already hold one for tax filing), REST surface, EU member,
Hungarian border (cluster effect with HU), strongly forced-buy event for ISVs after
the 2024-07 mandate. ~700K active VAT-registered businesses. See
docs/strategy/thesis-api-wedge.md (fi-awx) and
apps/docs-internal/docs/ops/country-prioritization.md.
Note — out of scope. RO e-Transport (the separate ANAF system that tracks movement of goods of high fiscal risk) is not part of this adapter. If the implementation bead discovers a coupling — e.g. customers asking to submit e-Factura and e-Transport in the same flow — file a follow-up bead, do not absorb the work here.
1. Regulatory scope
What must be reported
Romania's RO e-Factura (operated by ANAF — Agenția Națională de Administrare Fiscală, the National Agency for Fiscal Administration) is a centralised B2B and B2G electronic invoicing system. The applicable legislation is OUG 120/2021 (creating the system) as amended by Legea 139/2022, OUG 130/2021 (B2G mandate), Legea 296/2023, and OUG 115/2023 (B2B universal mandate plus the 2024-07 reporting/enforcement step).
Three successive expansions define current scope:
| Effective date | Scope change |
|---|---|
| 2022-07-01 | B2G mandatory. All economic operators issuing invoices to public-sector entities (autorități contractante) must transmit them through e-Factura. Defined by Legea 139/2022 art. 2 and OUG 130/2021. |
| 2022-07-01 | B2B for high-fiscal-risk products. Invoices for products on the high-risk list (alcohol, tobacco, mineral water, salt, lemons, vegetables, fruit, clothing, footwear) must transit e-Factura even before the universal B2B mandate. |
| 2024-01-01 | B2B reporting universal. All B2B invoices between Romanian-established taxable persons must be transmitted to e-Factura within 5 working days. Penalties suspended until 2024-04-01 (grace period). |
| 2024-07-01 | B2B as the sole legal channel. A B2B invoice issued between Romanian-established taxable persons that is not registered in e-Factura within the deadline is no longer a legal document for VAT deduction at the recipient. The invoice exists in law only when it has a seal from the Ministry of Finance. |
| 2025-01-01 | B2C reporting mandatory for B2B-or-mixed sellers (extends e-Factura to consumer invoices issued by VAT-registered taxable persons in RO). Originally optional under OUG 115/2023; OUG 69/2024 made it mandatory from 2025. |
| 2026-01-01 | (Per OUG 69/2024 and subsequent rectifying ordinances) Pre-filled VAT return (e-TVA) decision-support cross-check based on e-Factura data. Does not change reporting obligations on issuers but increases auditing pressure on data quality. |
Confirm before writing the adapter: the precise B2C status for the period the adapter ships. As of the 2026-04 freeze, B2C invoices issued by VAT-registered sellers are in scope; B2C cash receipts (case de marcat / fiscal printers) are not — those run through a separate AMEF/ANAF cash-register pipeline.
Who must report
- Romanian-established taxable persons issuing B2B or B2G invoices.
- Includes non-resident taxable persons VAT-registered in RO (with a
ROVAT prefix) for transactions taxable in RO. - Excludes purely foreign-to-foreign trade — the test is whether the supply is taxable in RO under the VAT Code (Codul Fiscal art. 268–278).
Exemptions
- Invoices issued between non-RO entities for non-RO supplies (out of scope).
- Receipts from electronic cash registers — handled by AMEF/MF, not e-Factura.
- Public utility periodic billings issued in bulk through aggregator providers may use a separate XML schema profile; we do not target this. Document if a customer asks.
Thresholds
- None. No de minimis threshold for the reporting obligation. Every in-scope invoice, including 0 RON, must be reported.
- The 2024-07 mandate explicitly targets legal effect for VAT deduction: a recipient cannot deduct VAT on an invoice that wasn't reported on time, no matter the amount.
2. API surface
Base URLs
| Environment | API base |
|---|---|
| Sandbox (test) | https://api.anaf.ro/test/FCTEL/rest/ |
| Production | https://api.anaf.ro/prod/FCTEL/rest/ |
| OAuth issuer (test + prod share) | https://logincert.anaf.ro/anaf-oauth2/v1/ |
The test and prod issuers use the same OAuth2 server but different
client_id registrations (one per environment). Cited from the ANAF Manual de
înregistrare oameni de IT (developer-onboarding guide) and the Specificatii
tehnice e-Factura PDF. The sandbox accepts arbitrary Romanian CIFs and never
emits a real Ministry-of-Finance signature; goldens validate XSDs but not legal
status.
API version
There is no version segment in the URL. ANAF advertises the wire format as
RO_CIUS (the Romanian Core Invoice Usage Specification, currently aligned to
EN 16931-1:2017+A1:2019 — the European semantic invoice standard). The
transport schema is UBL 2.1 (urn:oasis:names:specification:ubl:schema:xsd:Invoice-2)
plus the CIUS rules expressed as Schematron and code-list constraints. The XSD
itself does not change between RO_CIUS revisions; only the Schematron (*.sch)
file does. Latest CIUS at the 2026-04 freeze is RO_CIUS_2.1.1 (2024-09).
Endpoints
All endpoints under …/FCTEL/rest/ are HTTPS; uploads are POSTs with
Content-Type: text/plain + raw XML body, and queries are GETs with query
parameters. All requests require an OAuth2 bearer token (see section 3).
| Method + Path | Purpose | Notes |
|---|---|---|
POST /upload?cif={cif}&standard={standard} | Upload one invoice XML. standard=UBL for invoice/credit-note, standard=CN for OASIS UBL CreditNote-2.1 explicitly, standard=CII for UN/CEFACT Cross-Industry Invoice (rarely used in RO), standard=RASP for response messages. Returns index_incarcare (upload index). | One invoice per call. Body 1 MB max. The cif is the issuer's tax ID. |
POST /uploadb2c?cif={cif}&standard=UBL | B2C variant of /upload. Same wire format but the recipient leg uses BT-46 0 ("not VAT registered") with consumer code list. | Mandatory since 2025-01-01 for VAT-registered RO sellers issuing B2C. |
GET /stareMesaj?id_incarcare={index} | Poll status of one upload. Returns nok (rejected), in prelucrare (processing), ok (accepted, signature applied), XML cu erori nepreluat de sistem (terminal reject — XSD/Schematron failure). | The async receipt step. Poll until terminal. |
GET /listaMesajeFactura?zile={N}&cif={cif}&filtru={F} | List all messages (sent + received) for the issuer over the last {N} days (max 60). filtru ∈ {E=errors, T=sent, P=received, R=responses}. | Used by both issuers and recipients. Used to enumerate inbound for buyer-side flow. |
GET /listaMesajePaginatieFactura?startTime={ms}&endTime={ms}&cif={cif}&pagina={n}&filtru={F} | Paginated variant of the above. startTime/endTime are millisecond Unix timestamps. | Use this when 60-day window is insufficient or to incrementally reconcile. |
GET /descarcare?id={message_id} | Download response/receipt ZIP for a given message. The ZIP contains the original invoice XML, the ANAF response XML (including digital seal), and any error report. | id is the id from listaMesajeFactura, not index_incarcare. Confusing — see section 4. |
Adapter hot path: OAuth2 token (cached) → POST /upload → GET /stareMesaj
(polled until terminal) → GET /descarcare (only on ok, to retrieve the sealed
PDF/XML for the merchant's archive).
Rate limits
ANAF's published guidance is "reasonable use"; in practice the adapter must
honour 429 and 5xx with exponential backoff. The community-observed cap is
~1 request/second per CIF on stareMesaj and ~10 uploads/minute, but no hard
SLO is documented. Treat any sustained throttling as our bug to back off harder.
3. Authentication
Credentials (one-time, out-of-band)
ANAF e-Factura uses OAuth2 authorization-code with cert-bound client authentication. The qualified electronic certificate (semnătură electronică calificată) acts as the client authenticator — it does not sign the invoice payload (signing is done by the Ministry of Finance after acceptance, see section 5). The certificate must be:
- Qualified under EU Regulation 910/2014 (eIDAS) and listed on the trust
list (
https://www.anaf.ro/anaf/internet/ANAF/asistenta_contribuabili/...). Romanian-recognised CAs at the 2026-04 freeze: certSIGN, DigiSign, Trans Sped, Alfasign. EU-recognised CAs from other member states are accepted in theory but the SPV onboarding flow expects a RO CA; we recommend defaulting to certSIGN for our managed-cert offering. - Linked to a natural person who has a CNP (personal identifier), and that person must be designated as a representative of the CIF in SPV (Spațiul Privat Virtual — the ANAF self-service portal).
- Pre-registered at SPV through
Înregistrare în SPV, attaching theForm 150(cerere utilizare certificat digital). ANAF approves manually in 1–10 business days.
Cost: A 1-year qualified cert is RON ~70–150 (€15–30) from RO CAs. SPV access itself is free. Our adapter never holds the private key — see below.
OAuth2 client registration (developer side)
Separate from per-merchant cert: the adapter itself must register as an OAuth2 client with ANAF's IT developer programme:
- Email
iturl@anaf.ro(or use the form athttps://logincert.anaf.ro/) declaring the client's redirect URIs and intended use. - ANAF returns a
client_id+client_secretfor test, and after one round of round-trip testing a separate pair for production. - The client_secret is rotated only on demand; treat it as a long-lived shared secret.
Authorization-code flow
The merchant authorises the adapter once, then the adapter holds a refresh token thereafter. The cert authenticates the merchant during the front-channel authorisation step.
Step 1 — front-channel /authorize
Browser GET https://logincert.anaf.ro/anaf-oauth2/v1/authorize
?response_type=code
&client_id={our_client_id}
&redirect_uri={our_redirect}
&state={csrf}
&token_content_type=jwt // optional; we want JWTs
Browser must present a client cert during TLS handshake.
ANAF authenticates the user against the cert+SPV linkage,
redirects to {our_redirect}?code={code}&state={csrf}.
Step 2 — back-channel /token (initial)
POST https://logincert.anaf.ro/anaf-oauth2/v1/token
(basic-auth: client_id:client_secret)
grant_type=authorization_code
code={code}
redirect_uri={our_redirect}
token_content_type=jwt
→ { access_token, refresh_token, expires_in: 7776000 (90d), token_type: Bearer }
Step 3 — refresh
POST https://logincert.anaf.ro/anaf-oauth2/v1/token
(basic-auth: client_id:client_secret)
grant_type=refresh_token
refresh_token={rt}
→ { access_token (new, 90d), refresh_token (rotated) }
Token lifetimes:
- Access token: 90 days (7,776,000 seconds) — confirmed from ANAF's developer guide. Long-lived because the front-channel cert step is operationally heavy.
- Refresh token: ~1 year, single-use (rotated on each refresh).
- Implication: the adapter's refresh-token store is the long-lived secret per merchant; access-tokens are cached and refreshed on a daily cron, well before expiry.
Why this is Cloud-OK
- The cert is only used during the front-channel authorisation flow, in the end-user's browser TLS handshake. No private-key material crosses our servers.
- After step 2, the adapter holds only OAuth2 tokens — plain bearer secrets with no cryptographic obligation on us.
- Therefore a Cloud-deployed FiscalAPI tenant can mint tokens for any RO merchant who has clicked through the SPV-cert authorisation page; the merchant's cert never leaves their browser/USB-token.
Token usage on data-plane requests
POST /test/FCTEL/rest/upload?cif=12345678&standard=UBL HTTP/1.1
Host: api.anaf.ro
Authorization: Bearer eyJhbGciOiJSUzI1NiI…
Content-Type: text/plain
<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2" …>
…
</Invoice>
No request signing. No nonce. No X- headers. The bearer token is the entire
authentication.
4. Payload shape
Wire format: UBL 2.1 + RO_CIUS
The on-the-wire XML is OASIS UBL 2.1 (UBL-Invoice-2.1.xsd /
UBL-CreditNote-2.1.xsd) constrained by RO_CIUS (Romanian CIUS) Schematron
rules. RO_CIUS is itself derived from the EU CIUS for EN 16931 — so a valid
Peppol BIS Billing 3.0 invoice plus the RO-specific extensions is valid for
e-Factura. Conversely, a generic UBL 2.1 invoice that does not satisfy RO_CIUS
business rules (e.g. missing cbc:CustomizationID) will be nok even though
it is XSD-valid.
Mandatory invocation
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:efactura.mfinante.ro:CIUS-RO:1.0.1</cbc:CustomizationID>
<cbc:ID>FA-2026-001</cbc:ID>
<cbc:IssueDate>2026-04-20</cbc:IssueDate>
<cbc:DueDate>2026-05-20</cbc:DueDate>
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
<cbc:DocumentCurrencyCode>RON</cbc:DocumentCurrencyCode>
<!-- TaxCurrencyCode required if DocumentCurrencyCode != RON; must be RON -->
…
</Invoice>
Key constraints:
cbc:CustomizationIDmust be the RO_CIUS string above (current is1.0.1; Schematron has been re-versioned but theCustomizationIDis held stable for validation tooling). Source: ANAF RO_CIUS — Documentaţie tehnică.cbc:DocumentCurrencyCodeis the invoice currency. If notRON, the invoice must also includecbc:TaxCurrencyCode = RONand provide RON tax totals.cbc:InvoiceTypeCodeselects the document semantics; see table below.
Document-type codes (UN/CEFACT 1001 subset, RO_CIUS narrowed)
| Code | Meaning | Schema | UN/CEFACT 1001 |
|---|---|---|---|
| 380 | Standard invoice (factură) | UBL-Invoice-2.1.xsd | Commercial invoice |
| 381 | Credit note (factură de stornare / credit) | UBL-CreditNote-2.1.xsd | Credit note |
| 383 | Debit note (factură de debit) | UBL-Invoice-2.1.xsd | Debit note |
| 384 | Corrective invoice (factură rectificativă) | UBL-Invoice-2.1.xsd | Corrected invoice |
| 389 | Self-billed invoice (autofactură) | UBL-Invoice-2.1.xsd | Self-billed invoice |
| 751 | Invoice information for accounting purposes (rare, B2G framework) | UBL-Invoice-2.1.xsd | Invoice info |
RO_CIUS does not restrict to a single code; the Schematron checks the
combination of InvoiceTypeCode + presence of cac:BillingReference (for
corrections) + absence of PaymentTerms for credit notes, etc.
High-impact RO_CIUS rules
cbc:CustomizationIDexact match (ruleBR-RO-010).cac:AccountingSupplierPartymust includecbc:CompanyIDin the formRO12345678(with prefix) when supplier is VAT-registered, andcbc:CompanyLegalForm(e.g.S.R.L.).- Postal codes use
cbc:PostalZonewith the 6-digit CodPS (e.g.010101). The county usescbc:CountrySubentitywithRO-{XX}code (RO-Bfor București,RO-CJfor Cluj, etc.) — ISO 3166-2:RO. - Line VAT category code comes from a constrained subset of UN/CEFACT 5305:
S— standard rateAA— reduced rate (lower)Z— zero rateE— exempt with right to deductK— intra-community supply (reverse-charge)AE— VAT reverse charge (domestic; e.g. construction, scrap, electronics under RO Cod Fiscal art. 331)O— outside scope of VAT
- Reverse-charge lines must have
cbc:Percent = 0.00and the document-levelcac:TaxTotalmust include a per-category subtotal with the matching category code. - Multi-VAT invoices are required to carry one
cac:TaxSubtotalper distinct(category, percent)pair.
Code lists
- Currency: ISO 4217 (
RON,EUR,USD, …). - Country: ISO 3166-1 alpha-2 (
RO,HU,DE, …). - Payment means (
cbc:PaymentMeansCode): UN/CEFACT 4461 —30(credit transfer),48(bank card),54(credit card),42(debit transfer),10(cash). - VAT category: UN/CEFACT 5305 (subset above).
- Item classification: CPV (B2G) — optional but Peppol-recommended.
Sizes and limits
- Single XML upload max 1 MB.
- Single line item count: ANAF imposes no documented hard cap; RO_CIUS rule
BR-CO-25only requires that monetary totals tie. Practical cap observed: ~1,000 lines per invoice before merchants split. - Invoice ID (
cbc:ID) max length 200 chars; we recommend ≤32 for cross-system safety.
5. Ordering and sequencing
Issuer-side numbering
Romanian VAT law (Codul Fiscal art. 319) requires invoice numbers to be
sequential and unbroken per series, with the issuer free to choose the
series scheme (year-prefixed, location-prefixed, etc.). e-Factura does not
enforce a hash chain (unlike Spain Verifactu) and does not mint numbers on
the issuer's behalf — cbc:ID is sourced from the issuer.
Adapter rule: Transaction.document_number is the source of truth and
flows into cbc:ID verbatim. We do not gap-fill or re-mint.
Async server-side identifiers
Each successful upload produces three distinct identifiers that the adapter must thread through:
| Identifier | When generated | Where it appears | Purpose |
|---|---|---|---|
index_incarcare (upload index) | On POST /upload accept | Response body of /upload; query parameter on /stareMesaj | The receipt for "ANAF received this byte stream"; not yet an accepted invoice. |
id_solicitare (request ID) | On POST /upload accept | Inside <header> of /stareMesaj response | Mirrors index_incarcare; used in some helpdesk flows. |
id (message ID) | On terminal ok | Returned by /listaMesajeFactura; required by /descarcare | Permanent reference to the sealed message. |
The adapter must persist index_incarcare immediately on upload accept (this
is the only way to recover state if our process dies before polling). The id
is harvested later, when polling completes with ok.
Idempotency
ANAF does not offer client-side idempotency keys. A retry of /upload
with the same XML body produces a new index_incarcare and the recipient
sees a duplicate. The adapter must implement idempotency itself:
- Client computes a content hash on the XML body (SHA-256, hex, lowercased).
- A
(cif, content_hash)row is inserted withON CONFLICT DO NOTHINGbefore the network call. If a prior row exists with a non-nullindex_incarcare, return that. - If the content row exists but
index_incarcareis null (prior call errored mid-flight), the adapter retries using the same content; if ANAF accepts, store the newindex_incarcare. If ANAF rejects with a duplicate-content business error (rare; ANAF doesn't currently enforce this), reconcile vialistaMesajeFactura.
Submission ordering across multiple invoices
There is no required order between invoices. Each POST /upload is
independent, scoped to a single document. The adapter parallelises uploads
freely up to per-CIF rate caps.
Storno semantics
A "storno" (cancellation) is a new credit-note document referencing the
original via cac:BillingReference/cac:InvoiceDocumentReference/cbc:ID. The
original invoice stays on file; ANAF tracks the reversal as a relationship,
not a deletion. Adapter rule: building a credit note requires the original's
cbc:ID and cbc:IssueDate; we capture these in Transaction.related_document
at preparation time.
6. Submission window
| Issuer | Window |
|---|---|
| RO-established taxable persons issuing B2B invoices | 5 working days from the invoice issue date (cbc:IssueDate). Per OUG 115/2023 art. III. Working days exclude Romanian public holidays. |
| RO-established VAT-registered sellers issuing B2C invoices | 5 working days from issue (since 2025-01). Same rule. |
| Public-sector receivers (B2G) | Issuers have the same 5-working-day rule; the public-sector buyer has its own internal cycle but does not interact with our adapter. |
Operational position: the adapter targets same-day or near-real-time
submission for both robustness and cash-flow purposes (recipient cannot deduct
VAT until accepted), with a fallback queue that retries on failure for up to
the legal 5-working-day window. SubmissionWindow::Days(5) in the
CountryAdapter semantics.
If the merchant's issue-date drifts more than 5 working days from now() at
preparation time (e.g. a backfill job), the adapter logs a warning but still
attempts submission — ANAF accepts late submissions and applies the
penalty/seal status accordingly; the legal consequence is on the merchant, not
our pipeline.
7. Sandbox access
Onboarding
- Apply to the ANAF e-Factura developer programme by emailing
iturl@anaf.rofrom a corporate mailbox, declaring intended use and requesting test credentials. - ANAF returns:
- A test
client_id+client_secret(basic-auth credentials). - A list of test CIFs you can use as merchant tax IDs.
- The whitelisted redirect URIs you submitted. Onboarding takes 5–15 business days.
- A test
- Use the test issuer at
https://logincert.anaf.ro/anaf-oauth2/v1/authorizewithresponse_type=codeand a real qualified cert (the cert is real even in sandbox; ANAF doesn't issue test certs because the SPV linkage is the actual point of the OAuth flow). - Submit invoices to
https://api.anaf.ro/test/FCTEL/rest/upload. Sandbox accepts arbitrary RO CIFs and returns a test seal that is not legally binding. XSD + Schematron validation behaves identically to production.
Test data quirks
- Sandbox
/listaMesajeFacturaonly returns the last 60 days; expired test data is purged. - Sandbox
stareMesajresults return faster than production (typically seconds vs. up to minutes), so polling intervals tuned in sandbox should be doubled before going live. - The sandbox seal is a fixed dummy certificate; do not parse the seal in tests as if it were the production MF cert.
What we'll need
- A pair of test client credentials checked into the password vault under
RO_ANAF_TEST_CLIENT_ID/RO_ANAF_TEST_CLIENT_SECRET. - One real qualified cert (the team can use a single shared cert for the CI service account; certSIGN is cheapest).
- An
.envtemplate withRO_ANAF_BASE_URL,RO_ANAF_OAUTH_BASE_URL,RO_ANAF_CLIENT_ID,RO_ANAF_CLIENT_SECRET,RO_ANAF_REDIRECT_URI,RO_ANAF_REFRESH_TOKEN(per-merchant),RO_ANAF_DEFAULT_CIF.
8. Error model
Three error tiers
ANAF's error surfaces split cleanly into transport, upload-time, and business-validation tiers. The adapter must reflect this split through to the merchant.
Transport tier (HTTP / OAuth)
401 invalid_token— refresh and retry.401 invalid_client— credential bug, escalate.403on data-plane — token scope mismatch (test token against prod URL or vice versa). Config bug.429— back off; ANAF doesn't exposeRetry-Afterreliably, use exponential.5xx— retry with same payload (same content hash → same idempotency guard).
Upload-time tier (XSD + Schematron, returned in /upload response)
The /upload response is itself UBL — an <header> element with status
attributes:
<header xmlns="mfp:anaf:dgti:spv:respUploadFisier:v1"
ExecutionStatus="0"
index_incarcare="123456789"
dateResponse="202604201040"
Errors_count="0"/>
ExecutionStatus | Meaning | Action |
|---|---|---|
0 | Accepted for processing | Persist index_incarcare; poll stareMesaj. |
1 | Rejected at upload time | XSD or top-level Schematron failure. Read <Errors errorMessage="…"/> children. Not retryable as-is. |
Upload-time errors mean the bytes were structurally invalid. Examples:
XML cu erori— XSD validation failed.CIF necalibrat / inactiv— issuer CIF doesn't match the OAuth identity.Limita marimii fisierului— body > 1 MB.Numarul de documente trimise depăşeşte limita— rate cap hit.
Adapter mapping: AdapterError::ValidationFailed with the parsed errors.
Async business tier (Schematron + ANAF business rules, returned in stareMesaj + descarcare)
After acceptance, ANAF runs the full RO_CIUS Schematron pack and ministry
business rules (VAT registration lookup, BillingReference resolution, …)
asynchronously. Outcomes are visible via /stareMesaj?id_incarcare=...:
stare | Meaning | Action |
|---|---|---|
in prelucrare | Processing — keep polling (1–60s in sandbox; up to minutes in prod). | Continue polling. |
ok | Accepted, MF seal applied. The id from listaMesajeFactura is the permanent ID. | Mark accepted; download via /descarcare. |
nok | Rejected. Errors live in the descarcare ZIP under _errors_*.xml. | Map to AdapterError::Rejected with errors; not retryable as-is. |
XML cu erori nepreluat de sistem | Terminal: XML had errors that were detected post-acceptance. | Map to AdapterError::Rejected; not retryable as-is. |
Result-code taxonomy
| Level | Examples | What it means for us |
|---|---|---|
| Transport | 401, 5xx, timeout, DNS | Standard retries with backoff; refresh token on 401 invalid_token. |
Upload (ExecutionStatus=1) | XML cu erori, CIF inactiv, Limita marimii | Our payload is bad. Fix and resubmit (will get a new index_incarcare). |
Async business (stareMesaj=nok) | BR-RO-010 violation, Cod TVA cumparator inexistent, Suma TVA incorecta | Either our composition bug or merchant-data bug. Surface to merchant; not retryable as-is. |
Async terminal happy path (ok) | — | Success; download seal via /descarcare. |
Key distinction: ExecutionStatus=1 vs stare=nok
ExecutionStatus=1at upload time — request never entered the async processing queue. The adapter's job is to surface a structural error. Always our or our schema generator's bug; merchant data was probably fine except for fields we forgot to map.stare=nokafter polling — request entered processing and was rejected by RO_CIUS Schematron or business rules. ~60% of these in practice are merchant-data bugs (e.g. recipient CIF not VAT-registered when we taggedSrate); the rest are gaps in the adapter's RO_CIUS coverage.
Retry policy matrix
| Event | Retry same upload? | Retry with new payload? | Human ticket? |
|---|---|---|---|
401 invalid_token | After refresh_token round-trip | ❌ | ❌ |
5xx / timeout | ✅ (idempotency guard handles dedup) | ❌ | ❌ |
429 | After backoff | ❌ | ❌ |
ExecutionStatus=1 (XSD/Schematron) | ❌ | Only after fixing payload | ✅ if blocks mainline; otherwise auto-fix and replay |
stareMesaj=nok (business) | ❌ | Only after fixing data; new cbc:ID | ✅ surface to merchant |
stareMesaj=ok | N/A — success | — | — |
9. Implementation plan
Crate layout
rust/adapters/romania/
├── Cargo.toml # name = "romania-efactura"
└── src/
├── lib.rs # RomaniaAdapter (impl CountryAdapter)
├── config.rs # RomaniaConfig (cif, oauth refresh, ...)
├── auth.rs # OAuth2 token exchange + refresh + cache
├── client.rs # reqwest client, retry, polling for stareMesaj
├── ubl_invoice.rs # Transaction → UBL Invoice (380, 383, 384)
├── ubl_credit_note.rs # Transaction → UBL CreditNote (381)
├── cius.rs # RO_CIUS rule helpers (CompanyID format, county codes, ...)
├── code_lists.rs # static maps for VAT category, payment means, country, currency
├── errors.rs # AdapterError variants for the three tiers
├── poll.rs # stareMesaj polling state machine
└── tests.rs # XSD + golden conformance against testdata/conformance/romania/efactura
Trait fit
CountryAdapter::prepare() is the natural two-phase split:
- prepare() — build UBL XML in-memory (quick-xml), apply CIUS canonical
ordering, compute the SHA-256 idempotency hash, mint the deterministic
fiscal_id = cif:invoice_number, returnPreparedRecordwith the XML incontent. Fast, no network. Call XSD validation here so errors surface before any network hop. - submit() —
OAuth2 token(cache hit ≥99% of the time) →POST /upload→ onExecutionStatus=0returnSubmitResult { upstream_id: index_incarcare, status: Submitted, terminal: false }. Does not wait for the async processing result. - A separate
poll()job — runs every 30 s on a background queue, iterates submissions inSubmittedstate, calls/stareMesaj, and promotes them toAccepted{seal_id}orRejected{errors}.
DeploymentMode::Cloud is the primary target; Embedded works equivalently
(no on-device crypto). SubmissionWindow::Days(5) with operational target
of same-day.
Config shape
pub struct RomaniaConfig {
/// RO CIF (without the "RO" prefix, since SPV uses the bare digits).
/// e.g. "12345678".
pub cif: String,
/// OAuth client_id (stable per-deployment, not per-merchant).
pub oauth_client_id: String,
/// OAuth client_secret (stable per-deployment).
pub oauth_client_secret: String,
/// OAuth redirect URI registered with ANAF (stable per-deployment).
pub oauth_redirect_uri: String,
/// Per-merchant refresh token (long-lived, ~1y, single-use).
/// Stored encrypted at rest; rotated on each /token call.
pub oauth_refresh_token: String,
/// Per-merchant access token cache (90d TTL).
pub oauth_access_token: Option<CachedToken>,
/// true => api.anaf.ro/test, false => api.anaf.ro/prod.
pub sandbox: bool,
/// Optional VAT-group representative CIF, when the issuer is a member.
pub vat_group_representative: Option<String>,
/// Whether the merchant's seller registration is on the "high fiscal
/// risk products" list (controls some Schematron checks).
pub high_risk_products: bool,
}
Dependencies (expected)
quick-xml— UBL XML build + parse (already in workspace via Spain adapters).reqwest— already in workspace.serde/serde_json— input model parsing (already).chrono— date handling forIssueDate/DueDate/ Romanian working days.sha2— SHA-256 idempotency hash.base64— already in workspace.- Optional:
xsd-validatoror in-processlibxml2binding for XSD check inprepare()(we already pullxmlschemafor Italy). - Optional:
schematron-rsif it lands; otherwise, run RO_CIUS Schematron in CI only (xsltproc) and rely on production fallback rejection at runtime.
No native signing libs, no XAdES, no SOAP. The interface is plain XML over HTTPS with bearer auth.
Open questions (for implementation bead)
-
Schematron coverage in
prepare(). XSD validation is cheap (~5 ms), but RO_CIUS Schematron has 200+ rules and is slow in pure Rust. Do we (a) ship a Rust port of the rule set, (b) shell out to xsltproc + skeleton.xsl, or (c) skip Schematron inprepare()and rely on ANAF's async feedback? Recommend (c) for v1 — let ANAF be the validator — and add a CI gate that runs Schematron over all goldens to keep our generators honest. -
County code source of truth. RO_CIUS requires
cbc:CountrySubentityto beRO-{XX}ISO 3166-2:RO. OurLocation.addressdoesn't carry an ISO subdivision today. Either (a) add a structured field, or (b) derive from postal code atprepare()time using a lookup table. Recommend (a) — implicit derivation will silently misreport for cross-county branches. -
Idempotency window.
(cif, content_hash)dedup needs a TTL — too short and replays after a long outage will double-submit; too long and we'll block legitimate "same invoice content, new sequence" cases (e.g. a recurring monthly invoice the merchant accidentally ran twice). Recommend: no TTL on the(cif, content_hash, invoice_number)triple. Plain content hash without the invoice number is wrong because two different invoices in different sequences can be byte-identical. -
Polling cadence.
/stareMesajreturns within seconds in sandbox but up to minutes in production. A poll-every-30s job is wasteful in sandbox and slow in production. Recommend: linear backoff per submission (5s, 15s, 30s, 60s, …, capped at 5 min), reset on every transition. -
What if ANAF returns
nokbut the seal store says we already accepted? Race condition: the upload was retried after a network blip and the secondindex_incarcaregotnokwhile the first gotok.listaMesajeFacturais the tiebreaker — call it on conflict, prefer theokrecord, and discard the duplicateindex_incarcaresilently. -
Cross-border B2B (intra-community supply). Recipient is a non-RO EU VAT taxpayer;
cbc:CompanyIDcarries the destination VAT (HU12345678,DE…). The Schematron requires aKline category and document-levelcbc:Notereferencing the legal basis (Codul Fiscal art. 294). Our adapter must inject the legal-basis note from a config-supplied template when the supply is detected as intra-community.
What could force us off Cloud-OK?
Nothing identified in the spec. No HSM, no on-device cert handling for the signing of payloads — the cert is only the user-side TLS authenticator during front-channel auth. The data plane is plain bearer token, no request signature. ANAF, not the issuer, applies the seal post-acceptance.
This is consistent with the Phase-1 thesis: Romania, like Hungary, is a green light for fiscal-cloud deployment.
Async receipt model — confirmed dovetail with fi-b7q
The submit() → poll() → finalize() split required by Romania's async
acceptance maps cleanly onto fi-b7q's status-polling loop. The only adapter-
specific contribution is the index_incarcare → id_message two-step
identifier promotion (section 5). The poller already supports two-step
finalisation for similar reasons (Italy SDI's metadata-file flow and France
NF525's batch acks).
Testing strategy
- Unit (in-adapter): UBL builder property tests (CIUS rule coverage),
OAuth refresh token rotation, idempotency hash determinism. Golden XSD
validation on all 6 fixture triplets using
xmllint(CI tool already used by the workspace). - Conformance: the 6 golden triplets validated as part of
cargo test -p romania-adapters. RO_CIUS Schematron is run in a separate CI step usingSchematron-skeleton.xsl+ xsltproc. - Integration (against sandbox): full upload → poll → descarcare round
trip. Gated behind
RO_ANAF_SANDBOX_REFRESH_TOKENenv var so CI without a refresh token still passes unit tests.
Rollout sequence (for the implementation bead)
- Day 1–2:
ubl_invoice.rs+ubl_credit_note.rsbuilders, XSD validate against all 6 goldens. - Day 3:
auth.rs— OAuth2 token + refresh, mocked against a recorded/tokenresponse, real flow gated behind sandbox env var. - Day 4:
client.rs+/uploadhappy path, parse<header>envelope, persistindex_incarcare. - Day 5:
poll.rs—stareMesajpolling state machine + linear backoff +descarcareZIP unpack. - Day 6: Error model (three tiers) + retry policies +
listaMesajeFacturareconciler for orphan uploads. - Day 7: Cross-border + credit-note + multi-VAT edge cases against goldens.
- Day 8: Soak test against sandbox: 200 invoices, observe rate-limit edges, tune backoff.
10. Citations
All facts above are sourced from one of:
- OUG 120/2021 — Ordonanță de Urgență 120/2021 privind administrarea, funcționarea și implementarea sistemului național privind factura electronică RO e-Factura. Monitorul Oficial nr. 960/2021. https://legislatie.just.ro/Public/DetaliiDocument/247791
- Legea 139/2022 — confirming OUG 120/2021 with amendments (B2G mandate effective 2022-07-01).
- OUG 130/2021 — fiscal measures including the B2G activation date.
- OUG 115/2023 — the 5-working-day window and the 2024-07 transition of legal effect.
- OUG 69/2024 — B2C extension and e-TVA pre-fill mechanism.
- ANAF e-Factura developer portal — https://mfinante.gov.ro/web/efactura/informatii-tehnice (technical specifications, Schematron, code lists, sample XMLs).
- ANAF OAuth2 documentation —
https://www.anaf.ro/anaf/internet/ANAF/servicii_online/inreg_online_pf/info_inreg_pf
- the Manual de înregistrare oameni de IT PDF on the same portal.
- RO_CIUS technical documentation —
RO_CIUS_v1.0.1.pdf and the corresponding Schematron pack
RO_CIUS_Schematron.zipavailable from the e-Factura technical-info page above. - OASIS UBL 2.1 specification —
https://docs.oasis-open.org/ubl/os-UBL-2.1/UBL-2.1.html and
the canonical XSDs at
https://docs.oasis-open.org/ubl/os-UBL-2.1/xsd/maindoc/UBL-Invoice-2.1.xsd
UBL-CreditNote-2.1.xsd.
- EN 16931-1:2017+A1:2019 — Electronic invoicing — Part 1: Semantic data model of the core elements of an electronic invoice (CEN). Underlying semantic model for RO_CIUS.
- EU eInvoicing CEF reference — https://ec.europa.eu/digital-building-blocks/sites/display/DIGITAL/eInvoicing+specifications (CIUS context, code lists, test corpus).
- Codul Fiscal RO (Legea 227/2015 cu modificările ulterioare) — art. 268–278 (territoriality), art. 319 (invoicing), art. 331 (domestic reverse-charge categories).
Per-fixture citations live in each meta.json under
testdata/conformance/romania/efactura/golden/.