Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/josemmo/Verifactu-PHP/llms.txt

Use this file to discover all available pages before exploring further.

This guide takes you from a blank PHP project to a real VERI*FACTU registration record accepted by the AEAT pre-production endpoint. You will install the library, build a RegistrationRecord, configure your ComputerSystem descriptor, authenticate with your certificate, and inspect the response — all in five focused steps.
Use setProduction(false) throughout development and testing. The AEAT pre-production environment (prewww1.aeat.es) accepts the same certificate format and payload structure as production but does not affect your real tax records.
1

Install via Composer

Verifactu-PHP requires PHP 8.2 or higher and the libxml extension (bundled with most PHP distributions). Install the package with a single Composer command:
composer require josemmo/verifactu-php
Composer will pull in all required dependencies — Guzzle for HTTP transport, UXML for XML handling, and Symfony Validator for model validation — and generate the PSR-4 autoloader under vendor/autoload.php.
2

Build a RegistrationRecord

A RegistrationRecord represents a single invoice registration (alta). Populate its properties, compute the SHA-256 chain hash, and validate the model before sending it anywhere.
<?php

use josemmo\Verifactu\Models\Records\BreakdownDetails;
use josemmo\Verifactu\Models\Records\InvoiceIdentifier;
use josemmo\Verifactu\Models\Records\InvoiceType;
use josemmo\Verifactu\Models\Records\OperationType;
use josemmo\Verifactu\Models\Records\RegimeType;
use josemmo\Verifactu\Models\Records\RegistrationRecord;
use josemmo\Verifactu\Models\Records\TaxType;

require __DIR__ . '/vendor/autoload.php';

// --- Invoice identifier ---
$record = new RegistrationRecord();
$record->invoiceId = new InvoiceIdentifier();
$record->invoiceId->issuerId      = 'A00000000';           // NIF of the invoice issuer (9 chars)
$record->invoiceId->invoiceNumber = 'TICKET-2025-06-001';  // Series + number (max 60 chars)
$record->invoiceId->issueDate     = new DateTimeImmutable('2025-06-10');

// --- Basic invoice data ---
$record->issuerName  = 'Perico de los Palotes, S.A.';
$record->invoiceType = InvoiceType::Simplificada;          // simplified invoice (no recipient required)
$record->description = 'Factura simplificada de prueba';

// --- Tax breakdown (IVA 21 %) ---
$record->breakdown[0]               = new BreakdownDetails();
$record->breakdown[0]->taxType      = TaxType::IVA;
$record->breakdown[0]->regimeType   = RegimeType::C01;     // general tax regime
$record->breakdown[0]->operationType = OperationType::Subject; // taxable, non-exempt
$record->breakdown[0]->baseAmount   = '10.00';
$record->breakdown[0]->taxRate      = '21.00';
$record->breakdown[0]->taxAmount    = '2.10';

// --- Totals ---
$record->totalTaxAmount = '2.10';
$record->totalAmount    = '12.10';

// --- Hash chain (null = first record in the chain) ---
$record->previousInvoiceId = null;
$record->previousHash      = null;
$record->hashedAt          = new DateTimeImmutable();      // timestamp of hash generation
$record->hash              = $record->calculateHash();     // SHA-256 over canonical payload

// --- Validate (throws on constraint violations) ---
$record->validate();
calculateHash() computes the SHA-256 fingerprint over a canonical string that includes the issuer NIF, invoice number, issue date, invoice type, tax total, grand total, the previous record’s hash, and the generation timestamp. Set previousInvoiceId and previousHash to null only for the first record in a chain; for every subsequent record, supply the identifier and hash of the immediately preceding one.
3

Configure ComputerSystem

ComputerSystem describes the SIF (Sistema Informático de Facturación) that is generating the records. The AEAT embeds this information in every submission, so it must be accurate and consistent across all records you send.
<?php

use josemmo\Verifactu\Models\ComputerSystem;

$system = new ComputerSystem();
$system->vendorName              = 'Perico de los Palotes, S.A.'; // producer name (max 120 chars)
$system->vendorNif               = 'A00000000';                   // producer NIF (exactly 9 chars)
$system->name                    = 'Sistema Informático de Prueba'; // SIF name (max 30 chars)
$system->id                      = 'PA';                          // SIF code given by producer (max 2 chars)
$system->version                 = '0.0.1';                       // SIF version (max 50 chars)
$system->installationNumber      = '1234';                        // installation number (max 100 chars)
$system->onlySupportsVerifactu   = true;   // true = SIF can ONLY operate in VERI*FACTU mode
$system->supportsMultipleTaxpayers = false; // true = SIF can handle multiple taxpayers independently
$system->hasMultipleTaxpayers    = false;  // true = currently handling more than one taxpayer

$system->validate();
4

Create AeatClient and send

AeatClient constructs the SOAP envelope, attaches your mTLS certificate, targets the correct endpoint, and dispatches the request asynchronously using Guzzle promises.
Your certificate must be issued by the FNMT (Fábrica Nacional de Moneda y Timbre) or another certification authority recognised by the AEAT. Self-signed certificates will be rejected at the TLS handshake. Pass the path to a .pfx / PKCS#12 bundle (note the .p12 extension recognised by Guzzle) or an encrypted PEM file.
<?php

use josemmo\Verifactu\Models\Records\FiscalIdentifier;
use josemmo\Verifactu\Services\AeatClient;

// Taxpayer = the entity that issues the invoices
$taxpayer = new FiscalIdentifier('Perico de los Palotes, S.A.', 'A00000000');

// Build the client
$client = new AeatClient($system, $taxpayer);
$client->setCertificate(__DIR__ . '/certificado.pfx', 'contraseña'); // path + password
$client->setProduction(false); // false = pre-production endpoint

// Send one or more records (returns a Guzzle PromiseInterface)
$aeatResponse = $client->send([$record])->wait();
send() accepts an array of RegistrationRecord and/or CancellationRecord objects. Calling ->wait() blocks until the promise resolves; in an async context you can chain ->then() handlers instead.
5

Handle the response

The resolved promise returns an AeatResponse object. Check ResponseStatus::Correct to confirm the record was accepted without errors, or inspect the per-item errorDescription when the AEAT reports a problem.
<?php

use josemmo\Verifactu\Models\Responses\ResponseStatus;

if ($aeatResponse->status === ResponseStatus::Correct) {
    // CSV = Código Seguro de Verificación assigned by the AEAT
    $csv = $aeatResponse->csv;
    echo "Record accepted without errors. CSV: {$csv}\n";
} else {
    // Individual item errors contain the AEAT error code and description
    $errorDescription = $aeatResponse->items[0]->errorDescription;
    echo "Record rejected or accepted with errors: {$errorDescription}\n";
}
A ResponseStatus::Correct response includes a CSV (Código Seguro de Verificación) — the unique identifier assigned by the AEAT to the submitted batch. Store this value alongside your invoice data for audit purposes.
That’s it — you’ve just sent a VERI*FACTU registration record to the AEAT pre-production environment. From here, explore the concepts and guides below to handle cancellation records, corrective invoices, recipients, and production deployment.

Build docs developers (and LLMs) love