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

Every invoice submission (or cancellation) to AEAT follows the same seven-step pipeline. The Verifactu facade orchestrates all of this for you through a single method call — but understanding what happens at each stage helps you handle errors, debug issues, and extend the library when needed.
Invoice Model  →  Validate  →  Hash  →  Serialize XML  →  Sign XML  →  SOAP  →  Parse Response
The pipeline is implemented in VerifactuService, which is called by the Verifactu facade methods (registerInvoice(), cancelInvoice(), queryInvoices()).

Step-by-Step Walkthrough

1

Build the Invoice Model

You construct an InvoiceSubmission (for a new invoice) or InvoiceCancellation (to cancel one) and populate all required fields. This includes the invoice ID, tax breakdown, chaining data, system information, and timestamp.The library uses strongly-typed model classes that map directly to the AEAT XSD schema. All models extend a base Model class that provides field-level validation.
use eseperio\verifactu\models\InvoiceSubmission;
use eseperio\verifactu\models\InvoiceId;
use eseperio\verifactu\models\Chaining;
use eseperio\verifactu\models\ComputerSystem;
use eseperio\verifactu\models\LegalPerson;
use eseperio\verifactu\models\Breakdown;
use eseperio\verifactu\models\BreakdownDetail;
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;

$invoice = new InvoiceSubmission();

// Invoice identity
$invoiceId = new InvoiceId();
$invoiceId->issuerNif    = 'B12345678';
$invoiceId->seriesNumber = 'FA2024/001';
$invoiceId->issueDate    = '01-07-2024'; // DD-MM-YYYY
$invoice->setInvoiceId($invoiceId);

// Basic fields
$invoice->issuerName         = 'Empresa Ejemplo SL';
$invoice->invoiceType        = InvoiceType::STANDARD;
$invoice->operationDescription = 'Venta de productos';
$invoice->taxAmount          = 21.00;
$invoice->totalAmount        = 121.00;
$invoice->recordTimestamp    = '2024-07-01T12:00:00+02:00';

// Tax breakdown
$breakdown = new Breakdown();
$detail = new BreakdownDetail();
$detail->taxType                = TaxType::IVA;
$detail->regimeKey              = RegimeType::GENERAL;
$detail->taxRate                = 21.00;
$detail->taxableBase            = 100.00;
$detail->taxAmount              = 21.00;
$detail->operationQualification = OperationQualificationType::SUBJECT_NO_EXEMPT_NO_REVERSE;
$breakdown->addDetail($detail);
$invoice->setBreakdown($breakdown);
See the Chaining & Hashing page for how to set up the Chaining block and the Certificates page for ComputerSystem setup.
2

Pre-Hash Validation

Before generating the hash, VerifactuService calls $invoice->validate() to check that all required fields are present and correctly typed. Validation errors are returned as an associative array keyed by property name.
// You can run this manually before submitting:
$errors = $invoice->validate();

if (!empty($errors)) {
    foreach ($errors as $property => $messages) {
        echo "Field '{$property}': " . implode(', ', $messages) . PHP_EOL;
    }
}
If validation fails during registerInvoice(), an \InvalidArgumentException is thrown with the full validation report. This happens before any network call, so no partial submissions occur.
The hash field is intentionally excluded from pre-hash validation — it has not been calculated yet at this stage. A second validation pass runs after hash generation to verify the complete record.
3

Generate the SHA-256 Hash (Huella)

The library calls HashGeneratorService::generate($invoice) to compute the invoice’s SHA-256 hash (huella) and assigns the result to $invoice->hash. You do not need to call this yourself — it happens automatically inside registerInvoice() and cancelInvoice().Internally, HashGeneratorService concatenates the required fields in the exact order mandated by the AEAT specification, then runs strtoupper(hash('sha256', $dataString)).For an InvoiceSubmission the concatenated string looks like:
IDEmisorFactura=B12345678&NumSerieFactura=FA2024/001&FechaExpedicionFactura=01-07-2024&TipoFactura=F1&CuotaTotal=21.00&ImporteTotal=121.00&Huella=<PREV_HASH>&FechaHoraHusoGenRegistro=2024-07-01T12:00:00+02:00
See Chaining & Hashing for the full field concatenation rules and the cancellation variant.
4

Serialize to XML

InvoiceSerializer converts the validated, hashed model into a DOMDocument that matches the official AEAT XSD schema. For registrations, toInvoiceXml() produces the RegistroAlta block; for cancellations, toCancellationXml() produces the RegistroAnulacion block. The record block is later wrapped in the RegFactuSistemaFacturacion envelope (with a Cabecera containing the issuer NIF and name) by wrapXmlWithRegFactuStructure().
// This is done internally — shown here for illustration only.
// use eseperio\verifactu\services\InvoiceSerializer;

$invoiceDom = InvoiceSerializer::toInvoiceXml($invoice);
You can call InvoiceSerializer::toInvoiceXml($invoice) directly if you need to inspect or log the raw XML outside of the normal submission flow.
5

Digitally Sign the XML (XAdES Enveloped)

XmlSignerService signs the XML using the XAdES Enveloped standard (via the robrichards/xmlseclibs library). The <Signature> element is appended inside the document root, embedding the signature within the XML itself. For invoice registrations, the RegistroAlta block is signed before being wrapped in the RegFactuSistemaFacturacion envelope; the wrapping follows after signing. For cancellations, the fully wrapped XML is signed in one step.The signing process:
  1. Loads your PFX/P12 certificate and extracts the X.509 certificate and private key via CertificateManagerService.
  2. Creates an XMLSecurityDSig object with EXC_C14N canonicalization and SHA256 digest.
  3. Signs the document with RSA_SHA256.
  4. Attaches the X.509 certificate in <KeyInfo> with the subject name.
