Guides
CFDI and e-invoicing, for developers
A payment is not finished in Mexico until there is a valid CFDI behind it. This is the fiscal layer most international stacks ignore — SAT, PACs, complementos and catálogos — written for the engineer who has to make the factura come out automatically and correctly.
CFDI is where "accept a payment" turns into "produce a legal document the tax authority will accept." The good news is you never talk to the SAT directly — you integrate a PAC (authorized certifier) that stamps your XML. So these guides treat the PAC as a swappable dependency: pick by SDK quality and documentation, build to the CFDI shape, and stay vendor-neutral so a price change or outage does not strand you.
The durable patterns are the same across every complemento. Store the stamped, validated XML indexed by its UUID and reconcile it back to the payment id, so every charge has a provable factura attached — that link is what saves you in an audit. And treat the SAT's catálogos as data you refresh on a schedule, not constants you hardcode: they change, and a stale catalog is the most common reason a stamp gets rejected. From standard CFDI 4.0 to Carta Porte, hydrocarbons and the Banxico rules next door, that backbone holds.
The guides

Auto-Invoice CFDI 4.0 the Moment a Payment Lands: A Developer's Guide to Stripe & Mercado Pago in Mexico (2026)
A vendor-neutral, code-first guide to auto-stamping a CFDI 4.0 from a verified Stripe or Mercado Pago webhook in Mexico: PAC, CSD, RFC, PUE vs PPD.
Read →
How to Stamp a CFDI de Nómina 4.0 (timbrado) in 2026
Engineer's how-to for stamping a CFDI de Nómina 4.0 with complemento 1.2 Revisión E via a PAC API in 2026. Real XML, the OtrosPagos subsidio fix, Rev E validations, and a pre-timbrado checklist.
Read →
CFDI Timbrado Outbox: Never Lose or Double-Stamp an Invoice When Your PAC Fails
Stripe charges, you stamp the CFDI at your PAC, then write the UUID to your DB. Crash between those two writes and you orphan a stamp you paid for — or a naive retry changes the Fecha and mints a duplicate UUID. Fix it with a frozen comprobante, a transactional outbox, and PAC idempotency. ES-first, code-heavy.
Read →
Bulk-download your CFDI from the SAT web service with your e.firma, in Python
Engineer's how-to for the SAT Descarga Masiva web service: bulk-pull your issued or received CFDI (or metadata) straight from the SAT with your e.firma (FIEL) — no PAC, no portal scraping. The 4-step SOAP state machine (Autenticacion, SolicitaDescarga, VerificaSolicitud, Descarga), the WS-Security signing (SHA1 digest, RSA-SHA1, BinarySecurityToken, Timestamp), the real v1.5 limits, and Python code. Spanish-first.
Read →
Why Your SAT Descarga Masiva Sits at "EnProceso" — and Why the 2,000-CFDI/Day Cap Is a Myth
Your SolicitaDescarga returns EnProceso because SAT processes it asynchronously — you poll VerificaSolicitud until Terminada, then download the packages within 72h. This ES-first, engineer's guide models the request as a state machine (EnProceso/Terminada/Rechazada/Vencida/Error), debunks the "2,000 CFDI/day cap" myth with the real v1.5 limits (200k records/request, 1M metadata, 72h availability, don't-download-an-XML-twice), and shows how to split big date ranges into windows with idempotent, resumable jobs. Python/pseudocode.
Read →
'Solicitud Aceptada' but nothing downloads: the SAT Descarga Masiva CodEstatus & EstadoSolicitud reference (5000, 5002, 5005)
Your SolicitaDescarga returns 'Solicitud Aceptada' (CodEstatus 5000) and you've waited 48 hours: that code is just the acknowledgment, not a signal the package is ready. To download it you must poll VerificaSolicitud with the IdSolicitud and read EstadoSolicitud (1 Aceptada, 2 EnProceso, 3 Terminada, 4 Error, 5 Rechazada, 6 Vencida) — only Terminada yields the IdsPaquetes. This ES-first engineer's reference decodes every CodEstatus: auth/validation (300 usuario no válido, 301 XML mal formado, 302/303 sello, 304/305 certificado) and request (5002 se agotaron las solicitudes de por vida, 5003 tope máximo, 5004 sin información, 5005 duplicada, 5008 máximo 2 descargas por paquete, 5011 límite por folio/día, 404 error no controlado), each with the concrete fix, the two traps devs hit most (5005 and 5002), a 30-second debugging checklist, and the phpcfdi isFinished()/isRejected()/isExpired() and python-satcfdi helpers. Python.
Read →
SAT bulk-download in production: a pipeline to pull CFDI for many RFCs (accounting firm), in Python
An accounting firm doesn't bulk-download CFDI for one RFC on a happy-path loop — it pulls dozens of clients, issued and received, metadata and CFDI. This Spanish-first engineering guide models every SAT query as a durable job with a state machine: SolicitaDescarga once, IdSolicitud persisted, VerificaSolicitud with backoff, so you never hit 5005 duplicada or 5002 (lifetime request cap). Incremental sync with a per-RFC watermark, UUID dedup, the 72h window and 5008, and how to spread load across many RFCs. Python job schema and poller. Engineer, not accountant.
Read →
The SAT portal shows your CFDI but your Descarga Masiva returns an empty package: diagnose it with NumeroCFDIs, in Python
The SAT portal shows CFDI for a date range, yet your Descarga Masiva web service returns an empty package (empty ZIP) or 'never finishes' — a documented, recurring issue (python-satcfdi #49: portal has records but VerificaSolicitud returns NumeroCFDIs 0, IdsPaquetes []). This ES-first engineer's guide splits the problem in 30 seconds with one value: NumeroCFDIs in VerificaSolicitud. If it's 0, your WS query is not the portal query — rfcSolicitante must match RfcEmisor (emitidas/issued) or RfcReceptor (recibidas/received); asking the wrong direction returns 0 silently, and date-only or TZ-shifted ranges exclude every record (send full-day T00:00:00/T23:59:59 in Mexico central time). If the count matches but the ZIP is empty, you saved the base64 string instead of base64.b64decode-ing the Paquete to bytes, and you must only call Descarga on EstadoSolicitud 3 (Terminada). Includes the 30-second test (replicate the exact portal query, request Metadata) and why a symptom shared across identical GitHub issues is the shared mistake, not a library bug. Python. Engineer, not contador.
Read →
How to read a SAT bulk-download package: base64 to ZIP, the tilde-delimited Metadata TXT, and CFDI XML, in Python
Every SAT bulk-download tutorial ends when the package arrives — none tell you how to actually read it. The Descarga `Paquete` comes base64-encoded: decode it and unzip it as a ZIP (in memory). A Metadata request yields a `<UUID>-N.txt` file that is TILDE (`~`) delimited, with a header row and 12 columns (Uuid, RfcEmisor/Receptor, NombreEmisor/Receptor, RfcPac, FechaEmision, FechaCertificacionSat, Monto, EfectoComprobante I/E/N/P/T, Estatus Vigente/Cancelado, FechaCancelacion); a CFDI request yields the signed XML comprobantes instead. The trap (phpcfdi #23): NombreEmisor/NombreReceptor can contain the `~` itself, plus CR/LF and quotes, so a naive `line.split("~")` misaligns the columns — parse defensively with a max-split or a tolerant CSV reader (`delimiter="~"`) and normalize. Engineer's how-to in Python: use Metadata to scope, CFDI to validate.
Read →
Validate a CFDI You Received Before You Deduct It: XSD, Sello, Vigencia and the 69-B Blacklist, in Python
Every CFDI tutorial teaches you to emit. None teach you to trust what lands in accounts payable. Before you deduct a received CFDI, four checks have to pass: the XML validates against the CFDI 4.0 XSD, the sello is a real RSA-SHA256 signature over the cadena original, the SAT says Vigente (not Cancelado), and the emisor RFC is not Definitivo on the 69-B blacklist. Engineer's how-to in Python, with the total-as-string gotcha that silently breaks vigencia checks and the AP gate that rejects or holds on any failure.
Read →
Automate Accounts Payable in Mexico: Extract Supplier Invoices with Claude, Reconcile Against Received CFDI, and Monitor 69-B (Python, 2026)
A code-first AP pipeline for Mexico: use Claude Structured Outputs to extract typed fields from non-CFDI PDFs and tickets, then reconcile deterministically in Python against the received CFDI XML on UUID, RFC and total — plus a scheduled 69-B monitor.
Read →
Validate an RFC Without the Constancia de Situación Fiscal
Engineer's tutorial to validate a customer's tax identity at signup without the Constancia de Situación Fiscal. Post SAT Comunicado 04/2026, demanding the CSF to invoice is a finable infracción. Validate RFC format + check digit locally, collect CP/régimen/uso via dropdowns, and verify via a PAC web service.
Read →
Automate the Factura Global Inside the 24-Hour SAT Window: The Daily Job the PAC Blogs Skip
Sell to público en general? You aggregate sales into one CFDI global (regla 2.7.1.21 RMF 2026) and timbrar it within 24h of the close. Here's the daily job.
Read →
Mexico's 2026 Marketplace Tax Withholding: An Engineer's Guide to Retenciones (ISR + IVA) and the Retención CFDI
From Jan 1 2026, if your Mexican marketplace pays third-party sellers you may be the retenedor: withhold ISR 2.5% + IVA 8%, net it, issue the CFDI.
Read →
Don't Cancel the CFDI — Issue a Nota de Crédito: Automating the CFDI de Egreso for Stripe & Mercado Pago Refunds
When you refund a payment in Mexico you don't cancel the original CFDI — you issue a CFDI de Egreso (nota de crédito) related to it. A builder's guide to automating it.
Read →
Automate the Complemento de Pago (REP) from Stripe & Mercado Pago — Plus the RMF 2026 Cancellation Trap
When you invoice as PPD in Mexico the payment webhook is your fiscal trigger. Wire Stripe & Mercado Pago to stamp the REP — plus the RMF 2026 cancellation trap.
Read →
The June 2026 CFDI Catalog Update (c_NumPedimentoAduana): Your Code Doesn't Change — But Your Pipeline Must
On June 19, 2026 the SAT added mandatory entries to the CFDI 4.0 catalog c_NumPedimentoAduana. The XSD didn't change — so your code doesn't, but your pipeline must.
Read →
Carta Porte 3.1: A Developer's Guide to Stamping the SAT Freight Complement (Mexico, 2026)
A vendor-neutral, code-first guide to issuing a Carta Porte 3.1 in Mexico: CFDI de Traslado vs Ingreso, the four complement blocks, PAC stamping, and 2026 gotchas.
Read →
CFDI Hydrocarbons Complement: Stamp Fuel Invoices Without SAT Rejecting Them (April 2026)
Engineer how-to for the SAT Complemento de Hidrocarburos y Petrolíferos, in force April 24, 2026: 5 attributes, L_CNE permit check, fines $17k-97k.
Read →
E-invoicing Across LATAM in 2026: The Developer's Map (MX, AR, BR, CO, CL)
A developer's orientation map to e-invoicing across Mexico, Argentina, Brazil, Colombia and Chile: authority, format, clearance model and the 2026 changes you must ship before.
Read →
Banxico's Nivel 2 Bis + the 4-Step Transfer Rule: What Your Checkout and Cobros App Must Change Before Dec 14, 2026
Banxico's mid-2026 rules add a no-RFC Nivel 2 Bis account for micro-merchants and cap mobile transfers at 4 steps. Engineer's guide to onboarding + checkout.
Read →
Work with me on this
FAQ
- Do I integrate with the SAT directly to issue a CFDI?
- No. You integrate an authorized PAC (Proveedor Autorizado de Certificación), which validates and stamps (timbra) your CFDI XML on the SAT's behalf and returns the UUID. Your job is to build a correct CFDI 4.0 document and send it to the PAC — choose the PAC by SDK and docs quality, and keep your code vendor-neutral so you can switch.
- How should I store CFDIs so an audit is painless?
- Store the full stamped XML (not just a PDF) indexed by its UUID, and link every CFDI to the payment id that produced it. Keep the SAT-required retention period in mind. The reconciliation — charge ↔ UUID ↔ order — is the thing auditors ask for, so build it from day one rather than reconstructing it later.
- Do I need a complemento like Carta Porte or Hidrocarburos?
- Only if your operation triggers it. Carta Porte is required when goods move on public roads/rails/water/air; the Hidrocarburos complement applies to fuel-sector transactions. They are add-ons to the base CFDI with their own catálogos and validation rules — build the base CFDI cleanly first, then attach the complemento your business actually needs.