Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/eclipxe13/CfdiUtils/llms.txt

Use this file to discover all available pages before exploring further.

CfdiUtils provides a comprehensive validation system that checks your CFDI documents at multiple layers. When you validate a CFDI, the library doesn’t simply tell you whether it passed or failed — it runs every registered rule and records a result for each one, whether that result is a success, a failure, a warning, or a check that could not be performed. This granular reporting lets you pinpoint exactly which rules are satisfied and which are not, making debugging and compliance auditing significantly easier. The validation pipeline works in three layers:
  1. Schema validation — the document is checked against the official SAT XSD schemas declared in xsi:schemaLocation. If the document fails schema validation, further validation stops because the data cannot be trusted.
  2. XML structure checks — for CFDI 4.0, additional rules verify the correct namespace, root node name, and version number.
  3. Business rule validation — 80+ SAT-defined rules covering monetary precision, RFC formats, tax totals, digital seals, Timbre Fiscal Digital integrity, and more.
All validation messages produced by the library are in Spanish, matching the SAT’s own terminology.

Validation Architecture

The validation system is built from four cooperating classes in the CfdiUtils\Validate namespace.

MultiValidator

MultiValidator is the orchestrator. It holds an ordered list of ValidatorInterface objects and executes them one by one against the cfdi:Comprobante node. Each validator writes its results into the shared Asserts collection. If any validator sets the mustStop flag, the loop ends immediately and no subsequent validators run. You normally obtain a properly configured MultiValidator through MultiValidatorFactory rather than building one by hand.
// MultiValidator runs all registered validators in sequence
public function validate(NodeInterface $comprobante, Asserts $asserts): void;

// Check whether this multi-validator handles a given CFDI version
public function canValidateCfdiVersion(string $version): bool;

MultiValidatorFactory

MultiValidatorFactory creates pre-configured MultiValidator instances for each supported CFDI version and use case. It uses a Discoverer to automatically load all validator classes found in the Cfdi33/Standard, Cfdi33/RecepcionPagos, and Cfdi40/Standard directories.
Factory methodPurpose
newCreated33()Validators for a CFDI 3.3 document being created
newReceived33()Validators for a CFDI 3.3 document received (same set as created)
newCreated40()Validators for a CFDI 4.0 document being created
newReceived40()Validators for a CFDI 4.0 document received (same set as created)

Assert

An Assert represents a single validation rule result. It is phrased as an affirmative statement — for example, “The CFDI has a defined currency that belongs to the currency catalogue” — not as an error message. This makes every result self-documenting regardless of whether it passed or failed. Each Assert carries four properties:
PropertyAccessorDescription
codegetCode()Short identifier such as MONDEC01
titlegetTitle()Human-readable description of the rule
statusgetStatus()The outcome: OK, ERROR, WARN, or NONE
explanationgetExplanation()Optional detail about why the rule failed or was skipped

Asserts

Asserts is a keyed collection of Assert objects. Each code is unique within the collection — if a validator writes a result for a code that already exists, the previous entry is overwritten. The collection is iterable, countable, and provides helper methods to filter by status.
$asserts->hasErrors();    // true if any assert has status ERROR
$asserts->hasWarnings();  // true if any assert has status WARN
$asserts->errors();       // array of Assert objects with status ERROR
$asserts->warnings();     // array of Assert objects with status WARN
$asserts->oks();          // array of Assert objects with status OK
$asserts->nones();        // array of Assert objects with status NONE

Validation Statuses

Every Assert carries a Status value object. Status is immutable — once created, its value cannot be changed. The four possible values are:
StatusString valueMeaning
Status::ok()OKThe rule was checked and the document passed.
Status::error()ERRORThe rule was checked and the document failed. The CFDI is invalid and should be rejected.
Status::warn()WARNA potential issue was found, but it is unclear whether the CFDI should be rejected. Human review is recommended.
Status::none()NONEThe rule could not be evaluated (e.g., a required complemento was absent, or the currency is not one of the supported ones).
NONE does not mean the document is invalid. It means the specific condition required for that rule to apply was not met. For example, decimal-precision rules for MXN, USD, EUR, and XXX return NONE for any other currency because the SAT does not define decimal limits for it.

Validating a Created CFDI

When you build a CFDI using CfdiCreator33 or CfdiCreator40, the creator object already holds all the data needed for validation. You can call validate() directly on the creator instance — it internally constructs a MultiValidator using MultiValidatorFactory and runs it against the node tree you have built.
<?php
use CfdiUtils\CfdiCreator40;

/** @var CfdiCreator40 $creator */
$asserts = $creator->validate();

