14 min read

How to Integrate with ANAF e-Factura API (2026 Guide)

Complete technical guide to Romania's ANAF e-Factura system. Learn about OAuth2 authentication, UBL CIUS-RO XML format, API endpoints, error codes, and how to submit e-invoices programmatically.

What Is ANAF e-Factura?

e-Factura is Romania's mandatory electronic invoicing system, operated by ANAF (Agentia Nationala de Administrare Fiscala -- the National Agency for Fiscal Administration). It requires businesses to submit invoices in structured XML format through ANAF's centralized platform, where the tax authority validates, registers, and distributes them to recipients.

Unlike simple PDF invoicing, e-Factura mandates a specific XML schema based on UBL 2.1 with the CIUS-RO (Core Invoice Usage Specification for Romania) profile. Every invoice must pass Schematron validation against both the European EN 16931 base rules and Romania-specific BR-RO business rules before ANAF will accept it.

The system serves as both a compliance mechanism and a real-time reporting tool. ANAF receives every B2B and B2G invoice at the moment of issuance, enabling the tax authority to cross-reference VAT declarations in real time and close the VAT gap.

Who Needs to Use e-Factura in 2026?

As of January 2024, e-Factura is mandatory for all B2B transactions between Romanian VAT-registered entities. The rollout happened in phases:

  • January 2024: Mandatory for all B2B transactions between Romanian tax residents (both supplier and customer registered for VAT in Romania).
  • March 2024: Invoices not submitted through e-Factura are considered non-existent for tax deduction purposes. Buyers cannot deduct VAT on invoices that were not routed through the system.
  • July 2024: Extension to B2C transactions for certain categories (high-risk products).
  • 2025-2026: Full coverage of all taxable supplies, including cross-border B2B with Romanian VAT implications.

If your software serves Romanian businesses -- whether as an ERP, accounting platform, or marketplace -- you need e-Factura integration. There is no opt-out. Non-compliant invoices cannot be used for VAT deduction, which means your customers will demand compliance.

Technical Architecture of the ANAF API

The ANAF e-Factura API is a REST-style HTTP interface. Despite being a government API, it follows reasonable patterns: OAuth2 for authentication, XML request bodies for invoice submission, and JSON responses for status queries.

Environments

ANAF provides two environments:

EnvironmentBase URLPurpose
Sandbox (test)https://api.anaf.ro/test/FCTEL/restDevelopment and testing. No real fiscal effect.
Productionhttps://api.anaf.ro/prod/FCTEL/restLive invoices with legal validity.

Both environments use the same API contract. The test environment accepts any valid XML but does not actually register invoices with the fiscal authority. Always develop and test against the sandbox first.

Core API Endpoints

The ANAF e-Factura API has a small surface area. There are only a few endpoints you need to work with:

1. Upload Invoice

POST /upload/FACT1/{vatNumber}
Content-Type: text/xml
Authorization: Bearer {access_token}

{UBL XML body}

This endpoint accepts a UBL 2.1 XML document conforming to the CIUS-RO profile. The vatNumber in the URL path is the supplier's VAT number without the "RO" prefix (e.g., 12345678). ANAF returns a response containing an upload index number that you use to track the invoice.

2. Check Message Status

GET /listaMesajeFactura?cif={vatNumber}&zile={days}
Authorization: Bearer {access_token}

Returns a list of all invoice messages for the given VAT number within the specified day range (max 60 days). Each message includes a status: ok (accepted), nok (rejected), or in prelucrare (processing). You poll this endpoint to determine the outcome of your submission.

3. Download Response

GET /descarcare/{id}
Authorization: Bearer {access_token}

Downloads the ANAF response for a specific message ID. For accepted invoices, this returns the official signed XML. For rejected invoices, it returns an XML error report with specific validation failures.

OAuth2 Authentication Flow

ANAF uses OAuth2 Authorization Code flow with a significant twist: the initial authorization requires a qualified digital certificate (USB token). This certificate is issued by a Romanian-accredited certificate authority and is physically bound to a hardware USB device.

Authentication Steps

  1. Initial authorization (manual, one-time): A company representative navigates to ANAF's OAuth portal at https://logincert.anaf.ro/anaf-oauth2/v1/authorize with the USB certificate plugged in. They grant access to the e-Factura scope for your application's client_id. This produces an authorization code.
  2. Token exchange: Your application exchanges the authorization code for an access token and refresh token via POST https://logincert.anaf.ro/anaf-oauth2/v1/token.
  3. Token refresh: Access tokens expire after 90 days. Refresh tokens last 365 days. You must implement automatic refresh to avoid interrupting invoice submission.
