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.

A RegistrationRecord (registro de alta) is the primary billing record type in VERI*FACTU. It is used to report a newly issued invoice to the AEAT, or to resubmit and correct a previously reported one. Every registration record must contain the invoice header, an optional list of recipients, a full tax breakdown, grand totals, and a chained hash that links it to the previous record in your billing sequence.
1

Create the InvoiceIdentifier

Every record begins with an InvoiceIdentifier that uniquely identifies the invoice being reported. The issuer ID must be exactly 9 characters (the issuer’s NIF), the invoice number can be up to 60 characters, and the issue date must be a DateTimeImmutable.
use DateTimeImmutable;
use josemmo\Verifactu\Models\Records\InvoiceIdentifier;

$invoiceId = new InvoiceIdentifier(
    'A00000000',                         // issuerId — NIF, exactly 9 chars
    'FACT-2026-001',                     // invoiceNumber — max 60 chars
    new DateTimeImmutable('2026-03-15'), // issueDate
);
You can also use the constructor-less form and assign properties individually:
$invoiceId = new InvoiceIdentifier();
$invoiceId->issuerId      = 'A00000000';
$invoiceId->invoiceNumber = 'FACT-2026-001';
$invoiceId->issueDate     = new DateTimeImmutable('2026-03-15');
2

Create the RegistrationRecord and set the invoice header

Instantiate a RegistrationRecord and assign the core header fields: the invoice identifier from the previous step, the issuer’s legal name, the invoice type, and a human-readable description of the operation.
use josemmo\Verifactu\Models\Records\InvoiceType;
use josemmo\Verifactu\Models\Records\RegistrationRecord;

$record = new RegistrationRecord();
$record->invoiceId   = $invoiceId;
$record->issuerName  = 'Acme Servicios, S.A.'; // max 120 chars
$record->invoiceType = InvoiceType::Factura;    // see InvoiceType enum below
$record->description = 'Servicios de consultoría — marzo 2026'; // max 500 chars
InvoiceType enum values:
CaseValueDescription
FacturaF1Standard invoice (Art. 6, 7.2 and 7.3 of R.D. 1619/2012)
SimplificadaF2Simplified invoice or invoice without recipient identification
SustitutivaF3Invoice replacing previously declared simplified invoices
R1R1Corrective invoice (Art. 80.1, 80.2 and right-based error)
R2R2Corrective invoice (Art. 80.3)
R3R3Corrective invoice (Art. 80.4)
R4R4Corrective invoice (other cases)
R5R5Corrective simplified invoice
3

Add recipients

All invoice types except Simplificada and R5 require at least one recipient. Use FiscalIdentifier for domestic entities (name + NIF) and ForeignFiscalIdentifier for foreign entities (name + ISO-3166-1 alpha-2 country code + ID type + ID value).
use josemmo\Verifactu\Models\Records\FiscalIdentifier;
use josemmo\Verifactu\Models\Records\ForeignFiscalIdentifier;
use josemmo\Verifactu\Models\Records\ForeignIdType;

// Domestic recipient (Spanish NIF)
$record->recipients[] = new FiscalIdentifier(
    'Cliente Español, S.L.', // name — max 120 chars
    'B12345678',             // NIF — exactly 9 chars
);

// Foreign recipient (non-Spanish entity)
$record->recipients[] = new ForeignFiscalIdentifier(
    'Foreign Client Ltd.',   // name
    'GB',                    // country — ISO 3166-1 alpha-2
    ForeignIdType::VAT,      // ID type
    'GB123456789',           // ID value — must start with country code for VAT type
);
InvoiceType::Simplificada (F2) and InvoiceType::R5 cannot have any recipients. Attempting to add recipients to these invoice types will cause validate() to fail. Up to 1000 recipients are allowed per record.
4

Add the tax breakdown

Use BreakdownDetails to describe each distinct tax line. You do not list individual invoice lines — only the aggregated base amount and tax amount for each unique combination of tax type, regime, and operation type.
use josemmo\Verifactu\Models\Records\BreakdownDetails;
use josemmo\Verifactu\Models\Records\OperationType;
use josemmo\Verifactu\Models\Records\RegimeType;
use josemmo\Verifactu\Models\Records\TaxType;

$line = new BreakdownDetails();
$line->taxType       = TaxType::IVA;               // tax type
$line->regimeType    = RegimeType::C01;             // general regime
$line->operationType = OperationType::Subject;      // taxable, no passive subject
$line->baseAmount    = '1000.00';                   // taxable base
$line->taxRate       = '21.00';                     // VAT rate (%)
$line->taxAmount     = '210.00';                    // VAT amount

