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.

When to Cancel vs. When to Rectify

AEAT distinguishes two ways to correct an already-submitted invoice:
SituationAction
The invoice was submitted to AEAT but the underlying commercial transaction should not have been invoiced at all, or the invoice was sent by mistakeCancel (InvoiceCancellation)
The invoice was valid but details (amount, tax rate, recipient…) need correctionRectify — issue a new InvoiceSubmission of type R1R5 referencing the original
Use InvoiceCancellation to annul a record. This creates a RegistroAnulacion entry in the AEAT ledger and does not issue a replacement invoice. Once cancelled, the original invoice cannot be amended further; a rectification invoice must be submitted if the underlying transaction still took place.
Cancelling a correctly-issued invoice to re-issue it with different amounts is not the right workflow. Always use a rectification invoice (R1–R5) for corrections that involve an actual commercial transaction. Using cancellation for amount changes may trigger AEAT audits.

Building an InvoiceCancellation Step by Step

1
Set the Invoice ID
2
The InvoiceId must match the original invoice exactly — same issuerNif, seriesNumber, and issueDate as the previously registered record.
3
use eseperio\verifactu\models\InvoiceCancellation;
use eseperio\verifactu\models\InvoiceId;

$cancellation = new InvoiceCancellation();

$invoiceId = new InvoiceId();
$invoiceId->issuerNif    = 'B12345678';    // Must match the original submission
$invoiceId->seriesNumber = 'FA2024/001';   // Must match the original submission
$invoiceId->issueDate    = '2024-07-01';   // Must match the original submission (YYYY-MM-DD)