// Token exchange request
const response = await fetch("https://logincert.anaf.ro/anaf-oauth2/v1/token", {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({
    grant_type: "authorization_code",
    code: authorizationCode,
    client_id: ANAF_CLIENT_ID,
    client_secret: ANAF_CLIENT_SECRET,
    redirect_uri: "https://your-app.com/callback/anaf",
  }).toString(),
});

const tokens = await response.json();
// tokens.access_token — valid 90 days
// tokens.refresh_token — valid 365 days

The USB certificate requirement is the single biggest friction point in ANAF integration. Your users must physically have the certificate, install the correct drivers, and complete the browser-based authorization. There is no way to automate this initial step.

UBL CIUS-RO XML Format

ANAF requires invoices in UBL (Universal Business Language) version 2.1, specifically conforming to the CIUS-RO profile. This is Romania's national adaptation of the European standard EN 16931.

XML Structure Overview

A CIUS-RO invoice XML has this high-level structure:

<?xml version="1.0" encoding="UTF-8"?>
<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>INV-2026-0042</cbc:ID>
  <cbc:IssueDate>2026-02-10</cbc:IssueDate>
  <cbc:DueDate>2026-03-10</cbc:DueDate>
  <cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
  <cbc:DocumentCurrencyCode>RON</cbc:DocumentCurrencyCode>

  <!-- Supplier (AccountingSupplierParty) -->
  <cac:AccountingSupplierParty>
    <cac:Party>
      <cac:PartyTaxScheme>
        <cbc:CompanyID>RO12345678</cbc:CompanyID>
        <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
      </cac:PartyTaxScheme>
      <cac:PartyLegalEntity>
        <cbc:RegistrationName>Acme SRL</cbc:RegistrationName>
        <cbc:CompanyID>J40/1234/2020</cbc:CompanyID>
      </cac:PartyLegalEntity>
      <cac:PostalAddress>
        <cbc:StreetName>Str. Exemplu 1</cbc:StreetName>
        <cbc:CityName>Bucharest</cbc:CityName>
        <cbc:PostalZone>010101</cbc:PostalZone>
        <cac:Country><cbc:IdentificationCode>RO</cbc:IdentificationCode></cac:Country>
      </cac:PostalAddress>
    </cac:Party>
  </cac:AccountingSupplierParty>

  <!-- Customer, Tax Totals, Line Items, etc. -->
</Invoice>

The full XML for even a simple single-line invoice runs 150-200 lines. Complex invoices with multiple tax rates, allowances, and payment terms can exceed 500 lines. Every field has strict ordering requirements -- UBL elements must appear in the exact sequence defined by the schema.

Required Fields for CIUS-RO

Beyond the base EN 16931 requirements, CIUS-RO adds Romania-specific mandatory fields:

  • CustomizationID: Must be exactly urn:cen.eu:en16931:2017#compliant#urn:efactura.mfinante.ro:CIUS-RO:1.0.1
  • Supplier trade register number (CompanyID under PartyLegalEntity): Format J{county}/{number}/{year}
  • Supplier VAT number with "RO" prefix
  • Invoice currency: RON for domestic transactions (EUR allowed for cross-border)
  • Tax category codes: Must use the correct UNCL 5305 codes (S for standard rate, Z for zero-rate, E for exempt, etc.)

Schematron Validation Rules

Before submitting to ANAF, you should validate your XML locally against two sets of Schematron rules. ANAF will reject invalid invoices, and debugging rejections through the API is slow and painful.

EN 16931 Base Rules

The European base standard defines approximately 170 business rules (prefixed BR-, BR-CO-, BR-CL-). These cover fundamental requirements:

  • BR-01: An invoice shall have a specification identifier.
  • BR-05: An invoice shall have an invoice currency code.
  • BR-CO-15: Invoice total VAT amount shall equal the sum of all VAT category tax amounts.
  • BR-CL-01: Invoice currency code shall be coded using ISO 4217 alpha-3.

CIUS-RO Specific Rules

Romania adds approximately 30 additional rules (prefixed BR-RO-) that enforce national requirements:

  • BR-RO-010: The supplier's trade register number must be present and match the Romanian format.
  • BR-RO-065: The CustomizationID must include the CIUS-RO identifier.
  • BR-RO-070: For domestic invoices, currency should be RON.
  • BR-RO-080: The tax category must use valid Romanian VAT rates (19%, 9%, 5%, or 0%).

Running Schematron validation requires an XSLT processor. In Node.js, this typically means using Saxon-JS, which adds complexity to your build pipeline. The Schematron rule files (.sch) need to be compiled to XSLT (.xsl) first, then applied to your invoice XML.

Common ANAF Error Codes

When ANAF rejects an invoice, the error response is in Romanian and references specific validation rules. Here are the errors you will encounter most frequently:

