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.

This guide walks you through submitting your first invoice to AEAT’s sandbox environment. By the end you will have a working PHP script that builds a complete InvoiceSubmission, validates it, and sends it to AEAT.
This quickstart targets the sandbox (ENVIRONMENT_SANDBOX). Never use sandbox code paths in production — always switch to Verifactu::ENVIRONMENT_PRODUCTION and a valid production certificate before going live.

Prerequisites

  • PHP 8.1+ with ext-soap, ext-libxml, ext-openssl, and ext-dom enabled
  • eseperio/verifactu-php installed via Composer (Installation guide)
  • A digital certificate in .p12 / .pfx format for the AEAT sandbox, or the AEAT test certificate

1
Step 1 — Configure the library
2
Call Verifactu::config() once before any other operation. This registers your certificate and selects the correct AEAT endpoint.
3
<?php

require_once __DIR__ . '/vendor/autoload.php';

use eseperio\verifactu\Verifactu;

Verifactu::config(
    '/path/to/your-certificate.p12', // Absolute path to your PKCS#12 certificate
    'your-certificate-password',     // Certificate password
    Verifactu::TYPE_CERTIFICATE,     // TYPE_CERTIFICATE or TYPE_SEAL
    Verifactu::ENVIRONMENT_SANDBOX   // Use ENVIRONMENT_PRODUCTION for live submissions
);
4
Certificate type constants:
5
ConstantUse whenVerifactu::TYPE_CERTIFICATEPersonal or company certificateVerifactu::TYPE_SEALAEAT seal certificate (sello)
6
Environment constants and their SOAP endpoints:
7
ConstantEndpointENVIRONMENT_SANDBOXprewww1.aeat.es / prewww10.aeat.esENVIRONMENT_PRODUCTIONwww1.agenciatributaria.gob.es / www10.agenciatributaria.gob.es
8
9
Step 2 — Build the InvoiceId
10
InvoiceId holds the three fields that uniquely identify an invoice within the AEAT system.
11
use eseperio\verifactu\models\InvoiceId;

$invoiceId = new InvoiceId();
$invoiceId->issuerNif    = 'B12345678';   // Spanish NIF of the invoice issuer
$invoiceId->seriesNumber = 'FA2024/001';  // Series + invoice number (up to 60 chars)
$invoiceId->issueDate    = '2024-07-01';  // Issue date in YYYY-MM-DD format
12
The library automatically converts issueDate from YYYY-MM-DD (ISO 8601 — the format you set here) to DD-MM-YYYY when serialising to XML, as required by the AEAT schema. Always set dates in YYYY-MM-DD format in PHP.
13
14
Step 3 — Build the ComputerSystem
15
ComputerSystem identifies the invoicing software and the software provider. Every record must include it.
16
use eseperio\verifactu\models\ComputerSystem;
use eseperio\verifactu\models\LegalPerson;
use eseperio\verifactu\models\enums\YesNoType;

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

// Computer system metadata
$computerSystem = new ComputerSystem();
$computerSystem->providerName        = 'Software Provider SL';
$computerSystem->systemName          = 'My ERP System';
$computerSystem->systemId            = '01';
$computerSystem->version             = '1.0';
$computerSystem->installationNumber  = '1';
$computerSystem->onlyVerifactu       = YesNoType::YES; // This software only emits Verifactu invoices
$computerSystem->multipleObligations = YesNoType::NO;  // Single tax obligation
$computerSystem->hasMultipleObligations = YesNoType::NO;
$computerSystem->setProviderId($provider);
17
18
Step 4 — Build the tax Breakdown
19
Breakdown contains one or more BreakdownDetail lines — one per tax rate.
20
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;

$detail = new BreakdownDetail();
$detail->taxType                = TaxType::IVA;                                          // IVA (01)
$detail->regimeKey              = RegimeType::GENERAL;                                   // General regime (01)
$detail->operationQualification = OperationQualificationType::SUBJECT_NO_EXEMPT_NO_REVERSE; // S1
$detail->taxRate                = 21.00;  // 21% VAT
$detail->taxableBase            = 100.00; // Net amount before tax
$detail->taxAmount              = 21.00;  // Tax amount (taxableBase × taxRate / 100)

