Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Eseperio/verifactu-php/llms.txt

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

What Is Hash Chaining?

Every invoice record submitted to AEAT must include a SHA-256 hash called the huella (fingerprint). The huella of each invoice is computed from the invoice’s own fields plus the huella of the immediately preceding invoice. This creates a tamper-evident chain: changing any record in the sequence invalidates the hash of every record that follows it, making retroactive alterations detectable by AEAT.
Invoice 1  →  huella₁ = SHA-256(fields₁ + "")
Invoice 2  →  huella₂ = SHA-256(fields₂ + huella₁)
Invoice 3  →  huella₃ = SHA-256(fields₃ + huella₂)
   ...
The first invoice in a chain has no predecessor, so its Huella field in the concatenation string is an empty string.
The library calculates the huella automatically inside Verifactu::registerInvoice() and Verifactu::cancelInvoice(). You do not need to call HashGeneratorService::generate() yourself for normal submissions — just set up the Chaining block correctly and the library takes care of the rest.

The Chaining Model

Every InvoiceSubmission and InvoiceCancellation must include a Chaining object set via setChaining(). The Chaining model has two mutually exclusive states:
StateWhen to use
firstRecord = 'S'This is the first invoice your system ever submits to AEAT (or the first after a voluntary VERI*FACTU stop).
setPreviousInvoice(...)This invoice follows another one — provide the previous invoice’s identity and hash.

First Invoice in the Chain

use eseperio\verifactu\models\Chaining;

$chaining = new Chaining();
$chaining->setAsFirstRecord(); // sets firstRecord = 'S', clears previousInvoice

$invoice->setChaining($chaining);
You can also use the shorthand directly on the record:
$invoice->setAsFirstRecord(); // creates a Chaining internally and marks it as first

Subsequent Invoices

For every invoice after the first, supply the previous invoice’s issuer NIF, series number, issue date, and hash:
use eseperio\verifactu\models\Chaining;
use eseperio\verifactu\models\PreviousInvoiceChaining;

$previousChaining = new PreviousInvoiceChaining();
$previousChaining->issuerNif    = 'B12345678';
$previousChaining->seriesNumber = 'FA2024/001';
$previousChaining->issueDate    = '01-07-2024'; // DD-MM-YYYY
$previousChaining->hash         = 'A3F7...'; // uppercase hex SHA-256 of the previous record

$chaining = new Chaining();
$chaining->setPreviousInvoice($previousChaining);

$invoice->setChaining($chaining);
setPreviousInvoice() also accepts a plain array for convenience:
$chaining->setPreviousInvoice([
    'issuerNif'    => 'B12345678',
    'seriesNumber' => 'FA2024/001',
    'issueDate'    => '01-07-2024',
    'hash'         => 'A3F7...',
]);
Setting a previous invoice via setPreviousInvoice() automatically clears firstRecord, and calling setAsFirstRecord() automatically clears previousInvoice. The model’s validate() method enforces that exactly one of the two is set — both null or both non-null will fail validation.

The Huella Concatenation Format

Invoice Submission (RegistroAlta)

For an InvoiceSubmission, HashGeneratorService concatenates the following fields in this exact order, separated by &:
IDEmisorFactura=<issuerNif>&NumSerieFactura=<seriesNumber>&FechaExpedicionFactura=<issueDate>&TipoFactura=<invoiceType>&CuotaTotal=<taxAmount>&ImporteTotal=<totalAmount>&Huella=<previousHash>&FechaHoraHusoGenRegistro=<recordTimestamp>
Example:
IDEmisorFactura=B12345678&NumSerieFactura=FA2024/001&FechaExpedicionFactura=01-07-2024&TipoFactura=F1&CuotaTotal=21.00&ImporteTotal=121.00&Huella=A3F7C2...&FechaHoraHusoGenRegistro=2024-07-01T12:00:00+02:00
Concatenation fieldSourceNotes
IDEmisorFacturaInvoiceId::$issuerNifTrimmed string
NumSerieFacturaInvoiceId::$seriesNumberTrimmed string
FechaExpedicionFacturaInvoiceId::$issueDateFormat: DD-MM-YYYY
TipoFacturaInvoiceSubmission::$invoiceTypeEnum value (e.g. F1)
CuotaTotalInvoiceSubmission::$taxAmountNormalised decimal — see below
ImporteTotalInvoiceSubmission::$totalAmountNormalised decimal — see below
HuellaPreviousInvoiceChaining::$hash (or "" for first)Previous record’s huella
FechaHoraHusoGenRegistroInvoiceRecord::$recordTimestampISO 8601 with timezone

Invoice Cancellation (RegistroAnulacion)

For an InvoiceCancellation, the concatenation is shorter and uses different field names:
IDEmisorFacturaAnulada=<issuerNif>&NumSerieFacturaAnulada=<seriesNumber>&FechaExpedicionFacturaAnulada=<issueDate>&Huella=<previousHash>&FechaHoraHusoGenRegistro=<recordTimestamp>
Example:
IDEmisorFacturaAnulada=B12345678&NumSerieFacturaAnulada=FA2024/001&FechaExpedicionFacturaAnulada=01-07-2024&Huella=A3F7C2...&FechaHoraHusoGenRegistro=2024-07-01T13:00:00+02:00

