Integrating ANAF e-Factura: an engineer's account of the SPV OAuth flow

Almost everything written in English about RO e-Factura is a regulatory summary. Deadlines, scope, penalties, who is in and who is out. Useful once. Useless the day you actually have to make a system talk to it.

This is the other half. What the integration is really shaped like, written from having built one.

The part nobody warns you about

The authentication is OAuth 2.0, authorization code flow, and it looks entirely ordinary until you read the token lifetimes.

The access token lasts 90 days. The refresh token lasts 365. Those numbers seem generous, and that is the trap. A normal OAuth integration refreshes every hour, so the refresh path gets exercised constantly and breaks loudly on the first day it is wrong. Here it runs four times a year. Whatever bug is in it will surface in the third month, on a Saturday, in production, long after everyone has stopped thinking about the integration at all.

Worse: the refresh has to happen while the access token is still valid. Let it lapse and the refresh token will not save you. You are back at the start of the flow, which is not a scripted request. It is the interactive one, gated on a qualified digital signature certificate on a physical USB token, plugged into a specific machine, by a specific person who has the PIN.

So the failure mode of a missed cron job is not a 401 you retry. It is a phone call to whoever holds the token.

Build the refresh to run monthly, not on day 89. Alert when the stored token is older than 30 days. The cost of refreshing early is one HTTP request; the cost of refreshing late is a person and a car.

Getting to the point where you can write code at all

The enrolment is genuinely the longest part, and it is administrative rather than technical:

  1. A qualified certificate for electronic signature, on a token, from a Romanian accredited provider.
  2. That certificate registered in SPV against the company, with the right role.
  3. Form 084 filed to activate the company in the RO e-Factura register.
  4. Separately, developer registration for API access, through Servicii Online, confirmed by a code sent to your email.

None of this is parallelisable and none of it is same-day. If you are scoping a project that includes e-Factura, start this on day one and build the rest while it clears. We have watched an otherwise finished integration sit idle waiting on step 2.

The flow itself

Token exchange goes to https://logincert.anaf.ro/anaf-oauth2/v1/token. You get a JWT back. From there the shape is:

Upload. POST the invoice XML. You get an index_incarcare, an upload index. This is not a confirmation that the invoice was accepted. It is a receipt that the file arrived.

Poll. Ask for the state of that index. It will be processing, then either accepted or rejected.

Download. Pull the response envelope, which is a ZIP containing the original and either the signed acceptance or the validation errors.

The mistake to avoid is treating the upload response as the end of the transaction. It is the beginning of one. An invoice that uploads successfully and fails validation four minutes later is not sent, and if nothing in your system is watching for the second event, your records will say it was.

Store the index against the invoice immediately, and make the polled state the field that means "submitted". Anything else drifts.

What actually gets rejected

The format is UBL 2.1, and the validation is strict in ways that feel arbitrary until you have hit each one.

The failures cluster:

  • Unit of measure codes. They come from UN/ECE Recommendation 20. "buc" is not a code. H87 is. Half of the first batch of rejections in any integration is this.
  • VAT category codes and exemption reasons. A zero-rated line needs a category code and a reason code, and the pair has to be internally consistent. A reverse-charge line that omits the reason is rejected.
  • Partner identification. The CUI has to be present in the shape ANAF expects, with the RO prefix where the partner is VAT registered and without it where they are not. Getting this from a client's existing customer table, where it has been typed by hand for a decade, is the real job.
  • Addresses. County codes are a controlled list, not free text.
  • Rounding. The document totals have to agree with the sum of the lines, exactly, at two decimals.

That last one deserves its own article, and it has one, because the arithmetic that produces a mismatch is subtler than it looks.

Notice what the list has in common. Almost none of it is about e-Factura. It is about the quality of data that has been accumulating in an ERP since before anybody imagined it would be machine-validated by the state. The integration does not create these problems. It is the first thing that has ever refused to tolerate them.

Which is why the honest estimate for an e-Factura project is mostly data cleanup, and why an integrator who quotes you purely for the API work has either not done one or is planning to hand the cleanup back to you halfway through.

Timing, and the thing about the deadline

The transmission deadline moved on 1 January 2026: it is now 5 working days from the invoice issue date, where it was 5 calendar days. That change is quietly significant for anyone who built a scheduler in 2024 and used a fixed 120 hour timer. Working days is a different calculation, it needs a Romanian public holiday calendar, and that calendar moves with Orthodox Easter every year.

If your queue counts hours, it is now wrong in the safe direction on most weeks and wrong in the expensive direction around Easter and early December. Fix it before the calendar does it for you.

Verify the current rules directly with ANAF before you rely on them. Dates in this space have moved repeatedly, and an integrator who tells you the deadline from memory is telling you what it was when they last looked.

What we would tell you not to do

Do not move your invoicing into somebody else's SaaS just to get e-Factura, if you already have a system that works. That is the default advice you will get, because it is the easiest thing to sell you. It is also a migration of your whole billing process to solve one submission problem.

Do not build this without a sandbox cycle. There is a test environment. Use it until you can produce a clean acceptance for every invoice shape you actually issue, including the awkward ones: credit notes, advance payments, reverse charge, intra-community supply.

And do not let the integration be write-only. The same API tells you what your suppliers have sent you. Most companies build the outbound half, tick the compliance box, and leave the inbound half on the table, which is the half that could have replaced somebody typing purchase invoices into the accounting system by hand.

That asymmetry is the strangest thing about how this mandate has been implemented in practice. Every company was forced to build a pipe. Most are only using it in one direction.