Error PatternMeaningFix
BR-RO-010Missing or invalid trade register numberAdd CompanyID under PartyLegalEntity in J{xx}/{nnnn}/{yyyy} format
BR-RO-065Invalid CustomizationIDSet to the exact CIUS-RO URN string
BR-CO-15VAT totals do not sum correctlyRecalculate VAT amounts. Rounding must use banker's rounding to 2 decimal places
BR-S-08Standard rate VAT amount is incorrectCheck line-level VAT calculations against category totals
CII-SR-029XML element ordering violationEnsure all UBL elements appear in schema-defined order
HTTP 403OAuth token expired or invalidRefresh the access token. If refresh also fails, re-authorize with USB certificate
HTTP 429Rate limit exceededANAF allows approximately 100 requests per minute per VAT number. Implement backoff

For a comprehensive reference of all ANAF error codes, see our complete ANAF error codes guide.

The Manual Integration Approach

Building ANAF e-Factura integration from scratch involves significant work. Here is what a typical implementation looks like:

// Step 1: Build the UBL XML manually
// This is a simplified example — real invoices have 200+ lines of XML
function buildUblXml(invoice: Invoice): string {
  return `<?xml version="1.0" encoding="UTF-8"?>
<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>${ invoice.number }</cbc:ID>
  <cbc:IssueDate>${ invoice.issueDate }</cbc:IssueDate>
  <!-- ... 150+ more lines of XML construction ... -->
</Invoice>`;
}

// Step 2: Validate with Schematron (requires Saxon-JS)
import SaxonJS from "saxon-js";

async function validateCiusRo(xml: string): Promise<ValidationResult> {
  // Load pre-compiled Schematron XSLT
  const schematronXslt = await loadSchematronRules("cius-ro");
  const result = await SaxonJS.transform({
    sourceText: xml,
    stylesheetInternal: schematronXslt,
    destination: "serialized",
  });
  return parseSchematronOutput(result.principalResult);
}

// Step 3: Handle OAuth2 token management
async function getAnafToken(connection: Connection): Promise<string> {
  if (connection.tokenExpiresAt > new Date()) {
    return connection.accessToken;
  }
  // Refresh the token
  const response = await fetch("https://logincert.anaf.ro/anaf-oauth2/v1/token", {
    method: "POST",
    body: new URLSearchParams({
      grant_type: "refresh_token",
      refresh_token: connection.refreshToken,
      client_id: process.env.ANAF_CLIENT_ID,
      client_secret: process.env.ANAF_CLIENT_SECRET,
    }).toString(),
  });
  // Handle token rotation, error cases, storage...
  return newToken;
}

// Step 4: Submit to ANAF
async function submitToAnaf(xml: string, vatNumber: string, token: string) {
  const response = await fetch(
    `https://api.anaf.ro/prod/FCTEL/rest/upload/FACT1/${ vatNumber }`,
    {
      method: "POST",
      headers: {
        "Content-Type": "text/xml",
        Authorization: `Bearer ${ token }`,
      },
      body: xml,
    },
  );
  // Parse XML response for upload index...
}

// Step 5: Poll for status (ANAF is async — no instant confirmation)
async function pollAnafStatus(vatNumber: string, token: string) {
  // Poll every 30s for up to 24 hours
  // Parse the Romanian-language response messages
  // Handle: ok, nok, in_prelucrare states
  // Download detailed error report on rejection
}

This is a simplified view. A production implementation also needs: retry logic with exponential backoff, certificate management, audit logging, error translation from Romanian, XML element ordering enforcement, rounding rule compliance, and webhook notifications to your application. Conservatively, this is 2-4 months of engineering work.

The Mandato Approach: JSON In, Compliance Out

Mandato abstracts all of this complexity behind a single REST API. You send JSON, we produce valid CIUS-RO XML, handle OAuth2 authentication, submit to ANAF, poll for status, and notify you via webhooks.

import { MandatoClient } from "@getmandato/sdk";

const mandato = new MandatoClient({ apiKey: "mk_live_..." });

// That's the entire integration. One API call.
const invoice = await mandato.invoices.create({
  country: "RO",
  supplier: {
    vatNumber: "RO12345678",
    name: "Acme SRL",
    tradeRegister: "J40/1234/2020",
    address: {
      street: "Str. Exemplu 1",
      city: "Bucharest",
      zip: "010101",
      country: "RO",
    },
  },
  customer: {
    vatNumber: "RO87654321",
    name: "Client SRL",
    address: {
      street: "Bd. Unirii 10",
      city: "Cluj-Napoca",
      zip: "400094",
      country: "RO",
    },
  },
  currency: "RON",
  issueDate: "2026-02-10",
  dueDate: "2026-03-10",
  lines: [
    {
      description: "Software development services — January 2026",
      quantity: 1,
      unitPrice: 5000,
      vatRate: 19,
    },
    {
      description: "Cloud hosting (monthly)",
      quantity: 1,
      unitPrice: 200,
      vatRate: 19,
    },
  ],
});

