squareetlabs/laravel-verifactu
Laravel package for Veri*factu-compliant invoicing. It helps generate invoice records, QR codes, hashes and XML, and integrates Spanish anti-fraud invoicing requirements into Eloquent models and Laravel apps.
squareetlabs/laravel-verifactu is a Laravel wrapper around Spain's VeriFactu invoicing regime (RD 1007/2023) — the anti-fraud obligation that requires your billing software to produce tamper-evident, hash-chained invoice records, print a QR on every invoice, and be able to ship those records to the AEAT. The package handles the plumbing (record schema, chaining/hashing, QR payload, submission endpoints), not your business logic. You still own invoice numbering, VAT calculation, and your domain model.
Treat it as compliance infrastructure: it must not be the thing that surprises you in production. Read the README's config table and the migration files before you write a single line of integration code.
composer require squareetlabs/laravel-verifactu
php artisan vendor:publish --provider="SquareetLabs\Verifactu\VerifactuServiceProvider"
php artisan migrate
Then populate config/verifactu.php (or the published .env keys) with at minimum:
certificado exactly or AEAT rejects signatures..p12/.pfx). Store the password in config sourced from an env var, never committed.IdSistemaInformatico, configurable since v1.0.2): the code that identifies your billing system inside every record header. Prior to 1.0.2 this was hardcoded, which meant every project using the package presented the same identity to AEAT — workable for a single production deployment, useless the moment you need two test instances. If you are on 1.0.1 or earlier, upgrade before you go near the pruebas environment.config/verifactu.php — every behavioural switch lives here. Skim it fully; it's the fastest map of the package's capabilities. On 1.0.2+ confirm IdSistemaInformatico is actually present in your published copy (see Gotchas).database/migrations/ — the record table shape tells you exactly what fields you must supply. This is the real contract.VerifactuService or similar) — the public methods are your integration surface.src/Models/ and any Concerns/ or Traits/ directory — check whether a HasVerifactu-style trait is meant to be dropped onto your existing Invoice model.The smallest meaningful loop is: create an invoice → register it (record + hash + QR) → retrieve the QR string → submit to the test endpoint. Do that in a seeder or a tinker session, not in a controller. Something along these lines (adapt method names to the actual README — API naming is the one thing you should verify rather than assume):
$invoice = Invoice::create([...]); // your model, your numbering
$record = Verifactu::register($invoice); // package creates the chained record
$qr = $record->qrPayload(); // embed in PDF / HTML
$response = Verifactu::send($record); // hits AEAT test endpoint
Once that round-trips cleanly, you have a working mental model and can design the real integration.
Do not let controllers call the package directly. Introduce a thin domain service (e.g. App\Services\Fiscal\InvoiceRegistrar) that:
InvoiceFiscallyRegistered).This keeps the package swappable (there are competing VeriFactu packages) and gives you one place to add idempotency keys, logging, and retries.
DB::transaction(function () use ($invoice) {
$invoice->save();
$record = Verifactu::register($invoice);
$invoice->update(['verifactu_uuid' => $record->uuid]);
});
SubmitVerifactuRecord::dispatch($invoice)->onQueue('fiscal');
AEAT calls are slow, rate-limited, and can time out. Always dispatch them. Recommended job shape:
$tries = 5, backoff() returning [60, 300, 900, 3600, 7200].ShouldBeUnique keyed on the record UUID, so a duplicate dispatch can't double-submit.verifactu_submission_failures table and alert — never silently swallow.VeriFactu is a hash chain: each record's hash incorporates the previous one. That has three consequences for your architecture:
->lockForUpdate() on the last record, or a dedicated sequence row) around record creation. Concurrent registrations racing on the chain head will produce a broken chain that you cannot retroactively fix.$table->softDeletes() plus a boot guard that throws on delete()).The QR is not decoration; its payload has a mandated structure. Centralise it in a Blade component or a QrData value object so PDF, HTML email, and customer portal all render identical content:
<x-verifactu-qr :invoice="$invoice" />
Generate the QR at issuance time and persist the string on the record — do not regenerate it lazily from the live model later, or a later edit (e.g. changing the customer name) will silently change the QR and desynchronise it from the filed record.
If you issue invoices for more than one legal entity, config-level issuer settings are the wrong place. Check whether the package supports per-record issuer overrides or a "company profile" object; if not, instantiate the service with runtime config via a factory bound in the container per company:
$this->app->bind(Verifactu::class, fn () => Verifactu::forCompany($tenant->fiscalProfile));
This is a common gap in 20-star packages — verify it early, before you build thirty features on top. v1.0.2 partially closes this gap: IdSistemaInformatico is now configurable, so multiple installs/instances can present distinct software-system identities (which the pruebas environment requires if you run more than one sandbox). That is identity-per-deployment, not identity-per-tenant — it does not give you per-company issuer/NIF overrides. If you need true multi-entity support you are still building the factory yourself.
Http::fake() to stub AEAT responses (both accept-with-errors and reject cases) and assert your failure-handling paths.verifactu:verify-chain style Artisan command that re-walks the chain is the single highest-value thing you can build on top of this package. See below.IdSistemaInformatico (or NumeroInstalacion if the package exposes it). Two jobs sharing one identity produce confusing duplicate/collision responses that look like bugs in your code.IdSistemaInformatico moving from hardcoded to config-driven is a config-file change, not a schema change, so composer update alone will not surface it: your published config/verifactu.php from 1.0.1 has no key for it, and you'll silently keep whatever the package falls back to. After upgrading to 1.0.2:
config/verifactu.php against your published copy (php artisan vendor:publish --tag=verifactu-config --force in a scratch checkout, then git diff).--force publishing directly in your repo will clobber every environment-specific value you set..env / .env.example and to your deployment secrets checklist, not just the config file.Also read the actual PR diffs for v1.0.2 (#7 and #10) rather than trusting the release notes — "Feat/integration pr8 issue6" tells you nothing about whether a public method signature moved.
The package can generate a correctly-hashed record for a factually wrong invoice. It cannot know that your VAT was computed wrong, that the invoice date predates the registro, or that the numbering series skipped. Budget for a reconciliation layer:
// Verifies: contiguous numbering, hash chain integrity, submission status
php artisan fiscal:audit --from=2026-01-01
Ship this before go-live, not after. AEAT's plazo for correction (4 years, with 4-year retention obligations) means errors you don't detect are errors you live with.
openssl_x509_parse in a scheduled job)..p12 with a password that contains characters your .env parser will mangle — quote it.storage_path()-based paths are the safe choice.app()->environment('production') in the config when the endpoint is production.IdSistemaInformatico (and the sibling NombreSistemaInformatico / Version / NumeroInstalacion fields where present) are part of the signed record payload. Changing them mid-year means records with two different software identities in one chain — an audit question you do not want to answer. Treat the value as immutable once you have filed real invoices: set it deliberately at go-live, document it, and only change it with a migration plan. For non-production, vary it freely — that is exactly what 1.0.2 unlocks.
VeriFactu records carry a generation timestamp. A container with a drifting clock produces timestamps that can look out of order relative to the chain, or fall on the wrong side of a submission deadline. Keep NTP synced and consider using a single now() source rather than mixing Carbon::now() and DB CURRENT_TIMESTAMP (different timezones → different values → audit confusion). Store UTC in DB, present in Europe/Madrid.
The regulation allows systems to declare themselves as not VeriFactu-compliant, with a different QR annotation. This is a trap if chosen casually: it's a permanent visible label on your invoices. Only use it if you genuinely are not required to be compliant (e.g. non-Spanish scope). Verify what the package's mode flag actually does to the QR payload and the sent record.
AEAT returns a mix of correct, correct-with-errors, and incorrect states, and duplicate submissions return a different code than you might expect. Map these into an enum in your app rather than checking booleans:
enum SubmissionStatus: string {
case Accepted = 'Correcto';
case AcceptedWithErrors = 'AceptadoConErrores';
case Rejected = 'Incorrecto';
case Duplicate = 'Duplicado';
}
Treat AcceptedWithErrors as needing review, not as success.
Log::info.config('verifactu.models.record') style key. If it exists, subclass the record model early and add your own scopes/accessors there rather than wrapping it externally.Facturae dependency. If it's XML-over-HTTP, note that AEAT's schema validators are strict about element ordering — never hand-edit the XML.Laravel 10–12 compatibility is a wide net, and a package with ~23 stars likely ships breaking changes without a major-version bump discipline you'd expect from a larger vendor. v1.0.2 is a case in point: a patch bump added a config key and (per the vague second PR title) some integration changes, with release notes that don't enumerate the surface area touched. Pin to a specific minor/patch version, run the hash-fixture tests on every upgrade, and diff config/verifactu.php plus src/ on each bump — not just composer.json.
IdSistemaInformatico set, and does it match what you registered with AEAT for that environment? (New in 1.0.2 — a missing/duplicated identity is now a failure mode you can create yourself.)validate()/dry-run before sending.Duplicado responses and missing uniqueness constraints on your job.Because this package's correctness is measured in years (retention + audit windows), invest early in an append-only local mirror of every record plus its submission response, exported to cold storage on a schedule. If the package's schema ever changes under you, or you migrate off it, that mirror is what lets you prove compliance — and it's the single cheapest insurance you can buy against a small, single-maintainer dependency sitting on your critical path.
How can I help you explore Laravel packages today?