Download the PHP package bee-coded/laravel-efactura-sdk without Composer
On this page you can find all versions of the php package bee-coded/laravel-efactura-sdk. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Informations about the package laravel-efactura-sdk
Laravel e-Factura SDK
A Laravel package for integrating with Romania's ANAF e-Factura (electronic invoicing) system.
Features
- OAuth 2.0 Authentication - Complete OAuth flow with JWT tokens and automatic token refresh
- Document Operations - Upload, download, and check status of invoices
- UBL 2.1 XML Generation - Generate CIUS-RO compliant invoice XML
- Company Lookup - Query ANAF for company details (VAT status, addresses, etc.)
- Validation - Validate XML against ANAF schemas before upload
- PDF Conversion - Convert XML invoices to PDF format
- Rate Limiting - Built-in protection against exceeding ANAF API quotas
Requirements
- PHP 8.4+
- Laravel 11.0+
- Valid ANAF OAuth credentials
Installation
Publish the configuration file:
Migrating from v2 to v3
v3.0.0 is a major release with breaking changes. Some fail loudly; several are silent and change the document you file with ANAF. Work through the steps in order.
Using an AI assistant? The MCP server exposes this as a full topic with every exception message and code change:
get-sdk-docswithtopic: "migration-v2-v3". See AI Assistant Integration (MCP).
1. Facade alias rename (loud)
The auto-discovered alias EFacturaSdk → BeeCoded\EFacturaSdk\Facades\EFactura pointed at a
class that never existed, so it was always broken. It is now EFacturaSdkAuth →
BeeCoded\EFacturaSdk\Facades\EFacturaSdkAuth. Delete any hand-written alias for the old name and
import the facade directly.
2. PartyData::$isVatPayer is now required — and moved (loud, or silently wrong)
It lost its false default and moved from 5th to 4th constructor position, ahead of
$registrationNumber. A default let a caller who simply forgot the flag file a VAT-registered
company as not subject to VAT — a document ANAF accepts, with no error to notice.
Every shape of the old call now fails loudly. Omitting the flag raises ArgumentCountError (direct
construction), CannotCreateData (::from()), or The is vat payer field is required.
(::validate()).
A positional v2 call passes $registrationNumber into $isVatPayer, and that was the dangerous
case. In a file without declare(strict_types=1) — the Laravel app default — the ONRC string
coerced to isVatPayer = true, silently flipping a non-VAT-payer supplier to a VAT payer: every
line moved from VAT category O to Z, and the party gained a BT-31 seller VAT id it does not hold.
The document stayed internally consistent, so ANAF accepted and filed it. v2 defaulted this flag
to false, so the population most exposed is exactly the one that relied on that default: Romanian
non-VAT-payers. $isVatPayer is therefore typed bool|string and rejects a string:
Only true, false, 1, 0, '1' and '0' are accepted — exactly what Laravel's boolean rule
accepts, so ::from() and ::validateAndCreate() payloads are unaffected. Grep for
new PartyData( and convert every call to named arguments.
3. Non-RON invoices must declare taxAmountRon (loud)
InvoiceData gained an optional trailing ?float $taxAmountRon (BT-111) that is required when
currency !== 'RON' and rejected when it is — otherwise ValidationException.
v2 emitted the document-currency amount unchanged under currencyID="RON": a EUR invoice with
190.00 EUR of VAT filed 190.00 RON instead of ~945. ANAF cannot verify a conversion, so it was
accepted and filed. Pass the converted amount (not a rate — the rate is never transmitted):
RON-only apps need no change.
4. A non-VAT-payer supplier may not charge VAT (loud)
Every line must be taxPercent: 0, taxAmount: 0.0, else
ValidationException: Line N: A supplier that is not registered for VAT cannot charge VAT (BR-O-09).
5. XML output changes (silent — regenerate and diff)
| Change | v2 | v3 |
|---|---|---|
Line cbc:Percent under category O |
emitted → ANAF rejected (BR-O-05) | omitted (document-level TaxSubtotal keeps it) |
Customer PartyTaxScheme, non-VAT supplier |
emitted → rejected (BR-O-02) | suppressed |
quantity / PriceAmount decimals |
always 2 — 1.375→1.38, 0.0075→0.01 |
2–6 — filed exactly |
Credit-note dueDate |
dropped entirely | filed as PaymentMeans/PaymentDueDate |
PaymentMeans emitted for |
IBAN only | due date or IBAN |
| Greek/NI VAT ids | EL123456789 → GREL123456789 |
recognised, left alone |
If you worked around the 2-decimal truncation or the double-prefixing, remove the workaround — it will now double-apply.
6. Total helpers change at sub-cent boundaries (silent)
getTotalExcludingVat(), getTotalVat() and getTotalIncludingVat() now reproduce the filed XML
exactly (per-line rounding, and per-tax-group rounding respectively). Only sub-cent inputs move:
two lines of 0.5 × 0.01 file as 0.02, where v2's helper returned 0.01. Re-reconcile any
invoice with sub-cent line amounts.
The credit-note sign convention is unchanged: the helpers report the positive sense the lines are supplied in, while the filed XML states the negation. Do not "fix" this by negating.
7. Runtime behaviour (silent — bites in production)
- Uploads no longer auto-retry on 5xx, read timeouts, or unclassifiable transport errors.
ANAF mints a fresh
index_incarcareper accepted POST, so a blind retry files the invoice twice. A failed upload is not proof the invoice was not filed — remove blind retries and reconcile viagetMessages()before re-sending. Reads still retry; only pre-send errors (DNS/connect/TLS) retry an upload. - Each retry consumes global rate-limit quota, so a retrying read can throw
RateLimitExceededExceptionwhere v2 raisedApiException. It does not extendApiException— catch it first. downloadDocument()throwsApiExceptionon non-ZIP bodies. ANAF reports some errors as200+ JSON; v2 handed that back as a "successful" download that wrote JSON into a.zip.getCurrentDateForAnaf()/getDaysAgo()now resolve inEurope/Bucharest(new constantDateHelper::ANAF_TIMEZONE). On a UTC server between midnight and 02:00/03:00 Bucharest, v2 reported yesterday.toTimestamp()/getDayRange()still honour your timezone — you supply the date there.AnafDetailsClientnow honourshttp.retry_times/http.retry_delay(it ignored them before). If you tuned those keys, lookups now retry differently.
8. Additive — adopt if relevant
EFacturaClient::__construct()/::fromTokens()gained an optional?Closure $tokenReloader. ANAF rotates refresh tokens; in multi-worker deployments the worker that loses the refresh race otherwise spends a dead token. Recommended if you run more than one worker.RateLimiter::getRemainingQuota('company_lookup')— new single-bucket arm, takes no identifier.validateXml()/verifySignature()now populate$result->errorsfrom ANAF'sMessages[].messageand$result->infofromtrace_id. Both previously came backnull.BaseApiClient::call()/callRaw()gainedbool $idempotent = truebefore$tryCount— only relevant if you subclass, but a positional$tryCountnow lands in$idempotent.
Not a v3 change: nesbot/carbon was already in require; taxAmount became required on
InvoiceLineData back in v2.0; CompanyData::$isVatPayer (the lookup DTO) still defaults to
false.
Configuration
Add the following to your .env file:
Configuration Options
The keys you are most likely to change (excerpt — see below for the rest):
Publish the file to see every key with its comments:
Logging Channel (Recommended)
Add a dedicated logging channel in config/logging.php:
Rate Limiting Configuration
The SDK includes built-in rate limiting to prevent exceeding ANAF API quotas. Most defaults are set to 50% of ANAF's actual limits for safety. The one exception is company_lookup_per_second, which defaults to 1 — 100% of ANAF's 1 request/second cap, since that limit admits no lower positive value.
ANAF Official Rate Limits:
| Endpoint | ANAF Limit | SDK Default | Scope |
|---|---|---|---|
| Global (all methods) | 1,000/minute | 500/minute | All API calls |
/upload (RASP) |
1,000/day | 500/day | Per CUI |
/stare (status) |
100/day | 50/day | Per message ID |
/lista (simple) |
1,500/day | 750/day | Per CUI |
/lista (paginated) |
100,000/day | 50,000/day | Per CUI |
/descarcare (download) |
10/day | 5/day | Per message ID |
PlatitorTvaRest (company lookup) |
1/second | 1/second | Per request (global, not per CUI) |
Usage
OAuth Authentication Flow
The SDK provides a stateless OAuth implementation. You are responsible for storing tokens in your database.
Step 1: Redirect User to ANAF Authorization
Step 2: Handle OAuth Callback
Manual Token Refresh
API Operations
Creating the Client
Upload Invoice
Check Processing Status
Download Document
List Messages
Paginated Messages
Validate XML
Convert to PDF
Verify Signature
Automatic Token Refresh
The SDK automatically refreshes tokens when they're about to expire (120-second buffer before expiration).
Important: ANAF uses rotating refresh tokens. When a token is refreshed, both the access token AND refresh token are replaced. The old refresh token becomes invalid.
Recommended Pattern:
Rate Limiting
The SDK automatically enforces rate limits before each API call. When a limit is exceeded, a RateLimitExceededException is thrown.
Checking Remaining Quota
Before making API calls, you can check remaining quota:
Disabling Rate Limiting
For testing or special cases, you can disable rate limiting:
Or check status in code:
Generating Invoice XML
Using the UBL Builder
Creating a Credit Note
Credit Note Quantity Handling (Breaking Change in v1.1)
The SDK automatically negates quantities for credit notes. ANAF treats the <CreditNote> document type as inherently negative, so line quantities must be positive in the XML. The SDK handles this sign-flip internally.
How it works: pass quantities with their business meaning, and the SDK converts them for ANAF:
| You pass | SDK sends to ANAF | Meaning |
|---|---|---|
quantity: -2 |
+2 |
Crediting 2 returned items |
quantity: 1 |
-1 |
Debiting back a discount line |
Example — credit note with a discount reversal:
Upgrading from v1.0: If your code was passing positive quantities for credit note lines and relying on them going to ANAF as-is, you must now pass negative quantities instead (the SDK will negate them to positive). If you were already passing negative quantities (as documented), no changes are needed — the SDK now correctly converts them for ANAF.
Invoice Calculations
Why taxAmount is Required (Breaking Change in v2.0)
In v1.x, the SDK calculated VAT amounts internally by grouping lines by tax rate and multiplying sum_of_base_amounts × tax_rate. This caused rounding discrepancies when your application used tax-included pricing.
The problem:
When a line item has a tax-included price (e.g., 100.00 RON including 19% VAT), your application extracts the base price by subtraction:
But when the SDK grouped multiple such lines and recalculated VAT from the grouped base:
Your application computed 15.97 + 15.97 = 31.94. The SDK computed 31.93. This 0.01 RON difference meant the XML total sent to ANAF didn't match your local invoice total.
The fix:
Starting in v2.0, taxAmount is a required parameter on InvoiceLineData. You pass the VAT amount you already computed for each line, and the SDK uses it directly instead of recalculating. This guarantees the XML total matches your application's total exactly.
How to compute taxAmount:
| Pricing model | Formula | Example |
|---|---|---|
| Tax-exclusive (net price) | round(quantity × unitPrice × taxPercent / 100, 2) |
qty=2, price=100, 19% → 38.00 |
| Tax-inclusive (gross price) | grossTotal - round(grossTotal / (1 + taxPercent / 100), 2) |
gross=200, 19% → 200 - 168.07 = 31.93 |
The key rule: whatever VAT amount your application stores for the line item, pass that exact value as taxAmount. The SDK will use it as-is.
taxAmount sign convention:
The taxAmount sign must follow the quantity:
- Positive quantity → positive
taxAmount - Negative quantity (credit note lines) → negative
taxAmount
The SDK's credit note sign-flip (negating quantities for ANAF) also applies to taxAmount internally — you don't need to handle this yourself.
Upgrading from v1.x: Add
taxAmountto everynew InvoiceLineData(...)call. If you were using net pricing (tax-exclusiveunitPrice), compute it asround(round(quantity * unitPrice, 2) * taxPercent / 100, 2). If you were using tax-included pricing, pass the VAT amount you already extracted from the gross total.
Address Sanitization
Romanian addresses are automatically sanitized to ISO 3166-2:RO format:
Company Lookup
Query ANAF for company information (no authentication required):
Note (v2.2.0): a lookup whose CUIs are all not-found now returns
success === truewith the CUIs in$result->notFound(previously a single not-found CUI returned a failure result). Branch onhasNotFound()/hasCompanies(), not onsuccess, to distinguish outcomes. ANAF returns this case as HTTP 404 with a{found, notFound}body — the SDK treats that documented "none found" response as a not-found result, not an error.Detecting radiated (struck-off) companies: ANAF reports trade-registry strike-off in
date_generale.stare_inregistrare("RADIERE din data ..."), which the SDK maps toregistrationStatus, flipsisDeregisteredtotrue, and makesisActive()returnfalse. This is distinct from fiscal inactivity (isInactive) and from leaving the TVA-la-încasare registry (rtvaiDetails->actType === 'Radiere').Rate limit: ANAF limits the company-lookup endpoint to 1 request/second. The SDK enforces this and throws
BeeCoded\EFacturaSdk\Exceptions\RateLimitExceededException(HTTP 429, with->retryAfterSeconds) when exceeded. This is independent of the per-request payload cap of 100 CUIs. Disable viaefactura-sdk.rate_limits.enabled = false.
Validators
VAT Number Validation
CNP Validation
Date Helpers
Immutable Dates (CarbonImmutable)
Since v2.3.0: the date fields you pass data into accept any
Carbon\CarbonInterfaceimplementation — see the list below.
Apps that call Date::use(CarbonImmutable::class) get a CarbonImmutable from every Eloquent
datetime cast. CarbonImmutable is not a subclass of Carbon — both implement
CarbonInterface — so before v2.3.0 passing a cast in either threw a TypeError (on the
Carbon-only params) or, where the field also accepted a string and the caller had no
declare(strict_types=1), was silently coerced to a timezone-less string via __toString().
You can now pass model casts straight in:
Where it applies. These are the entry points that take a date from you:
| Entry point | Date parameters |
|---|---|
OAuthTokensData::__construct |
$expiresAt |
EFacturaClient::__construct |
$expiresAt |
InvoiceData::__construct |
$issueDate, $dueDate |
PaginatedMessagesParamsData::fromDateRange |
$startDate, $endDate |
DateHelper::formatForAnaf, toTimestamp, getDayRange, daysBetween |
date arguments |
The company-lookup DTOs (CompanyData, VatRegistrationData, SplitVatData,
InactiveStatusData, VatPeriodData) are not included: they are built by the SDK from ANAF
responses and never receive a date from you, so their date parameters remain ?Carbon.
What you get back is unchanged. An immutable date is converted to a mutable Carbon on the
way in (timezone and microseconds preserved); a date that is already a mutable Carbon is stored
as the same instance, and a string is left as a string. Reading never gives you a
CarbonImmutable: $tokens->expiresAt is ?Carbon, InvoiceData::$issueDate is
Carbon|string, and getIssueDateAsCarbon() returns a concrete Carbon.
Enums
StandardType
DocumentStandardType
MessageFilter
InvoiceTypeCode
Valid codes per ANAF BR-RO-020 schematron rule:
Note: The SDK automatically generates the correct UBL document type. Code 381 generates a <CreditNote> document with <CreditNoteTypeCode> and <CreditNoteLine> elements, while all other codes generate an <Invoice> document.
Exception Handling
Testing
When testing your application, you can mock the SDK services:
AI Assistant Integration (MCP)
This package includes an MCP server that helps AI coding assistants understand the SDK's DTOs, API methods, and conventions.
Setup: Add to your AI tool's MCP configuration:
Requires Node.js 18+.
The MCP server provides these tools:
| Tool | Description |
|---|---|
get-sdk-docs |
Documentation for topics: overview, invoice-flow, credit-notes, tax-calculation, oauth-flow, error-handling, address-sanitization, rate-limiting, company-lookup |
get-dto-structure |
Complete structure of any DTO (InvoiceData, InvoiceLineData, PartyData, etc.) |
get-enum-values |
All values for any enum (InvoiceTypeCode, MessageFilter, etc.) |
get-config-reference |
Full configuration schema with env vars and defaults |
get-api-reference |
API documentation for services (EFacturaClient, AnafAuthenticator, etc.) |
License
Licensed under the Apache License, Version 2.0. See LICENSE for details.
All versions of laravel-efactura-sdk with dependencies
illuminate/contracts Version ^11.0|^12.0|^13.0
illuminate/http Version ^11.0|^12.0|^13.0
illuminate/support Version ^11.0|^12.0|^13.0
nesbot/carbon Version ^2.72.2|^3.0
sabre/xml Version ^4.0
spatie/laravel-data Version ^4.0