// This is done internally — shown here for illustration only.
// use eseperio\verifactu\services\XmlSignerService;

$signedXml = XmlSignerService::signXml(
    $invoiceDom->saveXML(),
    '/path/to/certificate.pfx',
    'certificate-password'
);
XML signing requires the robrichards/xmlseclibs Composer package. If it is not installed, XmlSignerService::signXml() throws a \RuntimeException with the message “xmlseclibs library is required for XML signing.”
6

Transmit to AEAT via SOAP

SoapClientFactoryService builds a PHP SoapClient configured with the SOAP endpoint URL and a temporary PEM file (created by CertificateManagerService::createSoapCompatiblePemTemp()) that combines the certificate and private key for mutual TLS authentication.The signed XML is sent as a raw SoapVar with type XSD_ANYXML to avoid double-encoding, using the RegFactuSistemaFacturacion SOAP action for registrations/cancellations or ConsultaFactuSistemaFacturacion for queries.
// This is done internally by VerifactuService::getClient() and the call below.
$soapVar = new \SoapVar($signedXml, XSD_ANYXML);
$client->__soapCall('RegFactuSistemaFacturacion', [$soapVar]);
If a SoapFault is thrown (network error, TLS failure, or AEAT-level SOAP rejection), the exception is caught, logged via error_log(), and re-thrown as a \RuntimeException with a descriptive message. See the error codes table for common AEAT SOAP error codes.
7

Parse the AEAT Response

ResponseParserService reads the raw SOAP response XML and converts it into a typed PHP model:
  • InvoiceResponse — returned by registerInvoice() and cancelInvoice()
  • QueryResponse — returned by queryInvoices()
$response = Verifactu::registerInvoice($invoice);

if ($response->submissionStatus === \eseperio\verifactu\models\InvoiceResponse::STATUS_OK) {
    // Store the CSV — print it on the invoice and attach it to your records.
    $csv = $response->csv;
    echo "Registered. AEAT CSV: {$csv}";
} else {
    // Inspect per-line error details
    foreach ($response->lineResponses as $line) {
        $code = $line['CodigoErrorRegistro'] ?? '';
        $desc = $line['DescripcionErrorRegistro'] ?? '';
        echo "[{$code}] {$desc}" . PHP_EOL;
    }
}
All AEAT numeric error codes are mapped to human-readable messages using the official dictionary in src/dictionaries/ErrorRegistry.php. Each line in $response->lineResponses is an associative array; the getErrors() method on InvoiceResponse returns a [code => description] map of all errored lines.For queries, iterate $result->foundRecords to access the returned invoice records:
$result = Verifactu::queryInvoices($query);

// queryResult is 'ConDatos' when records were found, or 'SinDatos' when none match.
if (!empty($result->foundRecords)) {
    foreach ($result->foundRecords as $record) {
        // Each record is an associative array from the AEAT XML response.
        echo $record['IDFactura']['NumSerieFactura'] ?? '' . PHP_EOL;
    }
}

// paginationIndicator is 'S' when more pages are available.
if ($result->paginationIndicator === 'S') {
    // Use $query->setPaginationKey($result->paginationKey) to fetch the next page.
}

Service Responsibilities at a Glance

ServiceResponsibility
VerifactuPublic facade — thin wrapper around VerifactuService
VerifactuServiceOrchestrates the full pipeline; holds SOAP client and config
HashGeneratorServiceBuilds the AEAT-compliant field string and computes SHA-256
InvoiceSerializerConverts models to DOMDocument matching AEAT XSDs
XmlSignerServiceXAdES Enveloped signing with robrichards/xmlseclibs
CertificateManagerServiceLoads PFX/P12 certificates; creates temporary PEM for SOAP
SoapClientFactoryServiceInstantiates a SoapClient with TLS certificate options
ResponseParserServiceParses raw SOAP response XML into InvoiceResponse/QueryResponse
QrGeneratorServiceGenerates AEAT-compliant QR codes in GD, Imagick, or SVG format

Cancellation Workflow

The cancellation workflow is identical in structure, using InvoiceCancellation instead of InvoiceSubmission. The hash concatenation format differs slightly (uses IDEmisorFacturaAnulada / NumSerieFacturaAnulada / FechaExpedicionFacturaAnulada field names). See Chaining & Hashing for the exact format.
use eseperio\verifactu\Verifactu;
use eseperio\verifactu\models\InvoiceCancellation;
use eseperio\verifactu\models\InvoiceId;

$cancellation = new InvoiceCancellation();

$invoiceId = new InvoiceId();
$invoiceId->issuerNif    = 'B12345678';
$invoiceId->seriesNumber = 'FA2024/001';
$invoiceId->issueDate    = '01-07-2024';
$cancellation->setInvoiceId($invoiceId);

// ... set issuerName, chaining, systemInfo, recordTimestamp ...

$response = Verifactu::cancelInvoice($cancellation);

Exception Handling

Wrap all submission calls in a try/catch to handle the different failure modes:
try {
    $response = Verifactu::registerInvoice($invoice);
} catch (\InvalidArgumentException $e) {
    // Validation failed before any network call
    echo 'Validation error: ' . $e->getMessage();
} catch (\RuntimeException $e) {
    // SOAP communication failed or certificate could not be loaded
    echo 'Runtime error: ' . $e->getMessage();
} catch (\SoapFault $e) {
    // Low-level SOAP fault (network timeout, TLS error, etc.)
    echo 'SOAP fault: ' . $e->getMessage();
} catch (\Exception $e) {
    echo 'Unexpected error: ' . $e->getMessage();
}

Build docs developers (and LLMs) love