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:
| Case | Raw value | Meaning |
|---|
Correct | Correcto | Every record in the batch was accepted |
PartiallyCorrect | ParcialmenteCorrecto | At least one record has errors or was rejected |
Incorrect | Incorrecto | Every 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:
| Case | Raw value | Meaning |
|---|
Correct | Correcto | Record accepted without issues |
AcceptedWithErrors | AceptadoConErrores | Accepted but contains non-blocking errors |
Incorrect | Incorrecto | Record 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);
| Mode | Production URL | Pre-production URL |
|---|
| Personal certificate | https://www1.agenciatributaria.gob.es | https://prewww1.aeat.es |
| Entity seal | https://www10.agenciatributaria.gob.es | https://prewww10.aeat.es |