if ($asserts->hasErrors()) {
    echo "Validation failed with the following errors:" . PHP_EOL;
    foreach ($asserts->errors() as $error) {
        echo vsprintf('%-10s %-8s %s => %s', [
            $error->getCode(),
            $error->getStatus(),
            $error->getTitle(),
            $error->getExplanation(),
        ]) . PHP_EOL;
    }
} else {
    echo "CFDI passed all validation rules." . PHP_EOL;
}
The same pattern applies for CFDI 3.3 — simply replace CfdiCreator40 with CfdiCreator33.

Validating a Received CFDI

For CFDIs received from a third party or downloaded from the SAT portal, use CfdiValidator40 or CfdiValidator33. Pass the raw XML string to validateXml().
<?php
use CfdiUtils\CfdiValidator40;

$xmlContent = file_get_contents('/path/to/received-cfdi.xml');

$validator = new CfdiValidator40();
$asserts = $validator->validateXml($xmlContent);

if ($asserts->hasErrors()) {
    foreach ($asserts->errors() as $error) {
        echo vsprintf('%-10s %-8s %s => %s', [
            $error->getCode(),
            $error->getStatus(),
            $error->getTitle(),
            $error->getExplanation(),
        ]) . PHP_EOL;
    }
}
For CFDI 3.3:
<?php
use CfdiUtils\CfdiValidator33;

$xmlContent = file_get_contents('/path/to/received-cfdi33.xml');

$validator = new CfdiValidator33();
$asserts = $validator->validateXml($xmlContent);

if ($asserts->hasErrors()) {
    foreach ($asserts->errors() as $error) {
        echo vsprintf('%-10s %-8s %s => %s', [
            $error->getCode(),
            $error->getStatus(),
            $error->getTitle(),
            $error->getExplanation(),
        ]) . PHP_EOL;
    }
}
Both CfdiValidator33 and CfdiValidator40 contain an XmlResolver. If the resolver is removed, some validators (such as XmlFollowSchema and TimbreFiscalDigitalSello) will either skip their checks or attempt to fetch XSD schemas and SAT certificates directly from the internet. By default a new XmlResolver is created automatically, but you can supply a custom one via the constructor or setXmlResolver().If the XML may contain whitespace or encoding issues, clean it first: CfdiUtils\Cleaner\Cleaner::staticClean($xmlContent).

Working with Asserts

After validation you have a full Asserts collection. You can iterate every result, filter by status, or look up a specific rule by its code.
<?php
/** @var \CfdiUtils\Validate\Asserts $asserts */

// Iterate every assert regardless of status
foreach ($asserts as $assert) {
    echo vsprintf('%-10s %-8s %s => %s', [
        $assert->getCode(),
        $assert->getStatus(),
        $assert->getTitle(),
        $assert->getExplanation(),
    ]) . PHP_EOL;
}

// Check for errors
if ($asserts->hasErrors()) {
    echo count($asserts->errors()) . ' error(s) found.' . PHP_EOL;
}

// Check for warnings
if ($asserts->hasWarnings()) {
    echo count($asserts->warnings()) . ' warning(s) found.' . PHP_EOL;
}

// Look up a specific rule by code
if ($asserts->exists('MONDEC01')) {
    $assert = $asserts->get('MONDEC01');
    echo $assert->getStatus() . ': ' . $assert->getTitle() . PHP_EOL;
}

// Cast an assert to a string (format: "STATUS: CODE - Title")
foreach ($asserts as $assert) {
    echo $assert . PHP_EOL;
}

The mustStop Behavior

The XmlFollowSchema validator is special: if the XML document does not conform to its declared XSD schemas, the validator sets the mustStop flag on the Asserts collection. When MultiValidator detects this flag after running a validator, it immediately aborts the remaining validators. This behavior exists because all subsequent business-rule validators rely on the assumption that the document structure is valid. If the schema check fails, the data cannot be trusted and running further checks would produce meaningless or misleading results.
If XmlFollowSchema (code XSD01) returns an error or the mustStop flag is set, your Asserts collection will contain far fewer entries than a full validation run. Address schema errors first before interpreting the rest of the results.
<?php
/** @var \CfdiUtils\Validate\Asserts $asserts */

if ($asserts->mustStop()) {
    echo 'Validation stopped early — fix schema errors before re-validating.' . PHP_EOL;
}

if ($asserts->exists('XSD01')) {
    $xsd = $asserts->get('XSD01');
    if ($xsd->getStatus()->isError()) {
        echo 'Schema error: ' . $xsd->getExplanation() . PHP_EOL;
    }
}

See Also

Standard Validation Rules

Reference for all CFDI 3.3 standard validators: XSD schema, decimal precision, impuestos, sello digital, and more.

CFDI 4.0 Validation Rules

CFDI 4.0-specific validators, differences from 3.3, and new validation codes.

Build docs developers (and LLMs) love