$breakdown = new Breakdown();
$breakdown->addDetail($detail);
21
For invoices with multiple tax rates (e.g. 10% and 21%), call $breakdown->addDetail() once per rate.
22
23
Step 5 — Set up invoice chaining
24
Chaining links each invoice to the hash of the previous one, creating the tamper-evident chain AEAT requires.
25
use eseperio\verifactu\models\Chaining;

// For the very first invoice in a series:
$chaining = new Chaining();
$chaining->setAsFirstRecord(); // Sets PrimerRegistro = 'S'

// For subsequent invoices, link to the previous record's hash instead:
// $chaining->setPreviousInvoice([
//     'issuerNif'    => 'B12345678',
//     'seriesNumber' => 'FA2024/000',
//     'issueDate'    => '2024-06-30',
//     'hash'         => '<SHA-256 hash of previous invoice>',
// ]);
26
You do not calculate the hash yourself before submission. The library’s HashGeneratorService computes the SHA-256 “huella” automatically during Verifactu::registerInvoice(), following the exact AEAT field ordering specification. The hash field on the chaining is the hash of the previous invoice (already submitted), not the current one.
27
28
Step 6 — Assemble the InvoiceSubmission
29
Combine all the components into a complete InvoiceSubmission object.
30
use eseperio\verifactu\models\InvoiceSubmission;
use eseperio\verifactu\models\LegalPerson;
use eseperio\verifactu\models\enums\InvoiceType;
use eseperio\verifactu\models\enums\YesNoType;

$invoice = new InvoiceSubmission();

// Invoice identification
$invoice->setInvoiceId($invoiceId);

// Basic invoice data
$invoice->issuerName          = 'Empresa Ejemplo SL';      // Must match AEAT census exactly
$invoice->invoiceType         = InvoiceType::STANDARD;     // F1 — standard invoice
$invoice->operationDescription = 'Venta de productos';     // Free-text description
$invoice->taxAmount           = 21.00;                     // Total tax (all rates summed)
$invoice->totalAmount         = 121.00;                    // Grand total including tax
$invoice->simplifiedInvoice   = YesNoType::NO;
$invoice->invoiceWithoutRecipient = YesNoType::NO;

// Tax breakdown
$invoice->setBreakdown($breakdown);

// Chaining
$invoice->setChaining($chaining);

// Computer system
$invoice->setSystemInfo($computerSystem);

// Timestamp — use the actual generation date/time with timezone offset
$invoice->recordTimestamp = '2024-07-01T12:00:00+02:00';

// Recipient (required for STANDARD / F1 invoices)
$recipient = new LegalPerson();
$recipient->name = 'Cliente Ejemplo SL';
$recipient->nif  = 'A98765432';
$invoice->addRecipient($recipient);
31
InvoiceType enum reference:
32
ConstantAEAT codeDescriptionInvoiceType::STANDARDF1Standard invoice (Art. 6 RD 1619/2012)InvoiceType::SIMPLIFIEDF2Simplified invoiceInvoiceType::REPLACEMENTF3Replaces previously declared simplified invoicesInvoiceType::RECTIFICATION_1R1Rectification (Art. 80.1, 80.2, legal error)InvoiceType::RECTIFICATION_2R2Rectification (Art. 80.3)InvoiceType::RECTIFICATION_3R3Rectification (Art. 80.4)InvoiceType::RECTIFICATION_4R4Rectification (other cases)InvoiceType::RECTIFICATION_SIMPLIFIEDR5Rectification of simplified invoice
33
34
Step 7 — Validate before submission
35
Call validate() to catch any missing or invalid fields before hitting the AEAT endpoint.
36
$validationResult = $invoice->validate();