Decimal Normalisation

Monetary values (CuotaTotal and ImporteTotal) are normalised using number_format() with exactly two decimal places and a dot as the decimal separator. No thousands separator is used.
Raw valueNormalised
2121.00
121.5121.50
1000.1231000.12
00.00
This normalisation is applied automatically by HashGeneratorService::normalizeDecimal(). Make sure the taxAmount and totalAmount fields on your InvoiceSubmission are set to numeric values (int or float) before calling registerInvoice().

The recordTimestamp Field

The recordTimestamp field (FechaHoraHusoGenRegistro) is part of the hash input. It must be a valid ISO 8601 datetime string with an explicit timezone offset — never UTC Z notation.
// Correct — explicit offset
$invoice->recordTimestamp = '2024-07-01T12:00:00+02:00';

// Also correct — UTC offset written explicitly
$invoice->recordTimestamp = '2024-07-01T10:00:00+00:00';

// Incorrect — 'Z' suffix is not accepted by AEAT
// $invoice->recordTimestamp = '2024-07-01T12:00:00Z';
In practice, use Spain’s local time with the appropriate offset (+01:00 in winter, +02:00 in summer):
$tz = new \DateTimeZone('Europe/Madrid');
$now = new \DateTimeImmutable('now', $tz);
$invoice->recordTimestamp = $now->format('Y-m-d\TH:i:sP'); // e.g. 2024-07-01T12:00:00+02:00

How the Auto-Generated Hash Flows Through the Chain

Here is the complete sequence for two consecutive invoices:
use eseperio\verifactu\Verifactu;
use eseperio\verifactu\models\InvoiceSubmission;
use eseperio\verifactu\models\InvoiceId;
use eseperio\verifactu\models\Chaining;

// --- Invoice 1: first in chain ---
$invoice1 = new InvoiceSubmission();
$id1 = new InvoiceId();
$id1->issuerNif    = 'B12345678';
$id1->seriesNumber = 'FA2024/001';
$id1->issueDate    = '01-07-2024';
$invoice1->setInvoiceId($id1);
$invoice1->issuerName      = 'Empresa Ejemplo SL';
$invoice1->invoiceType     = \eseperio\verifactu\models\enums\InvoiceType::STANDARD;
$invoice1->taxAmount       = 21.00;
$invoice1->totalAmount     = 121.00;
$invoice1->recordTimestamp = '2024-07-01T12:00:00+02:00';
// ... setBreakdown(), setSystemInfo() ...

$invoice1->setAsFirstRecord(); // Chaining: firstRecord = 'S'

$response1 = Verifactu::registerInvoice($invoice1);
// At this point, $invoice1->hash has been set by the library.
$huella1 = $invoice1->hash; // Store this — you need it for invoice 2.


// --- Invoice 2: links to invoice 1 ---
$invoice2 = new InvoiceSubmission();
$id2 = new InvoiceId();
$id2->issuerNif    = 'B12345678';
$id2->seriesNumber = 'FA2024/002';
$id2->issueDate    = '02-07-2024';
$invoice2->setInvoiceId($id2);
$invoice2->issuerName      = 'Empresa Ejemplo SL';
$invoice2->invoiceType     = \eseperio\verifactu\models\enums\InvoiceType::STANDARD;
$invoice2->taxAmount       = 42.00;
$invoice2->totalAmount     = 242.00;
$invoice2->recordTimestamp = '2024-07-02T09:00:00+02:00';
// ... setBreakdown(), setSystemInfo() ...

$chaining2 = new Chaining();
$chaining2->setPreviousInvoice([
    'issuerNif'    => 'B12345678',
    'seriesNumber' => 'FA2024/001',
    'issueDate'    => '01-07-2024',
    'hash'         => $huella1, // <-- hash of invoice 1, returned after registerInvoice()
]);
$invoice2->setChaining($chaining2);

$response2 = Verifactu::registerInvoice($invoice2);
After each call to registerInvoice(), retrieve the generated hash from $invoice->hash and persist it alongside your invoice record. You will need it to build the Chaining block for the next invoice.

Advanced: Calling HashGeneratorService Directly

In most cases you should let the library handle hashing automatically. However, there are legitimate advanced scenarios where you might need to compute the huella separately — for example:
  • Verifying the hash of a historical record stored in your database
  • Pre-computing hashes in a batch job before the final submission
  • Writing custom tests that check your field values produce the expected hash
use eseperio\verifactu\services\HashGeneratorService;

// $invoice must be a fully populated InvoiceSubmission or InvoiceCancellation
// (all fields required for hashing must be set, but $invoice->hash is not needed)
$huella = HashGeneratorService::generate($invoice);

echo $huella; // Uppercase hexadecimal SHA-256, e.g. "A3F7C2D1..."
HashGeneratorService::generate() returns the hash as an uppercase hexadecimal string (64 characters), matching the format AEAT expects in the Huella XML element and in subsequent chaining fields.

Build docs developers (and LLMs) love