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.

Overview

Invoice registration (Alta) is the process of submitting a new invoice record to Spain’s AEAT via the Verifactu SOAP endpoint. When you call Verifactu::registerInvoice(), the library automatically:
  1. Validates all model properties against the AEAT XSD rules.
  2. Calculates the SHA-256 hash (Huella) using the official AEAT field ordering.
  3. Serialises the data into an XML document matching the RegistroAlta schema.
  4. Signs the XML block with XAdES Enveloped using your digital certificate.
  5. Transmits the signed payload to AEAT over SOAP.
  6. Parses the response into a typed InvoiceResponse object.
You do not need to calculate the hash yourself. Verifactu::registerInvoice() calls HashGeneratorService::generate() internally before sending the request. Do not pre-set $invoice->hash; leave it empty and let the library fill it in.
The issuerNif and issuerName you provide must exactly match your census data registered at AEAT (“Mis datos censales”). A mismatch will cause AEAT to reject the submission with the error “The value of the NIF field in the ObligadoEmision block is not identified”. Verify your data on the AEAT website before going live.

Building an InvoiceSubmission Step by Step

1
Create the Invoice ID
2
The InvoiceId object uniquely identifies an invoice within the issuer’s records. It is required on every submission.
3
use eseperio\verifactu\models\InvoiceId;
use eseperio\verifactu\models\InvoiceSubmission;

$invoice = new InvoiceSubmission();

$invoiceId = new InvoiceId();
$invoiceId->issuerNif  = 'B12345678';      // Spanish NIF of the issuing entity
$invoiceId->seriesNumber = 'FA2024/001';   // Series + invoice number (up to 60 chars)
$invoiceId->issueDate  = '2024-07-01';     // Issue date in YYYY-MM-DD format

$invoice->setInvoiceId($invoiceId);
4
Set Required Invoice Fields
5
These fields are mandatory for every invoice submission regardless of type.
6
use eseperio\verifactu\models\enums\InvoiceType;
use eseperio\verifactu\models\enums\YesNoType;

$invoice->issuerName          = 'Empresa Ejemplo SL';   // Must match AEAT census data
$invoice->invoiceType         = InvoiceType::STANDARD;   // F1 – standard invoice
$invoice->operationDescription = 'Venta de productos';
$invoice->taxAmount           = 21.00;   // Total tax (CuotaTotal)
$invoice->totalAmount         = 121.00;  // Grand total (ImporteTotal)

// These two are optional but recommended for clarity
$invoice->simplifiedInvoice        = YesNoType::NO;
$invoice->invoiceWithoutRecipient  = YesNoType::NO;
7
InvoiceType valueEnum caseDescriptionF1STANDARDStandard invoice (Art. 6, 7.2, 7.3 RD 1619/2012)F2SIMPLIFIEDSimplified invoice without recipient identificationF3REPLACEMENTReplaces previously declared simplified invoicesR1RECTIFICATION_1Rectification (Art. 80.1, 80.2, legal error)R2RECTIFICATION_2Rectification (Art. 80.3)R3RECTIFICATION_3Rectification (Art. 80.4)R4RECTIFICATION_4Rectification (other cases)R5RECTIFICATION_SIMPLIFIEDRectification of a simplified invoice
8
Build the Tax Breakdown
9
The Breakdown aggregates one or more BreakdownDetail lines, each describing a tax bucket (rate, base, amount, and VAT regime).
10
use eseperio\verifactu\models\Breakdown;
use eseperio\verifactu\models\BreakdownDetail;
use eseperio\verifactu\models\enums\TaxType;
use eseperio\verifactu\models\enums\RegimeType;
use eseperio\verifactu\models\enums\OperationQualificationType;

$breakdown = new Breakdown();

$detail = new BreakdownDetail();
$detail->taxType                 = TaxType::IVA;                                    // 01 – IVA
$detail->regimeKey               = RegimeType::GENERAL;                             // 01 – general
$detail->operationQualification  = OperationQualificationType::SUBJECT_NO_EXEMPT_NO_REVERSE; // S1
$detail->taxRate                 = 21.00;   // percentage
$detail->taxableBase             = 100.00;  // BaseImponible
$detail->taxAmount               = 21.00;   // CuotaRepercutida

