ANAF e-Factura TypeScript SDK
A comprehensive TypeScript SDK for interacting with the Romanian ANAF e-Factura system. This SDK provides OAuth 2.0 authentication, document upload/download, validation, UBL generation, and company data lookup capabilities.
Features
- OAuth 2.0 Authentication: Complete OAuth flow with USB token support
- Document Operations: Upload, status checking, and download
- Message Management: List and paginate invoice messages
- Validation: XML validation and digital signature verification
- PDF Conversion: Convert XML invoices to PDF format
- UBL Generation: Create compliant UBL 2.1 XML invoices
- Received-Invoice Parsing: Read incoming UBL 2.1 and CII documents into a typed header plus its lines
- Company Data Lookup: Fetch Romanian company details from public ANAF API
- TypeScript: Full type safety and IntelliSense support
Installation
pnpm add @florinszilagyi/anaf-ts-sdk
Quick Start
The SDK is organized into four main classes:
1. AnafAuthenticator - OAuth 2.0 Authentication
import { AnafAuthenticator } from '@florinszilagyi/anaf-ts-sdk';
const auth = new AnafAuthenticator({
clientId: 'your-oauth-client-id',
clientSecret: 'your-oauth-client-secret',
redirectUri: 'https://your-app.com/oauth/callback',
});
// Get authorization URL (user will authenticate with USB token)
const authUrl = auth.getAuthorizationUrl();
console.log('Redirect user to:', authUrl);
// Exchange authorization code for tokens
const tokens = await auth.exchangeCodeForToken(authorizationCode);
console.log('Access token:', tokens.access_token);
// Refresh tokens when needed
const newTokens = await auth.refreshAccessToken(tokens.refresh_token);
2. AnafEfacturaClient - API Operations
The client is constructed once with a refresh token and resolves access tokens itself, so no call takes a token argument.
import { AnafEfacturaClient } from '@florinszilagyi/anaf-ts-sdk';
const client = new AnafEfacturaClient(
{
vatNumber: 'RO12345678',
testMode: true, // Use test environment
refreshToken: tokens.refresh_token,
},
auth // the AnafAuthenticator from step 1
);
// Upload a document
const uploadResult = await client.uploadDocument(xmlContent, {
standard: 'UBL',
executare: true,
});
// Check upload status
const status = await client.getUploadStatus(uploadResult.indexIncarcare);
// Download processed document (ZIP archive: invoice + detached signature)
if (status.idDescarcare) {
const result = await client.downloadDocument(status.idDescarcare);
// Or unwrap the archive and get the invoice XML directly
const invoiceXml = await client.downloadDocumentXml(status.idDescarcare);
// …which is what you need to render the invoice as a PDF
const pdf = await client.convertXmlToPdf(invoiceXml, 'FACT1');
}
// List recent messages
const messages = await client.getMessages({
zile: 7, // Last 7 days
filtru: 'E', // Only errors
});
// Validate XML
const validation = await client.validateXml(xmlContent, 'FACT1');
// Convert XML to PDF
const pdfBuffer = await client.convertXmlToPdf(xmlContent, 'FACT1');
3. UblBuilder - UBL XML Generation
import { UblBuilder } from '@florinszilagyi/anaf-ts-sdk';
const builder = new UblBuilder();
const xml = builder.generateInvoiceXml({
invoiceNumber: 'INV-2024-001',
issueDate: new Date(),
supplier: {
registrationName: 'Company SRL',
companyId: 'RO12345678',
vatNumber: 'RO12345678',
address: {
street: 'Str. Example 1',
city: 'Bucharest',
postalZone: '010101',
},
},
customer: {
registrationName: 'Customer SRL',
companyId: 'RO87654321',
address: {
street: 'Str. Customer 2',
city: 'Cluj-Napoca',
postalZone: '400001',
},
},
lines: [
{
description: 'Product/Service',
quantity: 1,
unitPrice: 100,
taxPercent: 19,
},
],
isSupplierVatPayer: true,
});
Optional parties and contacts:
const xml = builder.generateInvoiceXml({
// ...the fields above
note: 'Delegat: Ion Exemplu', // BT-22
supplier: { /* ... */ contactName: 'Ana Exemplu' }, // BT-41
customer: { /* ... */ contactName: 'Ion Exemplu' }, // BT-56
// BG-11: only for a seller that has appointed a tax representative.
taxRepresentative: {
name: 'Reprezentant Exemplu SRL',
vatNumber: 'RO11223344', // with country prefix
address: { street: 'Str. Exemplu 5', city: 'Cluj-Napoca', postalZone: '400002', county: 'RO-CJ' },
},
});
The builder throws AnafValidationError when the tax representative has no
name, no prefixed VAT number, no ISO 3166-2:RO county code (RO-CJ) for a
Romanian address, or when the supplier is not a VAT payer.
4. AnafDetailsClient - Company Data Lookup
import { AnafDetailsClient } from '@florinszilagyi/anaf-ts-sdk';
const detailsClient = new AnafDetailsClient({
timeout: 30000,
url: 'https://webservicesp.anaf.ro/api/PlatitorTvaRest/v9/tva', // Optional: custom ANAF API URL
});
// Fetch company data by VAT code (single company)
const result = await detailsClient.getCompanyData('RO12345678');
if (result.success) {
console.log('Company:', result.data[0].name);
console.log('Address:', result.data[0].address);
console.log('VAT registered:', result.data[0].scpTva);
console.log('Registration number:', result.data[0].registrationNumber);
console.log('Phone:', result.data[0].contactPhone);
} else {
console.error('Error:', result.error);
}
// Validate VAT code format
const isValid = await detailsClient.isValidVatCode('RO12345678');
console.log('Valid format:', isValid);
// Batch fetch multiple companies (single API call)
const batchResult = await detailsClient.batchGetCompanyData(['RO12345678', 'RO87654321', 'RO11111111']);
if (batchResult.success) {
batchResult.data.forEach((company, index) => {
console.log(`Company ${index + 1}:`, company.name);
console.log(`VAT registered:`, company.scpTva);
});
} else {
console.error('Batch error:', batchResult.error);
}
// Bulk lookup: max 100 CUIs, a fixed request date, no e-Factura registry request
const bulk = await detailsClient.batchGetCompanyData(['12345678', '87654321'], {
date: '2026-01-31',
includeEfacturaRegistry: false,
});
if (bulk.success) {
bulk.data.forEach((company) => console.log(company.vatCode, company.caenCode));
console.log('Not found:', bulk.notFound);
} else if (bulk.retryable) {
console.log('Retry later', bulk.statusCode, bulk.retryAfter);
} else {
console.log('Final answer:', bulk.error, bulk.notFound);
}
// Configuration options
const customClient = new AnafDetailsClient({
timeout: 60000, // 60 second timeout
url: 'https://custom-anaf-proxy.example.com/api/tva', // Custom endpoint (e.g., proxy server)
});
5. parseReceivedInvoice - Reading Received Invoices
Parses a document received through SPV — UBL 2.1 Invoice/CreditNote or CII
CrossIndustryInvoice — into a flat header plus its source lines. Every amount
comes back as the exact decimal string the document published, never a
JavaScript number, so nothing is lost to float representation before it reaches
your own money handling.
import { parseReceivedInvoice } from '@florinszilagyi/anaf-ts-sdk';
const xml = await client.downloadDocumentXml(status.idDescarcare);
const invoice = parseReceivedInvoice(xml);
invoice.documentStandard; // 'ubl' | 'cii'
invoice.documentKind; // 'invoice' | 'credit_note' | 'corrective' | a raw type code
invoice.pdfStandard; // 'FACT1' | 'FCN' — which converter convertXmlToPdf needs
invoice.invoiceNumber; // 'EXMPL-2026-001'
invoice.issueDate; // '2026-08-01'
invoice.supplierName; // 'Exemplu SRL'
invoice.supplierVatCode; // 'RO12345678'
invoice.totalAmount; // '119.00' — a string, not 119
invoice.notes; // every invoice note (BT-22) in document order; [] when there are none
invoice.supplierContact; // { name, telephone, email } | null
invoice.customerContact; // { name, telephone, email } | null
invoice.taxRepresentative; // { name, vatCode, address } | null (BG-11)
invoice.sourceLines; // one ReceivedInvoiceLine per document line
invoice.sourceLineDiagnostics; // human-readable notes for malformed/absent line facts
Every header field is nullable. invoiceNumber, issueDate,
totalAmount and the rest come back as null when the document does not
publish them or publishes them in a form that does not validate — the parser
never substitutes a default. Check the fields your own flow depends on before
using them.
The parser refuses to guess. It throws:
AnafValidationError— the document is oversized (12 MB of UTF-8) or its root element is not a recognised invoice root.AnafXmlParsingError— the XML is not well-formed, declares an external entity in its DTD (even an unreferenced one), or names an element with a reserved JavaScript property name such as__proto__.AnafAmbiguousTaxTotalError— the document carries several VAT totals and none of them uniquely matches the document currency.
Fields it cannot read as a valid decimal, date or currency come back as null
rather than coerced, and unreadable line facts are reported in
sourceLineDiagnostics while the line itself is kept.
General and DTD entity expansion stays disabled (these documents are
supplier-authored, and a recursive entity definition would balloon memory).
The five predefined XML entities and decimal/hex character references — the
way Romanian diacritics usually arrive — are decoded, exactly one level deep:
& is the literal text &, never &.
AnafDetailsClient Configuration
The AnafDetailsClient supports the following configuration options:
| Option | Type | Default | Description |
|---|---|---|---|
timeout | number | 30000 | Request timeout in milliseconds |
url | string | 'https://webservicesp.anaf.ro/api/PlatitorTvaRest/v9/tva' | ANAF API endpoint URL |
Configuration Examples
// Default configuration
const client = new AnafDetailsClient();
// Custom timeout
const clientWithTimeout = new AnafDetailsClient({
timeout: 60000, // 60 seconds
});
// Custom API endpoint (useful for proxy servers or testing)
const clientWithCustomUrl = new AnafDetailsClient({
url: 'https://your-proxy.example.com/anaf-api',
timeout: 45000,
});
// Minimal configuration
const minimalClient = new AnafDetailsClient({
timeout: 15000, // Fast timeout for quick responses
});
Use Cases for Custom URL
- Proxy Server: Route requests through your own proxy for logging/monitoring
- Load Balancer: Distribute requests across multiple ANAF endpoints
- Testing: Point to a mock server during development
- Regional Endpoints: Use different ANAF regional servers if available
- Corporate Firewall: Route through approved corporate gateways
Complete Example
import { AnafAuthenticator, AnafClient, AnafDetailsClient, UblBuilder } from '@florinszilagyi/anaf-ts-sdk';
// Setup
const auth = new AnafAuthenticator({
clientId: process.env.ANAF_CLIENT_ID,
clientSecret: process.env.ANAF_CLIENT_SECRET,
redirectUri: 'https://myapp.com/oauth/callback',
});
const client = new AnafClient({
vatNumber: 'RO12345678',
testMode: true,
});
const detailsClient = new AnafDetailsClient();
const builder = new UblBuilder();
// 1. Get customer company data
const customerData = await detailsClient.getCompanyData('RO87654321');
if (!customerData.success) {
throw new Error(`Customer not found: ${customerData.error}`);
}
// 2. Authentication (one-time setup)
const authUrl = auth.getAuthorizationUrl();
// Direct user to authUrl, they authenticate with USB token
const tokens = await auth.exchangeCodeForToken(authCode);
// 3. Generate invoice XML using fetched company data
const xml = builder.generateInvoiceXml({
invoiceNumber: 'INV-2024-001',
issueDate: new Date(),
supplier: {
registrationName: 'My Company SRL',
companyId: 'RO12345678',
vatNumber: 'RO12345678',
address: {
street: 'Str. Example 1',
city: 'Bucharest',
postalZone: '010101',
},
},
customer: {
registrationName: customerData.data[0].name,
companyId: customerData.data[0].vatCode,
vatNumber: customerData.data[0].vatCode,
address: {
street: customerData.data[0].address,
city: 'Cluj-Napoca', // Parse from address if needed
postalZone: customerData.data[0].postalCode || '400001',
},
},
lines: [
{
description: 'Consulting Services',
quantity: 1,
unitPrice: 1000,
taxPercent: customerData.data[0].scpTva ? 19 : 0, // Apply VAT if customer is VAT registered
},
],
isSupplierVatPayer: true,
});
// 4. Upload to ANAF
const uploadResult = await client.uploadDocument(tokens.access_token, xml);
// 5. Monitor status
const status = await client.getUploadStatus(tokens.access_token, uploadResult.index_incarcare);
// 6. Download result
if (status.id_descarcare) {
const result = await client.downloadDocument(tokens.access_token, status.id_descarcare);
}
Development & Testing
Prerequisites
-
USB Security Token: Required for ANAF authentication
- Supported tokens: Any qualified certificate from Romanian CA
- Install manufacturer drivers (SafeNet, Gemalto, etc.)
- Certificate must be registered with ANAF SPV
-
ANAF OAuth Application: Register at ANAF Portal
- Navigate: Servicii Online → Înregistrare utilizatori → DEZVOLTATORI APLICAȚII
- Register application with your callback URL
Environment Setup
Create a .env file in your project root:
ANAF_CLIENT_ID=your_oauth_client_id_here
ANAF_CLIENT_SECRET=your_oauth_client_secret_here
Local Development with ngrok
For local testing, you need a public HTTPS URL for OAuth callbacks:
-
Install ngrok:
# Using npm npm install -g ngrok # Or download from https://ngrok.com/ -
Expose local server:
# Start your local server on port 3000 npm start # In another terminal, expose it publicly ngrok http 3000 -
Update OAuth Settings:
- Copy the ngrok HTTPS URL (e.g.,
https://abc123.ngrok.io) - Register callback URL:
https://abc123.ngrok.io/oauth/callback - Update your AnafAuthenticator configuration:
const auth = new AnafAuthenticator({ clientId: process.env.ANAF_CLIENT_ID, clientSecret: process.env.ANAF_CLIENT_SECRET, redirectUri: 'https://abc123.ngrok.io/oauth/callback', // Your ngrok URL }); - Copy the ngrok HTTPS URL (e.g.,
OAuth Authentication Flow
The complete OAuth flow with USB token authentication:
-
Generate Authorization URL:
const authUrl = auth.getAuthorizationUrl(); console.log('Direct user to:', authUrl); -
User Authentication Process:
- User clicks/visits the authorization URL
- ANAF login page opens
- Insert USB Token: User inserts USB security token
- Enter PIN: User enters token PIN when prompted
- Certificate Selection: Browser shows certificate selection dialog
- Select Certificate: User selects appropriate certificate
- Authorize Application: User grants permissions to your app
- Redirect: Browser redirects to your callback URL with authorization code
-
Handle Callback:
// Your callback endpoint receives: ?code=AUTH_CODE&state=STATE app.get('/oauth/callback', async (req, res) => { const { code } = req.query; try { const tokens = await auth.exchangeCodeForToken(code); // Store tokens securely res.send('Authentication successful!'); } catch (error) { res.status(400).send('Authentication failed'); } }); -
Use Access Token:
// Token is valid for 1 hour const client = new AnafClient({ vatNumber: 'RO12345678' }); const result = await client.uploadDocument(tokens.access_token, xmlContent); -
Refresh Tokens:
// Refresh before expiration const newTokens = await auth.refreshAccessToken(tokens.refresh_token);
Automated Testing
The SDK includes comprehensive Jest tests with an integrated OAuth flow:
# Run all tests
pnpm test
# Run OAuth authentication tests with callback server
pnpm test:auth
# Run tests with coverage
pnpm test:coverage
Manual OAuth Testing
The test suite includes a helpful OAuth testing flow:
-
Start Test:
pnpm test:auth -
Callback Server: Automatically starts on
http://localhost:4040 -
Get OAuth URL: Test displays authorization URL in console
-
Complete OAuth:
- Copy URL to browser
- Insert USB token when prompted
- Enter PIN and select certificate
- Authorize application
- Browser redirects to
localhost:4040/callback
-
Automatic Token Handling: Test captures code and exchanges for tokens
Testing Environment
- Test Environment: All tests use ANAF test environment
- OAuth Endpoints:
logincert.anaf.ro - API Endpoints:
api.anaf.ro/test - Callback URL:
http://localhost:4040/callback(for tests)
Token Management
- Tokens are automatically saved to
token.secretduring tests - Access tokens expire in 1 hour
- Refresh tokens have longer validity
- Tests automatically refresh expired tokens
- Invalid tokens are cleaned up automatically
Troubleshooting
USB Token Issues
❌ Certificate selection failed
Solutions:
- Ensure USB token is properly inserted
- Install manufacturer drivers
- Try different browsers (Chrome recommended)
- Check certificate validity in browser settings
OAuth Callback Issues
❌ Redirect URI mismatch
Solutions:
- Verify callback URL matches registered URL exactly
- Include protocol (https://) and path
- For ngrok: use HTTPS URL, not HTTP
- Check for trailing slashes
Network Issues
❌ Connection refused or timeout
Solutions:
- Check internet connection
- Verify firewall settings
- For ngrok: ensure tunnel is active
- Try different ngrok region:
ngrok http 3000 --region eu
Token Expiration
❌ Access token expired
Solutions:
- Use refresh token to get new access token
- Implement automatic token refresh in your app
- Store token expiration time and refresh proactively
API Coverage
The SDK implements all endpoints from the ANAF e-Factura OpenAPI specification:
Authentication
- ✅ OAuth 2.0 authorization flow
- ✅ Token exchange and refresh
Document Operations
- ✅ Upload documents (
/upload,/uploadb2c) - ✅ Check upload status (
/stareMesaj) - ✅ Download processed documents (
/descarcare)
Message Management
- ✅ List messages with pagination (
/listaMesajePaginatieFactura) - ✅ List recent messages (
/listaMesajeFactura)
Validation & Conversion
- ✅ XML validation (
/validare/{standard}) - ✅ Digital signature validation (
/api/validate/signature) - ✅ XML to PDF conversion (
/transformare/{standard}) - ✅ XML to PDF without validation (
/transformare/{standard}/DA)
UBL Generation
- ✅ UBL 2.1 compliant XML generation
- ✅ Romanian CIUS-RO specification support
Company Data Lookup
- ✅ Fetch company data by VAT code
- ✅ Validate VAT code format
- ✅ Batch fetch multiple companies
- ✅ Cache management
Environment Configuration
The SDK supports both test and production environments:
// Test environment (recommended for development)
const client = new AnafClient({
vatNumber: 'RO12345678',
testMode: true,
});
// Production environment
const client = new AnafClient({
vatNumber: 'RO12345678',
testMode: false,
});
Error Handling
The SDK provides specific error types for different scenarios:
import { AnafAuthenticationError, AnafValidationError, AnafApiError } from '@florinszilagyi/anaf-ts-sdk';
try {
await client.uploadDocument(token, xml);
} catch (error) {
if (error instanceof AnafAuthenticationError) {
// Handle authentication issues - refresh token or re-authenticate
console.log('Authentication failed:', error.message);
} else if (error instanceof AnafValidationError) {
// Handle validation errors - fix XML or parameters
console.log('Validation error:', error.message);
} else if (error instanceof AnafApiError) {
// Handle API errors - check status, retry, or contact support
console.log('API error:', error.message);
}
}
TypeScript Support
The SDK is written in TypeScript and provides comprehensive type definitions:
import type { InvoiceInput, UploadStatus, ListMessagesResponse, ValidationResult, OAuthTokens } from '@florinszilagyi/anaf-ts-sdk';
Security Best Practices
- Never commit tokens: Add
token.secretand.envto.gitignore - Use HTTPS: Always use HTTPS for OAuth callbacks in production
- Validate certificates: Ensure USB token certificates are valid and not expired
- Secure token storage: Store tokens securely (encrypted, database, secure storage)
- Implement refresh: Automatically refresh tokens before expiration
- Test environment: Use test mode for development and staging
Production Deployment
When deploying to production:
-
Register Production OAuth App:
- Use your production domain for callback URL
- Get separate client credentials for production
-
Environment Configuration:
const client = new AnafClient({ vatNumber: 'RO12345678', testMode: false, // Production mode }); -
Secure Callback Handling:
- Use HTTPS for all OAuth callbacks
- Validate state parameter
- Implement CSRF protection
- Log authentication events
-
Token Management:
- Store tokens securely (encrypted database)
- Implement automatic refresh
- Handle refresh token expiration gracefully
- Monitor token usage and expiration
License
MIT License - see LICENSE file for details.
Contributing
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests
- Run tests and linting
- Submit a pull request
Support
For questions about:
- ANAF e-Factura API: Check ANAF official documentation
- This SDK: Open an issue on GitHub
- OAuth Setup: Consult ANAF SPV documentation: ANAF official documentation
- USB Token Issues: Contact your certificate provider
Perfect for: SaaS applications, accounting software, ERP integrations, invoicing systems
Requirements: USB security token, ANAF OAuth registration, Node.js 16+