if ($validationResult !== true) {
    // $validationResult is an associative array: ['fieldName' => ['error message', ...]]
    foreach ($validationResult as $field => $errors) {
        echo "Field '{$field}': " . implode(', ', $errors) . PHP_EOL;
    }
    exit(1);
}
37
Common validation errors to watch for:
38
  • invoiceId: missing issuerNif, seriesNumber, or issueDate in wrong format
  • recipients: required for InvoiceType::STANDARD invoices
  • breakdown: at least one BreakdownDetail required
  • chaining: either firstRecord or a previous invoice hash must be set (not both)
  • systemInfo: all ComputerSystem fields including hasMultipleObligations are required
  • 39
    40
    Step 8 — Submit to AEAT
    41
    Once validation passes, submit the invoice with Verifactu::registerInvoice():
    42
    use eseperio\verifactu\models\InvoiceResponse;
    
    try {
        $response = Verifactu::registerInvoice($invoice);
    } catch (\SoapFault $e) {
        // Network / SOAP communication error
        echo "SOAP error: " . $e->getMessage() . PHP_EOL;
        exit(1);
    } catch (\Exception $e) {
        // Certificate, XML signing, or other error
        echo "Error: " . $e->getMessage() . PHP_EOL;
        exit(1);
    }
    
    43
    44
    Step 9 — Handle the response
    45
    if ($response->submissionStatus === InvoiceResponse::STATUS_OK) {
        // SUCCESS: invoice accepted by AEAT
        echo "Invoice registered successfully!" . PHP_EOL;
        echo "AEAT CSV: " . $response->csv . PHP_EOL;
    
        // The CSV is now printed on the invoice and embedded in the QR code
        // Store $response->csv in your database alongside the invoice record
    
    } else {
        // REJECTED: one or more lines were rejected by AEAT
        echo "Submission status: " . $response->submissionStatus . PHP_EOL;
    
        // lineResponses contains per-invoice error details
        if (is_array($response->lineResponses)) {
            foreach ($response->lineResponses as $line) {
                if (!empty($line['CodigoErrorRegistro'])) {
                    echo "Error code: " . $line['CodigoErrorRegistro'] . PHP_EOL;
                    echo "Description: " . $line['DescripcionErrorRegistro'] . PHP_EOL;
                }
            }
        }
    
        // Convenience helper — returns ['errorCode' => 'description'] array
        $errors = $response->getErrors();
        print_r($errors);
    }
    
    46
    InvoiceResponse::STATUS_OK is the string 'Correcto'. Any other value in submissionStatus means AEAT rejected the submission.
    47

    Complete working example

    Here is the full script from Steps 1–9 in one block, ready to run:
    <?php
    
    require_once __DIR__ . '/vendor/autoload.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 ─────────────────────────────────────────────────────────────
    Verifactu::config(
        '/path/to/your-certificate.p12',
        'your-certificate-password',
        Verifactu::TYPE_CERTIFICATE,
        Verifactu::ENVIRONMENT_SANDBOX
    );
    
    // ── 2. Invoice ID ─────────────────────────────────────────────────────────────
    $invoiceId = new InvoiceId();
    $invoiceId->issuerNif    = 'B12345678';
    $invoiceId->seriesNumber = 'FA2024/001';
    $invoiceId->issueDate    = '2024-07-01'; // YYYY-MM-DD; serializer converts to DD-MM-YYYY
    
    // ── 3. Computer system ────────────────────────────────────────────────────────
    $provider = new LegalPerson();
    $provider->name = 'Software Provider SL';
    $provider->nif  = 'B87654321';
    
    $computerSystem = new ComputerSystem();
    $computerSystem->providerName           = 'Software Provider SL';
    $computerSystem->systemName             = 'My ERP System';
    $computerSystem->systemId               = '01';
    $computerSystem->version                = '1.0';
    $computerSystem->installationNumber     = '1';
    $computerSystem->onlyVerifactu          = YesNoType::YES;
    $computerSystem->multipleObligations    = YesNoType::NO;
    $computerSystem->hasMultipleObligations = YesNoType::NO;
    $computerSystem->setProviderId($provider);
    
    // ── 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);
    
    // ── 5. Chaining ───────────────────────────────────────────────────────────────
    $chaining = new Chaining();
    $chaining->setAsFirstRecord(); // First invoice in the chain
    
    // ── 6. Recipient (required for F1 / STANDARD) ─────────────────────────────────
    $recipient = new LegalPerson();
    $recipient->name = 'Cliente Ejemplo SL';
    $recipient->nif  = 'A98765432';
    
    // ── 7. Assemble InvoiceSubmission ─────────────────────────────────────────────
    $invoice = new InvoiceSubmission();
    $invoice->setInvoiceId($invoiceId);
    $invoice->issuerName               = 'Empresa Ejemplo SL'; // Must match AEAT census
    $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';
    $invoice->setBreakdown($breakdown);
    $invoice->setChaining($chaining);
    $invoice->setSystemInfo($computerSystem);
    $invoice->addRecipient($recipient);
    
    // ── 8. Validate ───────────────────────────────────────────────────────────────
    $validation = $invoice->validate();
    if ($validation !== true) {
        echo "Validation errors:" . PHP_EOL;
        foreach ($validation as $field => $errors) {
            echo "  [{$field}] " . implode(', ', $errors) . PHP_EOL;
        }
        exit(1);
    }
    
    // ── 9. Submit ─────────────────────────────────────────────────────────────────
    try {
        $response = Verifactu::registerInvoice($invoice);
    } catch (\SoapFault $e) {
        echo "SOAP fault: " . $e->getMessage() . PHP_EOL;
        exit(1);
    } catch (\Exception $e) {
        echo "Error: " . $e->getMessage() . PHP_EOL;
        exit(1);
    }
    
    // ── 10. Handle response ───────────────────────────────────────────────────────
    if ($response->submissionStatus === InvoiceResponse::STATUS_OK) {
        echo "✅ Invoice accepted by AEAT" . PHP_EOL;
        echo "CSV: " . $response->csv . PHP_EOL;
    } else {
        echo "❌ Submission rejected — status: " . $response->submissionStatus . PHP_EOL;
        foreach ($response->getErrors() as $code => $description) {
            echo "  [{$code}] {$description}" . PHP_EOL;
        }
    }
    

    Generating a QR code

    After a successful submission, generate the AEAT-compliant QR code to print on the invoice:
    // Attach the CSV returned by AEAT to the invoice object
    $invoice->csv = $response->csv;
    
    // Returns raw PNG image data as a string (GD renderer, 300px)
    $qrImageData = Verifactu::generateInvoiceQr($invoice);
    
    // Embed in HTML
    $base64 = base64_encode($qrImageData);
    echo '<img src="data:image/png;base64,' . $base64 . '" alt="AEAT QR Code" />';
    
    The QR payload includes the invoice’s NIF, series number, issue date, total amount, and — once submitted — the CSV. The huella (hash) parameter is omitted automatically if the hash has not yet been calculated.

    Common AEAT error codes

    CodeMeaning
    100SOAP request signature is not valid
    101SOAP request is empty
    106Certificate is on a blocklist or is a test certificate used in production
    1105The value of the NIF field in the ObligadoEmision block does not match AEAT census
    1106The issuer name does not match the NIF in the AEAT census
    If you receive error 1105 or 1106: the issuerNif and issuerName fields must exactly match the data registered with AEAT. Verify them at Mis datos censales on the AEAT website.

    Next steps

    • Cancel an invoice — use InvoiceCancellation with Verifactu::cancelInvoice()
    • Query submitted invoices — use InvoiceQuery with Verifactu::queryInvoices()
    • Advanced QR options — choose GD, Imagick, or SVG renderer and control resolution via QrGeneratorService constants
    • Custom SOAP endpoint — use VerifactuService::config() directly for full control over endpoint URLs

    Build docs developers (and LLMs) love