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.

AeatClient wraps the AEAT SOAP web service that VERI*FACTU SIFs are required to use for submitting billing records. The class handles request building, mutual-TLS certificate authentication, XML serialization, and response parsing — letting you focus on record construction rather than SOAP mechanics.

Prerequisites

Before configuring the client you need:
  • An FNMT or AEAT-recognised certificate issued to the entity that will authenticate the connection. Accepted formats:
    • PKCS#12 / PFX bundle (.p12 or .pfx extension)
    • Encrypted PEM certificate
  • A configured ComputerSystem object that describes your SIF.
  • A FiscalIdentifier object with the name and NIF of the taxpayer whose invoices are being submitted.

Configuring the client

<?php
use josemmo\Verifactu\Models\ComputerSystem;
use josemmo\Verifactu\Models\Records\FiscalIdentifier;
use josemmo\Verifactu\Services\AeatClient;

// 1. Describe the SIF (billing computer system)
$system                        = new ComputerSystem();
$system->vendorName            = 'Acme Software, S.A.';     // vendor legal name
$system->vendorNif             = 'A00000000';               // vendor NIF (9 chars)
$system->name                  = 'My Billing SIF';          // system name
$system->id                    = 'XX';                       // system ID code
$system->version               = '1.0.0';                   // software version
$system->installationNumber    = 'ABC0123';                  // installation number
$system->onlySupportsVerifactu = true;                       // exclusive VERI*FACTU mode
$system->supportsMultipleTaxpayers = true;
$system->hasMultipleTaxpayers  = false;
$system->validate();

// 2. Taxpayer whose invoices are being submitted
$taxpayer = new FiscalIdentifier('Acme Servicios, S.A.', 'A00000001');

// 3. Create the client
$client = new AeatClient($system, $taxpayer);
$client->setCertificate(__DIR__ . '/certs/certificate.pfx', 'p@ssw0rd');
$client->setProduction(false); // use pre-production environment during development
Always call setProduction(false) during development and testing. This routes requests to https://prewww1.aeat.es instead of the live production endpoint, so test records are never written to the real AEAT registry.

Sending records

Pass an array of RegistrationRecord or CancellationRecord objects to send(). The method returns a PromiseInterface — call ->wait() for synchronous, blocking execution.
use josemmo\Verifactu\Models\Records\RegistrationRecord;

// $record is a fully built and validated RegistrationRecord or CancellationRecord
$response = $client->send([$record])->wait();
You can include up to 1000 records in a single send() call. Mixing registration and cancellation records in the same call is allowed.

Processing the response

The AeatResponse object reports the overall submission status, the AEAT-generated CSV verification code, and the mandatory wait time before the next call.
use josemmo\Verifactu\Models\Responses\ResponseStatus;

echo "Status:        " . $response->status->value . "\n";
echo "CSV:           " . ($response->csv ?? 'n/a') . "\n";
echo "Submitted at:  " . ($response->submittedAt?->format('c') ?? 'n/a') . "\n";
echo "Wait (seconds): " . $response->waitSeconds . "\n";

switch ($response->status) {
    case ResponseStatus::Correct:
        // All records were accepted — CSV is always present
        echo "All records accepted. CSV: {$response->csv}\n";
        break;

    case ResponseStatus::PartiallyCorrect:
        // Some records were accepted, others rejected — check per-record items
        echo "Partial acceptance. Check individual record statuses.\n";
        break;

    case ResponseStatus::Incorrect:
        // All records were rejected
        echo "All records rejected.\n";
        break;
}
ResponseStatus values:
CaseRaw valueMeaning
CorrectCorrectoEvery record in the batch was accepted
PartiallyCorrectParcialmenteCorrectoAt least one record has errors or was rejected
IncorrectIncorrectoEvery record in the batch was rejected
After a successful send() call, you must wait at least $response->waitSeconds seconds before making the next call to the same endpoint. The AEAT enforces this throttle server-side; submitting too quickly will result in errors.

Per-record status

When the response status is PartiallyCorrect or Incorrect, iterate $response->items to find which records succeeded and which failed.
use josemmo\Verifactu\Models\Responses\ItemStatus;

foreach ($response->items as $item) {
    $invoiceRef = "{$item->invoiceId->issuerId}/{$item->invoiceId->invoiceNumber}";

    switch ($item->status) {
        case ItemStatus::Correct:
            echo "[$invoiceRef] Accepted.\n";
            break;

        case ItemStatus::AcceptedWithErrors:
            // Accepted but flagged with a non-blocking warning
            echo "[$invoiceRef] Accepted with errors: {$item->errorCode} — {$item->errorDescription}\n";
            break;

        case ItemStatus::Incorrect:
            // Rejected — must be corrected and resubmitted
            echo "[$invoiceRef] Rejected: {$item->errorCode} — {$item->errorDescription}\n";
            break;
    }
}
ItemStatus values:
CaseRaw valueMeaning
CorrectCorrectoRecord accepted without issues
AcceptedWithErrorsAceptadoConErroresAccepted but contains non-blocking errors
IncorrectIncorrectoRecord rejected — must be corrected and resubmitted

Using a representative

If a third-party software provider or accounting firm submits invoices on behalf of the taxpayer, configure a representative using setRepresentative().
use josemmo\Verifactu\Models\Records\FiscalIdentifier;

$representative = new FiscalIdentifier('Gestoría Pérez, S.L.', 'B98765432');
$client->setRepresentative($representative);
The represented taxpayer must first authorise the representative by filing the GENERALLEY58 form with the AEAT. To remove a previously configured representative, pass null.

Voluntary remission end date

When a SIF that has been operating in VERI*FACTU mode wants to switch to Non-VERI*FACTU mode, it must notify the AEAT of the end date of its voluntary remission. Use setVoluntaryRemissionEndDate() and then call send() — even with an empty array — to notify AEAT immediately.
use DateTimeImmutable;

// Notify AEAT that voluntary remission ends on 2026-12-31
$client->setVoluntaryRemissionEndDate(new DateTimeImmutable('2026-12-31'));

// Send the notification with no records attached
$client->send([])->wait();
Pass null to clear a previously set end date:
$client->setVoluntaryRemissionEndDate(null);

AEAT requirement response

When the SIF operates in Non-VERI*FACTU mode, the AEAT can issue a formal request (requerimiento) asking for records. Use setRequirementReference() to include the AEAT’s reference code in your submissions.
// First batch of records for this requirement
$client->setRequirementReference('REF00001ABDEAF1234');
$client->send([$record1, $record2])->wait();

// Intermediate batches — same reference, isLastRequirementSubmission = false (default)
$client->send([$record3])->wait();

// Final batch — signals to AEAT that all records have been sent
$client->setRequirementReference('REF00001ABDEAF1234', true);
$client->send([$record4])->wait();

// Disable requirement mode when done
$client->setRequirementReference(null);
Set $isLastRequirementSubmission = true only in the final batch so the AEAT knows no more pages will follow.

Entity seal certificate

Some entities use an entity seal (sello de entidad) certificate instead of a personal certificate. When this is the case, call setEntitySeal(true) to route requests to the entity-seal-specific AEAT endpoints (www10 / prewww10 instead of www1 / prewww1).
$client->setEntitySeal(true);
ModeProduction URLPre-production URL
Personal certificatehttps://www1.agenciatributaria.gob.eshttps://prewww1.aeat.es
Entity sealhttps://www10.agenciatributaria.gob.eshttps://prewww10.aeat.es

Build docs developers (and LLMs) love