$breakdown->addDetail($detail);
$invoice->setBreakdown($breakdown);
11
TaxType options: IVA (01), IPSI (02), IGIC (03), OTHER (05).
12
OperationQualificationType options:
13
CaseValueMeaningSUBJECT_NO_EXEMPT_NO_REVERSES1Subject, not exempt, no reverse chargeSUBJECT_NO_EXEMPT_REVERSES2Subject, not exempt, with reverse chargeNOT_SUBJECT_ARTICLEN1Not subject (Art. 7, 14, others)NOT_SUBJECT_LOCATIONN2Not subject due to location rules
14
For exempt operations, set $detail->exemptOperation (an ExemptOperationType instance) instead of operationQualification. Exactly one of the two must be provided.
15
Configure Chaining
16
Chaining links each record to the previous one via its hash, forming a tamper-evident chain. Use firstRecord only for the very first invoice ever submitted by this issuer.
17
use eseperio\verifactu\models\Chaining;

$chaining = new Chaining();

// ── Option A: first invoice ever ──────────────────────────────────────────────
$chaining->firstRecord = 'S';   // or call $chaining->setAsFirstRecord()

// ── Option B: subsequent invoices ─────────────────────────────────────────────
$chaining->setPreviousInvoice([
    'issuerNif'    => 'B12345678',
    'seriesNumber' => 'FA2024/000',
    'issueDate'    => '2024-06-30',
    'hash'         => '<sha256-hash-of-previous-invoice>',
]);

$invoice->setChaining($chaining);
18
The library stores the hash returned in the AEAT response after each successful submission. Persist $response->csv and the generated $invoice->hash in your database so you can reference them when chaining the next invoice.
19
Set Up ComputerSystem (SistemaInformatico)
20
ComputerSystem describes the invoicing software. AEAT requires this block on every record. The providerId (LegalPerson) identifies the software developer (not the invoice issuer).
21
use eseperio\verifactu\models\ComputerSystem;
use eseperio\verifactu\models\LegalPerson;
use eseperio\verifactu\models\enums\YesNoType;

$provider = new LegalPerson();
$provider->name = 'Software Provider SL';
$provider->nif  = 'B87654321';

$computerSystem = new ComputerSystem();
$computerSystem->systemName          = 'ERP Company';
$computerSystem->version             = '1.0';
$computerSystem->providerName        = 'Software Provider SL';
$computerSystem->systemId            = '01';
$computerSystem->installationNumber  = '1';
$computerSystem->onlyVerifactu       = YesNoType::YES;
$computerSystem->multipleObligations = YesNoType::NO;
$computerSystem->hasMultipleObligations = YesNoType::NO;
$computerSystem->setProviderId($provider);

$invoice->setSystemInfo($computerSystem);
22
Set the Record Timestamp
23
recordTimestamp is the generation date and time of the record. It must include the UTC offset.
24
$invoice->recordTimestamp = '2024-07-01T12:00:00+02:00';  // ISO 8601 with timezone
25
Add Recipients
26
For F1 standard invoices, at least one recipient is required. Use addRecipient() to attach one or more LegalPerson objects. Validation will fail if the recipients list is empty for F1.
27
$recipient = new LegalPerson();
$recipient->name = 'Cliente Ejemplo SL';
$recipient->nif  = 'A98765432';

$invoice->addRecipient($recipient);
28
For foreign recipients without a Spanish NIF, use setOtherId() instead of setting nif:
29
$foreignRecipient = new LegalPerson();
$foreignRecipient->name = 'ACME GmbH';
$foreignRecipient->setOtherId([
    'countryCode' => 'DE',
    'idType'      => '04',   // VAT number
    'id'          => 'DE123456789',
]);
$invoice->addRecipient($foreignRecipient);

Validate Before Submission

Call validate() to catch errors before hitting the AEAT endpoint. It returns true on success, or an associative array of property-keyed error messages.
$validationResult = $invoice->validate();

