DEV Community

Gabriel Petrescu
Gabriel Petrescu

Posted on

Every ANAF e-Factura rejection, explained — a developer's field guide

Romania's e-Factura is mandatory, and the hard part isn't the idea — it's ANAF's stack. You hit SOAP endpoints, an OAuth + digital-certificate lifecycle, and rejection codes that read like hex dumps. This is the field guide I wish I'd had: what actually makes ANAF reject an invoice, and how to read the error.

Why does ANAF reject my e-Factura?

Almost every rejection falls into one of four buckets:

  1. Schema / RO_CIUS validation — the XML is well-formed but breaks a business rule of the Romanian CIUS (the national customization of the EU standard, EN 16931). This is the most common and the most cryptic.
  2. Missing or malformed mandatory fields — a required party identifier, tax category, or total is absent or in the wrong shape.
  3. Certificate / OAuth lifecycle — your token expired, the cert isn't registered in SPV, or the auth handshake failed.
  4. SPV submission state — the document was accepted for processing but later rejected, and you have to poll for the verdict.

If you can classify a failure into one of these four, you already know where to look.

What is RO_CIUS and why does it fail when my XML "looks fine"?

RO_CIUS is Romania's Core Invoice Usage Specification — a national profile on top of the EU standard EN 16931. Your XML can be valid UBL and still violate a RO_CIUS rule, because CIUS adds country-specific constraints the base schema doesn't enforce. Typical categories:

  • Party identification — the supplier/customer tax ID (CUI/CIF) or registration number missing, malformed, or inconsistent with the VAT-payer flag.
  • Tax breakdown — the VAT category codes and the tax subtotals don't reconcile with the document totals (a rounding or category mismatch).
  • Currency & dates — wrong currency code, or a date format/issue-date rule the CIUS tightens.
  • Mandatory business terms — an EN 16931 business term (the "BT-…" fields) that RO_CIUS marks required is absent.

The trap: the base UBL schema passes, so your XML editor says "valid," but ANAF's CIUS layer rejects it. You need a validator that checks the RO_CIUS business rules, not just the schema.

How do I read a cryptic ANAF error code?

The fastest path is to run the invoice through a validator that translates the rule into plain language instead of echoing a rule ID. A raw message like a bare rule reference tells you nothing; what you want is "line 4's VAT category is 'exempt' but there's no exemption reason code" — the what and the where.

You can paste an invoice into the free Fiscura validator (fiscura.ro) and get exactly that — every RO_CIUS / EN 16931 error explained in plain RO/EN, no key and no card. (Full disclosure: I build Fiscura. The validator is genuinely free and the explanations are the whole point of this article.)

What about the certificate + OAuth + SPV part?

This is the other half of the pain, and it's orthogonal to your XML:

  • You authenticate to ANAF's SPV with a qualified digital certificate.
  • You manage an OAuth token lifecycle (obtain, refresh, handle expiry mid-batch).
  • You submit the invoice, get an upload index, and then poll for the processing verdict — acceptance isn't synchronous.

None of that is business logic you want to own. It changes, it's under-documented, and a single expired token silently fails a batch.

Can I skip building all of this?

Yes — that's the shortcut. Instead of hand-rolling SOAP + cert + polling, it's one REST call, or a few lines with the SDK:

npm i @gaperi/fiscura-sdk
Enter fullscreen mode Exit fullscreen mode

The API covers the full path: validate RO_CIUS, generate compliant UBL 2.1, submit to ANAF SPV (using your own ANAF cert), convert UBL↔CII, and even extract a PDF invoice into structured JSON. There's a free tier (100 calls/day, no card), a self-serve developer account, and a quickstart in the docs. It's EU-hosted (Frankfurt), so it's GDPR-clean.

Want to see it run before you sign up? Clone the examples — a live, no-signup validation call in seconds (curl + plain-fetch JS + the SDK): github.com/gaperi-consult/fiscura-examples.

The short version

  • Classify the rejection: RO_CIUS validation · missing field · cert/OAuth · SPV state.
  • Your XML being schema-valid doesn't mean it's RO_CIUS-valid — check the business rules.
  • Read errors in plain language, not rule IDs — paste one into the free validator.
  • Don't hand-build the ANAF cert/OAuth/SPV lifecycle unless you have to.

Informational developer tooling, not tax advice. If you integrate e-Factura, I'd love to hear which ANAF error wasted the most of your time.

Top comments (1)

Collapse
 
andreas_philippi profile image
Andreas Philippi •

Useful list, and to answer your closing question: we run factureaza.ro, an invoicing SaaS that has uploaded to SPV since the B2B mandate in 2024, and the error that cost us the most time is not a validation error at all. It is on the way back.

descarcare normally returns a ZIP (the signed invoice, or the error XML for a rejection). Sometimes it answers HTTP 200 with a JSON body instead: {"eroare": "..."}. Most of those errors are transient, except one: "S-au facut deja 10 descarcari de mesaj in cursul zilei". The same message can be downloaded only 10 times a day, and ANAF counts per message, not per app. One integration that downloads each answer once never gets near it. But many companies connect several tools to the same SPV: the invoicing app, the ERP, the accountant's software, sometimes a sync script. They all pull the same messages, and whoever comes after the tenth download is locked out of that invoice until tomorrow. ANAF writes the error with and without diacritics, so match it loosely.

What we do: download each answer once and keep it, check that the bytes are a complete ZIP before storing anything (we have also had empty archives), and when another tool has used up the limit, wait a day instead of retrying.

If you write a follow-up, status polling and archiving is where most integrators get it wrong.