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 completeDocumentation 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.
InvoiceSubmission, validates it, and sends it to AEAT.
Prerequisites
- PHP 8.1+ with
ext-soap,ext-libxml,ext-openssl, andext-domenabled eseperio/verifactu-phpinstalled via Composer (Installation guide)- A digital certificate in
.p12/.pfxformat for the AEAT sandbox, or the AEAT test certificate
Call
Verifactu::config() once before any other operation. This registers your certificate and selects the correct AEAT endpoint.<?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
);
Verifactu::TYPE_CERTIFICATEVerifactu::TYPE_SEALENVIRONMENT_SANDBOXprewww1.aeat.es / prewww10.aeat.esENVIRONMENT_PRODUCTIONwww1.agenciatributaria.gob.es / www10.agenciatributaria.gob.esuse 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
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.ComputerSystem identifies the invoicing software and the software provider. Every record must include it.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);
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);
For invoices with multiple tax rates (e.g. 10% and 21%), call
$breakdown->addDetail() once per rate.Chaining links each invoice to the hash of the previous one, creating the tamper-evident chain AEAT requires.
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>',
// ]);
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.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);
InvoiceType::STANDARDF1InvoiceType::SIMPLIFIEDF2InvoiceType::REPLACEMENTF3InvoiceType::RECTIFICATION_1R1InvoiceType::RECTIFICATION_2R2InvoiceType::RECTIFICATION_3R3InvoiceType::RECTIFICATION_4R4InvoiceType::RECTIFICATION_SIMPLIFIEDR5$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);
}
invoiceId: missing issuerNif, seriesNumber, or issueDate in wrong formatrecipients: required for InvoiceType::STANDARD invoicesbreakdown: at least one BreakdownDetail requiredchaining: either firstRecord or a previous invoice hash must be set (not both)systemInfo: all ComputerSystem fields including hasMultipleObligations are requireduse 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);
}
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);
}
InvoiceResponse::STATUS_OK is the string 'Correcto'. Any other value in submissionStatus means AEAT rejected the submission.Complete working example
Here is the full script from Steps 1–9 in one block, ready to run:Generating a QR code
After a successful submission, generate the AEAT-compliant QR code to print on the invoice: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
| Code | Meaning |
|---|---|
100 | SOAP request signature is not valid |
101 | SOAP request is empty |
106 | Certificate is on a blocklist or is a test certificate used in production |
1105 | The value of the NIF field in the ObligadoEmision block does not match AEAT census |
1106 | The 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
InvoiceCancellationwithVerifactu::cancelInvoice() - Query submitted invoices — use
InvoiceQuerywithVerifactu::queryInvoices() - Advanced QR options — choose GD, Imagick, or SVG renderer and control resolution via
QrGeneratorServiceconstants - Custom SOAP endpoint — use
VerifactuService::config()directly for full control over endpoint URLs