if ($validationResult !== true) {
    foreach ($validationResult as $property => $errors) {
        echo "Error in '$property': " . implode(', ', $errors) . "\n";
    }
    exit;
}

Submit the Invoice

use eseperio\verifactu\Verifactu;
use eseperio\verifactu\models\InvoiceResponse;

// Configure once at application bootstrap
Verifactu::config(
    '/path/to/certificate.pfx',
    'certificate-password',
    Verifactu::TYPE_CERTIFICATE,
    Verifactu::ENVIRONMENT_PRODUCTION
);

$response = Verifactu::registerInvoice($invoice);

Handle the InvoiceResponse

use eseperio\verifactu\models\InvoiceResponse;

if ($response->submissionStatus === InvoiceResponse::STATUS_OK) {
    // Store $response->csv as proof of submission
    echo "AEAT CSV: " . $response->csv . "\n";
} else {
    // Inspect per-line error codes
    foreach ($response->lineResponses as $lineResponse) {
        echo "Error " . $lineResponse['CodigoErrorRegistro']
           . ": " . $lineResponse['DescripcionErrorRegistro'] . "\n";
    }

    // Or use the convenience helper
    foreach ($response->getErrors() as $code => $description) {
        echo "[$code] $description\n";
    }
}
InvoiceResponse propertyTypeDescription
submissionStatusstring"Correcto" on success (InvoiceResponse::STATUS_OK)
csvstringAEAT-issued Código Seguro de Verificación
lineResponsesarrayPer-record details, including error codes
waitTimestringSuggested wait time before next submission
submissionDataarrayPresentation metadata returned by AEAT

Complete Working Example

<?php

use eseperio\verifactu\Verifactu;
use eseperio\verifactu\models\InvoiceSubmission;
use eseperio\verifactu\models\InvoiceId;
use eseperio\verifactu\models\Breakdown;
use eseperio\verifactu\models\BreakdownDetail;
use eseperio\verifactu\models\Chaining;
use eseperio\verifactu\models\ComputerSystem;
use eseperio\verifactu\models\LegalPerson;
use eseperio\verifactu\models\InvoiceResponse;
use eseperio\verifactu\models\enums\InvoiceType;
use eseperio\verifactu\models\enums\TaxType;
use eseperio\verifactu\models\enums\RegimeType;
use eseperio\verifactu\models\enums\OperationQualificationType;
use eseperio\verifactu\models\enums\YesNoType;

// 1. Configure the library (once at bootstrap)
Verifactu::config(
    '/path/to/certificate.pfx',
    'certificate-password',
    Verifactu::TYPE_CERTIFICATE,
    Verifactu::ENVIRONMENT_SANDBOX  // use ENVIRONMENT_PRODUCTION for live submissions
);

// 2. Invoice ID
$invoiceId = new InvoiceId();
$invoiceId->issuerNif    = 'B12345678';
$invoiceId->seriesNumber = 'FA2024/001';
$invoiceId->issueDate    = '2024-07-01';

// 3. Build the invoice
$invoice = new InvoiceSubmission();
$invoice->setInvoiceId($invoiceId);
$invoice->issuerName           = 'Empresa Ejemplo SL';
$invoice->invoiceType          = InvoiceType::STANDARD;
$invoice->operationDescription = 'Venta de productos';
$invoice->taxAmount            = 21.00;
$invoice->totalAmount          = 121.00;
$invoice->simplifiedInvoice        = YesNoType::NO;
$invoice->invoiceWithoutRecipient  = YesNoType::NO;
$invoice->recordTimestamp      = '2024-07-01T12:00:00+02:00';

// 4. Tax breakdown
$detail = new BreakdownDetail();
$detail->taxType                = TaxType::IVA;
$detail->regimeKey              = RegimeType::GENERAL;
$detail->operationQualification = OperationQualificationType::SUBJECT_NO_EXEMPT_NO_REVERSE;
$detail->taxRate                = 21.00;
$detail->taxableBase            = 100.00;
$detail->taxAmount              = 21.00;