$cancellation->setInvoiceId($invoiceId);
4
Set the Issuer Name
5
The issuerName field is required on cancellations (fix for issue #27). It must match the original issuer name submitted to AEAT.
6
$cancellation->issuerName = 'Empresa Ejemplo SL';
7
Configure Chaining
8
Just like invoice submissions, cancellations participate in the hash chain. Reference the hash of the most recent record in the chain (which may be an invoice submission or another cancellation).
9
use eseperio\verifactu\models\Chaining;

$chaining = new Chaining();

// Reference the previous record's hash
$chaining->setPreviousInvoice([
    'issuerNif'    => 'B12345678',
    'seriesNumber' => 'FA2024/001',       // series+number of the previous record
    'issueDate'    => '2024-07-01',       // issue date of the previous record
    'hash'         => '<sha256-hash-of-previous-record>',
]);

$cancellation->setChaining($chaining);
10
You do not calculate the cancellation’s own hash. Verifactu::cancelInvoice() calls HashGeneratorService::generate() internally before transmitting the SOAP request.
11
Set Up ComputerSystem
12
Attach the same ComputerSystem block used for invoice submissions. It identifies the invoicing software and its registered provider.
13
use eseperio\verifactu\models\ComputerSystem;
use eseperio\verifactu\models\LegalPerson;
use eseperio\verifactu\models\enums\YesNoType;

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

$computerSystem = new ComputerSystem();
$computerSystem->systemName             = 'ERP Company';
$computerSystem->version                = '1.0';
$computerSystem->providerName           = 'Software Provider SL';
$computerSystem->systemId               = '01';
$computerSystem->installationNumber     = '1';
$computerSystem->onlyVerifactu          = YesNoType::YES;
$computerSystem->multipleObligations    = YesNoType::NO;
$computerSystem->hasMultipleObligations = YesNoType::NO;
$computerSystem->setProviderId($provider);

$cancellation->setSystemInfo($computerSystem);
14
Set the Record Timestamp
15
$cancellation->recordTimestamp = '2024-07-01T14:30:00+02:00';  // ISO 8601 with UTC offset
16
Set Optional Fields
17
PropertyTypeDescriptionnoPreviousRecordYesNoTypeYES when no previous registration exists for this invoice in AEATpreviousRejectionYesNoTypeYES when cancelling a record that was previously rejectedgeneratorGeneratorTypeWho generated this cancellation (see table below)generatorDataLegalPerson (via setGeneratorData())Identity of the generator when it is not the issuerfechaFinVeriFactustring (DD-MM-YYYY)Optional — signals voluntary end of Verifactu submissions
18
GeneratorType values:
19
CaseValueMeaningISSUEREThe obligated issuer generated the cancellationRECIPIENTDThe invoice recipient generated itTHIRD_PARTYTA third party generated it
20
use eseperio\verifactu\models\enums\GeneratorType;

$cancellation->noPreviousRecord  = YesNoType::NO;
$cancellation->previousRejection = YesNoType::NO;
$cancellation->generator         = GeneratorType::ISSUER;

// Only required when generator is RECIPIENT or THIRD_PARTY
// $cancellation->setGeneratorData($generatorLegalPerson);

Validate and Submit

use eseperio\verifactu\Verifactu;
use eseperio\verifactu\models\InvoiceResponse;

// Validate first
$validationResult = $cancellation->validate();
if ($validationResult !== true) {
    foreach ($validationResult as $property => $errors) {
        echo "Error in '$property': " . implode(', ', $errors) . "\n";
    }
    exit;
}

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

Handle the InvoiceResponse

The cancelInvoice() method returns the same InvoiceResponse type as registerInvoice().
if ($response->submissionStatus === InvoiceResponse::STATUS_OK) {
    echo "Cancellation accepted. AEAT CSV: " . $response->csv . "\n";
} else {
    foreach ($response->getErrors() as $code => $description) {
        echo "[$code] $description\n";
    }
}

Complete Working Example

<?php

use eseperio\verifactu\Verifactu;
use eseperio\verifactu\models\InvoiceCancellation;
use eseperio\verifactu\models\InvoiceId;
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\YesNoType;
use eseperio\verifactu\models\enums\GeneratorType;

// Configure once at application bootstrap
Verifactu::config(
    '/path/to/certificate.pfx',
    'certificate-password',
    Verifactu::TYPE_CERTIFICATE,
    Verifactu::ENVIRONMENT_SANDBOX
);

// Invoice ID — must match the original submission exactly
$invoiceId = new InvoiceId();
$invoiceId->issuerNif    = 'B12345678';
$invoiceId->seriesNumber = 'FA2024/001';
$invoiceId->issueDate    = '2024-07-01';

$cancellation = new InvoiceCancellation();
$cancellation->setInvoiceId($invoiceId);
$cancellation->issuerName      = 'Empresa Ejemplo SL';
$cancellation->recordTimestamp = '2024-07-01T14:30:00+02:00';

// Chain to the previous record
$chaining = new Chaining();
$chaining->setPreviousInvoice([
    'issuerNif'    => 'B12345678',
    'seriesNumber' => 'FA2024/001',
    'issueDate'    => '2024-07-01',
    'hash'         => '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
]);
$cancellation->setChaining($chaining);

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

$computerSystem = new ComputerSystem();
$computerSystem->systemName             = 'ERP Company';
$computerSystem->version                = '1.0';
$computerSystem->providerName           = 'Software Provider SL';
$computerSystem->systemId               = '01';
$computerSystem->installationNumber     = '1';
$computerSystem->onlyVerifactu          = YesNoType::YES;
$computerSystem->multipleObligations    = YesNoType::NO;
$computerSystem->hasMultipleObligations = YesNoType::NO;
$computerSystem->setProviderId($provider);
$cancellation->setSystemInfo($computerSystem);

// Optional fields
$cancellation->noPreviousRecord  = YesNoType::NO;
$cancellation->previousRejection = YesNoType::NO;
$cancellation->generator         = GeneratorType::ISSUER;

// Validate
$validationResult = $cancellation->validate();
if ($validationResult !== true) {
    print_r($validationResult);
    exit;
}

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

if ($response->submissionStatus === InvoiceResponse::STATUS_OK) {
    echo "Cancellation accepted. AEAT CSV: " . $response->csv . "\n";
} else {
    foreach ($response->getErrors() as $code => $description) {
        echo "[$code] $description\n";
    }
}

Exception Handling

try {
    $response = Verifactu::cancelInvoice($cancellation);
} catch (\InvalidArgumentException $e) {
    // Validation failed before the SOAP call
    echo "Validation error: " . $e->getMessage();
} catch (\RuntimeException $e) {
    // SOAP communication error
    echo "Network/SOAP error: " . $e->getMessage();
} catch (\Exception $e) {
    echo "Unexpected error: " . $e->getMessage();
}

Build docs developers (and LLMs) love