
Validate CFDI XML against the SAT schema (XSD)
September 12, 2026
Validating the XML of a CFDI (Mexico's electronic invoice) you received isn't a matter of opening the file and seeing that it has data. CFDI XML schema validation checks that the document conforms to the schema the SAT (Mexico's tax authority, Servicio de Administración Tributaria) defined for its version. That schema is in Anexo 20 (the SAT technical standard that defines the CFDI: its XML structure, catalogs, and validation rules). Validation also covers the schemas of every complement (complemento, an XML extension the SAT defines for specific operations) that the CFDI itself declares.
The difference matters because an XML file can parse without errors in your favorite language and still be invalid. xml.etree tells you whether the document is well-formed. It tells you nothing about whether Exportacion is present, whether ClaveProdServ exists in the SAT catalog, or whether the nodes come in the order the schema requires.
That's the job of structural validation, and it's the first of the checks you run on a supplier invoice. The first, not the only one.
What CFDI XML schema validation means
Validating a CFDI's XML means verifying that the document meets two conditions: it's well-formed according to the XML specification, and it's valid against the Anexo 20 XSDs for its version plus the XSDs of the complements declared in schemaLocation. Any parser checks the first condition. The second one requires the SAT's schemas.
Well-formed and valid are different things. A well-formed XML has its tags closed, its characters escaped, and a single root. A valid XML also honors a contract, the CFDI XML schema, which sets the required attributes, the values each one accepts, and the order of the child nodes.
The forged CFDIs I've seen almost always fail right here. They don't fail on the digital seal (the issuer's signature over the CFDI, made with its CSD), which forgers don't even bother to fake properly. They fail on schema details someone overlooked while copying the structure of a real CFDI.
The hierarchy the CFDI 4.0 XSD requires
The Anexo 20 XSD defines the children of cfdi:Comprobante as a sequence. Sequence means fixed order: if your supplier sent the Addenda (commercial data a recipient asks for, outside the SAT's rules; not a complement) before the Complemento, the document is invalid even though both nodes exist and have the right content.
- cfdi:InformacionGlobal (optional)
- cfdi:CfdiRelacionados (optional, repeatable)
- cfdi:Emisor (required)
- cfdi:Receptor (required)
- cfdi:Concepto (1 or more)
- cfdi:Impuestos (optional)
- tfd:TimbreFiscalDigital
- cfdi:Addenda (optional)
On a CFDI you receive, cfdi:Complemento is never optional in practice: if it doesn't carry tfd:TimbreFiscalDigital, what you have is an unstamped CFDI (stamping: the certification by a SAT-authorized provider that makes a CFDI valid), in other words a draft. One of those reaches accounts payable more often than you'd think.
What structural validation checks
| What gets checked | Example failure |
|---|---|
| Required attributes are present | Exportacion is missing from a CFDI 4.0 |
| Values are in the SAT catalog | FormaPago set to 99 when the SAT catalog doesn't allow it for that case |
| Patterns and lengths | An 11-character Rfc, a UUID outside the 8-4-4-4-12 format |
| Data types and decimals | Total with four decimals in a currency that allows two |
| Order of the child nodes | Addenda placed before Complemento |
| Declared namespaces | The cfdi prefix pointing to cfd/3 on a CFDI marked as 4.0 |
| Complement schemas | The namespace of Carta Porte (the CFDI complement that documents the transport of goods in Mexico) is declared without its XSD in schemaLocation |
This table is why structural validation pays off even when you run the other checks. It catches the broken CFDI before it enters your ERP and creates an accounting entry that someone later has to reverse.
The CFDI schemaLocation decides what the document is validated against
The xsi:schemaLocation attribute holds pairs of values separated by whitespace: first a namespace, then the URL of the XSD that defines it. A CFDI 4.0 with a payment complement (the complement that records each payment received against a PPD invoice; the type P CFDI that carries it is also called REP) declares at least two pairs.
Here's the detail a lot of people miss: cfdi:Complemento accepts any element, but under the CFDI 4.0 XSD each one must match a schema the validator can find. If the issuer declares a complement's namespace without its XSD in schemaLocation, a validator that applies the XSD rejects the CFDI. A validator set to skip unknown content lets the complement through unchecked. That explains why a "valid" CFDI can carry a payment complement with data that meets none of the rules.
schemaLocation pairs are separated by whitespace, and a line break between the namespace and its URL is as valid as a space. If your code splits the attribute with a plain split(" "), an XML formatted with line breaks gives you misaligned pairs. Split on any run of whitespace.
The errors that break parsing
These are the ones that keep coming back. None of them is exotic, and every one of them has reached production at some point.
| Symptom | Root cause | What to do |
|---|---|---|
| The parser fails at character 1 | A UTF-8 BOM at the start of the file | Strip the BOM before parsing, or open the file with utf-8-sig |
| Accented characters turn into garbled symbols | The file is UTF-8, as its declaration says, but your code decodes it as ISO-8859-1 | Follow the encoding in the XML declaration, not the system default |
| Undefined entity error | An unescaped & in a trade name | The issuer must escape it as &; it isn't fixed on the recipient's side |
| Valid against the XSD, but the seal doesn't verify | Double or leading spaces in an attribute | See the note below |
| Complement rejected as undeclared | Namespace declared without its XSD in schemaLocation | schemaLocation must declare the XSD of every complement |
| Rejected for its version | Version="4.0" with the cfd/3 namespace | The namespace wins, not the attribute |
The fourth case deserves an explanation, because it's the most baffling one the first time you see it. The Anexo 20 data types use whiteSpace="collapse", which means the XML processor collapses consecutive spaces and trims the ones at the ends before comparing. The original string (cadena original, the sequence of CFDI data that the digital seal signs) that the seal is computed from is built with the values already collapsed. So if the XML carries RAMIREZ LOPEZ with two spaces, the schema accepts it, but the seal has to have been computed over RAMIREZ LOPEZ with one. If the issuing system sealed the raw string, the CFDI is structurally valid and cryptographically invalid at the same time.
This case is real. It shows up whenever someone builds the original string by concatenating strings instead of applying the XSLT transformation the SAT publishes.
A valid structure doesn't mean the invoice has tax effect
This is where most guides stop, and where the real risk begins.
An XML can comply 100% with Anexo 20 and still not support a deduction. Complying with the schema says the document is well built. It doesn't say who signed it, whether that signature verifies, whether the certificate was current on the issue date, whether the CFDI is still active (vigente: not canceled with the SAT) today, or whether the issuer is listed as an EFOS (Empresas que Facturan Operaciones Simuladas, taxpayers that issue invoices for simulated transactions).
The order in the diagram isn't decorative. Structural validation goes first because it's the cheapest check and because, if it fails, the others are meaningless: you can't verify the seal of a document whose original string can't be built. The lists go last because they don't depend on the XML, only on the issuer's RFC (Mexico's taxpayer ID, Registro Federal de Contribuyentes). Those lists are Lista 69-B (the SAT list of taxpayers presumed to issue invoices for nonexistent transactions, under Article 69-B of the Federal Tax Code) and Lista 69-B Bis (the SAT list of taxpayers involved in a presumed improper transfer of the right to offset tax losses, under Article 69-B Bis of the Federal Tax Code).
The check most people skip is the SAT status. A canceled CFDI keeps its structure intact and its seal still verifies: cancellation doesn't touch the XML. The only way to find out is to query the SAT's service, and you have to query it again before you close the books, not only when you receive the CFDI.
As for the SAT's Article 69-B lists, the order has a practical consequence: a CFDI that passes the five technical checks can still have no tax effect if the issuer is listed as definitive. I cover that in detail in the guide to Lista 69-B and how to screen suppliers.
How to automate CFDI XML validation
Keeping the XSDs of Anexo 20 and of every complement up to date is recurring work that adds nothing to your product. Schemas change, complements get new versions, and the day the SAT publishes a revision, your validator starts rejecting legitimate CFDIs. Always at month-end close.
Fiscalapi's CFDI validation API runs the structural check against the SAT's official service, so the schema it validates against is always the current one. You send the XML in base64, select the criteria you need, and get a verdict per criterion with the description of the status behind it, ready to file alongside the CFDI.
Validate your first XML in the test environment
Anexo 20 structure, issuer's digital seal, certificate validity, SAT seal (the seal inside the digital tax stamp that certifies the CFDI), CFDI status, and the SAT's Article 69-B lists in a single call. You select the criteria you need and pay only for those.
The response contract and the official SDKs for .NET, TypeScript, Python, PHP, and Java are in the documentation. If you want to send a real XML before you write a line of integration code, signing up for the test environment is free.
Frequently asked questions
If you're going to build your own validator, start with the XSLT transformation for the original string that the SAT publishes instead of concatenating attributes by hand. That's the decision that separates a validator that works from one that works until a name with two spaces shows up.
CFDI validation by API
Structure, seals, certificate, status, and the SAT's Article 69-B lists in a single endpoint.
Anexo 20
The specification that defines the schema the XML is validated against. Spanish-language guide.
Lista 69-B
The check that doesn't depend on the XML and still leaves the CFDI with no tax effect.