$breakdown = new Breakdown();
$breakdown->addDetail($detail);
$invoice->setBreakdown($breakdown);

// 5. Chaining (first invoice)
$chaining = new Chaining();
$chaining->firstRecord = 'S';
$invoice->setChaining($chaining);

// 6. Computer system
$provider = new LegalPerson();
$provider->name = 'Software Provider SL';
$provider->nif  = 'B87654321';

$computerSystem = new ComputerSystem();
$computerSystem->systemName             = 'ERP Company';
$computerSystem->version                = '1.0';
$computerSystem->providerName           = 'Software Provider SL';
$computerSystem->systemId               = '01';
$computerSystem->installationNumber     = '1';
$computerSystem->onlyVerifactu          = YesNoType::YES;
$computerSystem->multipleObligations    = YesNoType::NO;
$computerSystem->hasMultipleObligations = YesNoType::NO;
$computerSystem->setProviderId($provider);
$invoice->setSystemInfo($computerSystem);

// 7. Recipient (required for F1)
$recipient = new LegalPerson();
$recipient->name = 'Cliente Ejemplo SL';
$recipient->nif  = 'A98765432';
$invoice->addRecipient($recipient);

// 8. Validate
$validationResult = $invoice->validate();
if ($validationResult !== true) {
    print_r($validationResult);
    exit;
}

// 9. Submit
$response = Verifactu::registerInvoice($invoice);

// 10. Handle response
if ($response->submissionStatus === InvoiceResponse::STATUS_OK) {
    echo "Registered successfully. AEAT CSV: " . $response->csv . "\n";
    // Persist $invoice->hash for chaining the next invoice
} else {
    foreach ($response->getErrors() as $code => $description) {
        echo "[$code] $description\n";
    }
}

Special Cases

Simplified Invoices (F2)

Simplified invoices (InvoiceType::SIMPLIFIED) do not require a recipient unless you explicitly identify the buyer. Set invoiceWithoutRecipient = YesNoType::YES to omit recipients entirely, or YesNoType::NO if you choose to include one.
$invoice->invoiceType             = InvoiceType::SIMPLIFIED;
$invoice->simplifiedInvoice       = YesNoType::YES;
$invoice->invoiceWithoutRecipient = YesNoType::YES;
// Do NOT call addRecipient() when invoiceWithoutRecipient is YES

Rectification Invoices (R1–R5)

Rectification invoices must declare which original invoice(s) they rectify. Use addRectifiedInvoice() to reference each original invoice. R1–R4 types require a recipient; R5 (simplified rectification) must not have one.
use eseperio\verifactu\models\enums\RectificationType;

$invoice->invoiceType       = InvoiceType::RECTIFICATION_1;
$invoice->rectificationType = RectificationType::SUBSTITUTION; // or DIFFERENCE

$invoice->addRectifiedInvoice(
    'B12345678',    // issuerNif of the original invoice
    'FA2024/001',   // seriesNumber of the original invoice
    '2024-07-01'    // issueDate of the original invoice
);

Invoices Issued by a Third Party

If a third party issues the invoice on behalf of the obligated issuer, set issuedBy and setThirdParty():
use eseperio\verifactu\models\enums\ThirdPartyOrRecipientType;

$thirdParty = new LegalPerson();
$thirdParty->name = 'Gestoría XYZ SL';
$thirdParty->nif  = 'B11111111';

$invoice->issuedBy = ThirdPartyOrRecipientType::THIRD_PARTY;
$invoice->setThirdParty($thirdParty);

FechaFinVeriFactu (v1.2.1+)

The optional fechaFinVeriFactu field signals voluntary termination of Verifactu submissions. It must be the 31st of December of the current or previous year, formatted as DD-MM-YYYY.
// Signal that this is the last Verifactu submission for 2025
$invoice->fechaFinVeriFactu = '31-12-2025';
fechaFinVeriFactu corresponds to AEAT validation rule 31.1.3. The library validates that the date is always December 31st before serialising the XML.

Build docs developers (and LLMs) love