Record chaining is the mechanism that gives VERI*FACTU its tamper-evident properties. Each billing record embeds a SHA-256 hash of the record that came before it, creating a linked chain where altering any historical record immediately invalidates all subsequent hashes. This design makes it computationally infeasible to retroactively modify, insert, or delete a record without the change being detectable — whether by the AEAT’s own systems or during a tax audit. Verifactu-PHP implements the full chaining protocol as defined in RD 1007/2023, including the exact field ordering and encoding required by the AEAT spec.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.
How the chain works
Every record — whether aRegistrationRecord or a CancellationRecord — carries four chaining-related fields inherited from the Record base class:
| Property | Type | Description |
|---|---|---|
previousInvoiceId | ?InvoiceIdentifier | The invoice identifier of the immediately preceding record |
previousHash | ?string | First 64 uppercase hex characters of the preceding record’s SHA-256 hash |
hash | string | This record’s own SHA-256 hash (64 uppercase hex chars) |
hashedAt | DateTimeImmutable | Timestamp (with timezone) at which the hash was computed |
- First record in the chain: set
previousInvoiceId = nullandpreviousHash = null. The XML serializer writes<sum1:PrimerRegistro>S</sum1:PrimerRegistro>instead of a back-reference. - Every subsequent record: set
previousInvoiceIdto theInvoiceIdentifierof the prior record andpreviousHashto the prior record’shashstring. - Both
previousInvoiceIdandpreviousHashmust either both be set or both benull— thevalidatePreviousInvoicecallback enforces this. CancellationRecordis stricter: it always requires bothpreviousInvoiceIdandpreviousHashto be set, even if it is the first record the SIF has ever emitted.
Hash calculation for RegistrationRecord
The hash of aRegistrationRecord is the uppercase SHA-256 hex digest of a UTF-8 string built by concatenating the following key-value pairs in exact order, separated by &:
RegistrationRecord::calculateHash():
Hash calculation for CancellationRecord
ACancellationRecord uses a shorter payload that references the invoice being cancelled rather than general invoice metadata:
CancellationRecord::calculateHash():
Using calculateHash()
Once all fields that participate in the hash payload are set, callcalculateHash() and assign the result to $record->hash. Always set hashedAt immediately before computing the hash — it is part of the payload, so changing it afterwards will invalidate the hash.
Chain integrity validation
validate() automatically verifies that the stored hash matches the record’s current state via the validateHash callback defined in the Record base class:
calculateHash() was called, the violation message will include the expected hash value, making it straightforward to diagnose which record is out of sync.
First record in a chain
When the SIF has no prior records — either because it is issuing its very first billing record ever, or because the chain has been officially reset — set both chain-back-reference properties tonull:
<sum1:PrimerRegistro>S</sum1:PrimerRegistro> in the <sum1:Encadenamiento> block instead of a <sum1:RegistroAnterior> element.
Subsequent records
For every record after the first, supply theInvoiceIdentifier of the preceding record and its hash string:
The
hashedAt timestamp is serialised using DateTimeInterface::ISO8601 format (e.g. 2025-01-15T10:30:00+01:00). PHP’s DateTimeImmutable uses your system timezone by default. Always construct hashedAt with an explicit timezone — for example new DateTimeImmutable('now', new DateTimeZone('Europe/Madrid')) — to ensure the offset in the hash payload and in the XML output is correct. A mismatch between the timezone used during calculateHash() and the one stored in hashedAt will cause the hash to be permanently wrong.