console.log(invoice.id);       // "inv_abc123"
console.log(invoice.status);   // "submitted"
console.log(invoice.govId);    // ANAF upload index

What happens behind the scenes when you make this API call:

  1. Validation: Your JSON is validated against EN 16931 and CIUS-RO business rules. If anything is wrong, you get clear English-language error messages with field-level pointers.
  2. Conversion: JSON is converted to fully compliant UBL 2.1 CIUS-RO XML, with correct element ordering, namespace declarations, and tax calculations.
  3. Authentication: We use the OAuth2 tokens your company connected during onboarding. Tokens are automatically refreshed before expiry.
  4. Submission: The XML is uploaded to ANAF's API. We handle retries and rate limiting.
  5. Status tracking: We poll ANAF for the invoice status and fire a webhook to your application when the status changes to accepted or rejected.
  6. Error translation: If ANAF rejects the invoice, we translate the Romanian error messages into actionable English feedback.

Receiving Status Updates via Webhooks

// Register a webhook endpoint in your Mandato dashboard or via API
// Mandato sends POST requests when invoice status changes

app.post("/webhooks/mandato", async (req, res) => {
  const event = mandato.webhooks.constructEvent(
    req.body,
    req.headers["x-mandato-signature"],
  );

  switch (event.type) {
    case "invoice.accepted":
      // ANAF accepted the invoice — update your records
      await db.invoices.update(event.data.externalId, {
        status: "accepted",
        govId: event.data.govId,
        acceptedAt: new Date(event.data.timestamp),
      });
      break;

    case "invoice.rejected":
      // ANAF rejected — event.data.errors has translated error messages
      await db.invoices.update(event.data.externalId, {
        status: "rejected",
        errors: event.data.errors, // English error messages with fix suggestions
      });
      break;
  }

  res.status(200).send("ok");
});

Romanian VAT Rates Reference

Romania uses four VAT rate tiers. Using the wrong rate or tax category code is a common source of ANAF rejections.

RateUBL Category CodeDescriptionCommon Uses
19%S (Standard)Standard rateMost goods and services
9%S (Standard)Reduced rate IFood, water, medicines, hotels, restaurant services
5%S (Standard)Reduced rate IIBooks, housing (first-time buyers under thresholds), cultural events
0%Z (Zero rated)Zero rateIntra-EU supplies, exports
ExemptE (Exempt)VAT exemptFinancial services, insurance, medical services, education

When using the Mandato API, you just pass the numeric vatRate value (19, 9, 5, or 0). We map it to the correct UBL tax category code and handle the special cases for exempt supplies.

Compliance Timeline and Penalties

Romania has been aggressive about enforcement. Key dates and consequences:

  • Penalty for non-compliance: Invoices not submitted through e-Factura are treated as non-existent. Buyers cannot claim VAT deduction.
  • Fines: ANAF can impose fines ranging from 5,000 RON to 10,000 RON per invoice for systematic non-compliance.
  • Audit risk: Businesses not using e-Factura are flagged for priority audit by ANAF.

The practical impact is stark: if your software does not submit invoices through e-Factura, your Romanian customers cannot do business. Their clients will refuse invoices that were not processed through the system, because they cannot deduct the VAT.

Getting Started with Mandato

Setting up ANAF e-Factura through Mandato takes about 5 minutes for the technical integration. The OAuth2 connection with ANAF (which requires the USB certificate) is guided through our dashboard with step-by-step instructions.

  1. Create an account at app.getmandato.dev. The free sandbox includes 50 invoices per month.
  2. Add your company with your Romanian VAT number and trade register number.
  3. Connect to ANAF through our guided OAuth2 flow. You will need the USB digital certificate for this one-time step.
  4. Submit your first invoice using the API or SDK. Test against the sandbox first.
  5. Go live by switching to production mode when you are ready.

Full API documentation is available at docs.getmandato.dev. The TypeScript SDK (@getmandato/sdk) provides full type safety and autocompletion for all API endpoints.

Romania is just the first country. Mandato is expanding to Italy (SDI), Belgium (Peppol), Poland (KSeF), France (PPF), and Germany (XRechnung). When you build on our API, you get new country support without changing your integration. Check our EU e-invoicing timeline to see what is coming next.

Skip the complexity. Use Mandato.

One API for all EU e-invoicing systems. Send JSON, we handle XML conversion, government authentication, submission, and status tracking. Start with our free sandbox in 5 minutes.

Free sandbox with 50 invoices/month. No credit card required.