$record->breakdown[] = $line;
TaxType enum values:
CaseValueDescription
IVA01Impuesto sobre el Valor Añadido
IPSI02IPSI (Ceuta and Melilla)
IGIC03IGIC (Canary Islands)
Other05Other taxes
OperationType — when to include taxRate and taxAmount:
CategoryCasestaxRate / taxAmount required?
SubjectSubject (S1), PassiveSubject (S2)✅ Yes
Non-subjectNonSubject (N1), NonSubjectByLocation (N2)❌ No
ExemptExemptByArticle20ExemptByOther (E1–E6)❌ No
A single RegistrationRecord can contain a minimum of 1 and a maximum of 12 breakdown entries.
5

Set the invoice totals

Set the aggregated tax total (sum of all taxAmount values across breakdown lines) and the overall invoice total (base + tax).
$record->totalTaxAmount = '210.00'; // sum of all taxAmount values
$record->totalAmount    = '1210.00'; // base + tax
Always pass monetary amounts as strings with exactly two decimal places (e.g. '210.00', not 210 or 210.0). All amount fields use the regex pattern /^-?\d{1,12}\.\d{2}$/ and validation will fail for any other format.
6

Chain the record to the previous one

Every record must be linked to its predecessor in the billing chain. For the very first record ever issued by your SIF, set both fields to null. For all subsequent records, provide the InvoiceIdentifier and SHA-256 hash of the previous record.
// First record ever issued by this SIF
$record->previousInvoiceId = null;
$record->previousHash      = null;

// All subsequent records
$record->previousInvoiceId = new InvoiceIdentifier(
    'A00000000',
    'FACT-2026-000',
    new DateTimeImmutable('2026-03-14'),
);
$record->previousHash = 'F7B94CFD8924EDFF273501B01EE5153E4CE8F259766F88CF6ACB8935802A2B97';
7

Calculate the hash and validate

Finally, record the timestamp at which the hash was computed, call calculateHash() to generate the SHA-256 fingerprint, then validate the entire record.
use DateTimeImmutable;
use josemmo\Verifactu\Exceptions\InvalidModelException;

$record->hashedAt = new DateTimeImmutable(); // current date and time
$record->hash     = $record->calculateHash();

try {
    $record->validate();
    echo "Record is valid.\n";
} catch (InvalidModelException $e) {
    echo "Validation failed: $e\n";
}

Complete working example

The snippet below puts all steps together into a single, runnable PHP file.
<?php
use DateTimeImmutable;
use josemmo\Verifactu\Exceptions\InvalidModelException;
use josemmo\Verifactu\Models\Records\BreakdownDetails;
use josemmo\Verifactu\Models\Records\FiscalIdentifier;
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;

// 1. Invoice identifier
$invoiceId                = new InvoiceIdentifier();
$invoiceId->issuerId      = 'A00000000';
$invoiceId->invoiceNumber = 'FACT-2026-001';
$invoiceId->issueDate     = new DateTimeImmutable('2026-03-15');

// 2. Record header
$record              = new RegistrationRecord();
$record->invoiceId   = $invoiceId;
$record->issuerName  = 'Acme Servicios, S.A.';
$record->invoiceType = InvoiceType::Factura;
$record->description = 'Servicios de consultoría — marzo 2026';

// 3. Recipient
$record->recipients[] = new FiscalIdentifier('Cliente Español, S.L.', 'B12345678');

// 4. Tax breakdown
$line                = new BreakdownDetails();
$line->taxType       = TaxType::IVA;
$line->regimeType    = RegimeType::C01;
$line->operationType = OperationType::Subject;
$line->baseAmount    = '1000.00';
$line->taxRate       = '21.00';
$line->taxAmount     = '210.00';
$record->breakdown[] = $line;

// 5. Totals
$record->totalTaxAmount = '210.00';
$record->totalAmount    = '1210.00';

// 6. Chain (first record — no predecessor)
$record->previousInvoiceId = null;
$record->previousHash      = null;

// 7. Hash
$record->hashedAt = new DateTimeImmutable();
$record->hash     = $record->calculateHash();

// 8. Validate
try {
    $record->validate();
    echo "Record is valid.\n";
} catch (InvalidModelException $e) {
    echo "Validation failed: $e\n";
}

Correction records

When a previously submitted registration record contains errors and was rejected by the AEAT, you can resubmit it as a corrected version instead of creating a brand-new record. Set $record->isCorrection = true to mark the resubmission as a correction (subsanación). Combine it with $record->isPriorRejection to describe the rejection context:
isPriorRejection valueMeaning
false (default)Not resubmitting after a rejection
trueResubmitting after the record was rejected in the immediately preceding submission to AEAT
nullResubmitting after a rejection, but the rejected record was never sent to AEAT (local-only rejection)
$record->isCorrection    = true;
$record->isPriorRejection = true; // was rejected in the previous AEAT submission
isPriorRejection can only be set on records that also have isCorrection = true. Setting it without the correction flag will cause validate() to throw an InvalidModelException.

Build docs developers (and LLMs) love