
Carta Porte 3.1: A Developer's Guide to Stamping the SAT Freight Complement (Mexico, 2026)
A Carta Porte 3.1 is a SAT CFDI complement required to move goods in Mexico. Issue a CFDI de Traslado (tipo T) for your own goods, or a CFDI de Ingreso with the complement when a carrier charges freight. You don't build the XML by hand: send the business data (Ubicaciones, Mercancías, Autotransporte, Figura del Transporte) to a PAC API, it timbra, and you store the stamped XML.
Why does moving a pallet in Mexico need a CFDI?
If you move merchandise inside Mexico — a pallet of inventory between your own warehouses, or a truckload a carrier hauls for you — the SAT wants a document riding with that freight. That document is the Carta Porte 3.1, a complemento (a complement) of the CFDI: extra structured data that bolts onto a regular CFDI and describes the shipment. It is not a separate file you build by hand; it is issued as a CFDI and timbrado (stamped) by a PAC, exactly like a normal factura.
If you’ve already wired up auto-invoicing CFDIs from Stripe or Mercado Pago, this is the freight sibling of that flow: same PAC, same timbrado, same “store the stamped XML” discipline — just a different complement on top.
Version 3.1 is the only one in force. The SAT retired 3.0 (and the older 2.0), so PACs reject anything older outright. And the stakes are real: depending on the infraction, fines run roughly $400–$17,030 MXN for failing to issue or accompany the transport document, and an emisión without required requisitos/complementos sits in a higher band (Anexo 5 of the current RMF — confirm the current amounts). The bigger exposure isn’t even a fixed peso figure: moving goods without a valid Carta Porte can trigger a presunción de contrabando — a criminal matter, plus the goods in transit are exposed at a checkpoint.
One more trap before you write a line of code: the SAT refreshed the Carta Porte catálogos in January 2026 (the substantive update landed 13/01/2026, expanding the pedimentos relationships for the current ejercicio). Stale catalog keys get timbrado rejected, and current-year operations — pedimentos del ejercicio, for instance — require the 2026 keys. Confirm the current catálogo set against the SAT, since these refresh.
Honest framing up front: I’m an engineer, not a contador (an accountant). I’ll show you the integration pattern that ships. For fiscal edge cases — material peligroso, pedimentos, weird ownership structures, the exact fine that applies — loop in your contador and confirm the current SAT rules.
Traslado (tipo T) or Ingreso? Which CFDI you actually issue
The first decision is which CFDI the complement rides on. There are exactly two shapes, and the rule is simple once you ask the right question: is someone charging for the freight?
- No — I’m moving my own goods. You issue a CFDI de Traslado, tipo de comprobante
"T". The merchandise owner is the emisor (issuer), there’s no sale, and the value is typically zero. If you’re a retailer or manufacturer moving inventory between your own locations, this is you, and you issue it yourself. - Yes — a carrier charges for the haul. The transportista (carrier) issues a CFDI de Ingreso with the Carta Porte complement. The carrier is the emisor and invoices the goods owner as receptor. If you outsource freight, the carrier issues this; you just receive it.
The complement itself is the same either way. What changes is the CFDI it rides on (T vs I) and who issues it.
My opinion after shipping these: if you’re the shipper moving your own inventory, default to issuing the CFDI de Traslado tipo T yourself — it’s fully under your control. The moment money changes hands for the freight, it’s not your document to issue; it’s the carrier’s CFDI de Ingreso and you’re the receptor.
CFDI de Traslado (tipo T)
- You move your OWN goods, no sale
- Emisor = the goods owner (you)
- tipo de comprobante = T
- Value typically zero
- You issue it yourself
CFDI de Ingreso + complemento
- A carrier CHARGES for the freight
- Emisor = the transportista (carrier)
- tipo de comprobante = I
- You are the receptor
- The carrier issues it
The four blocks the complement structures
Whichever CFDI it rides on, the Carta Porte 3.1 complement structures the same four blocks. These are exactly the business data you’ll hand the PAC API in the next section, so it’s worth knowing what lives where. Node and attribute names below are illustrative — confirm the exact spelling and the current catálogos against the SAT tech docs and your PAC.
- Ubicaciones — Origen plus Destino. Each carries an
RFC, fecha/hora de salida or llegada,DistanciaRecorrida, and adomiciliowith aCodigoPostal. - Mercancías — the goods:
BienesTransp/ClaveProdServ(the SAT product key),Descripcion,Cantidad,ClaveUnidad,PesoEnKg, and theMaterialPeligrosoflag when it applies. - Autotransporte — the vehicle:
PlacaVM,ConfigVehicular,PermSCTplusNumPermisoSCT, and the insurance pairAseguraRespCivil/PolizaRespCivil. - FiguraTransporte — the operator/driver:
TipoFigura,RFCFigura,NumLicencia(and adomiciliowhere required).
Get these four right and you’ve got the shipment fully described. Everything downstream — assembling the XML, timbrar, storing the result — is plumbing the PAC handles for you.
How a PAC API turns business data into a stamped CFDI
Here’s the part the docs bury: you do not assemble the Carta Porte XML by hand. Facturapi, Facturama, Fiscalapi and others expose REST APIs that build the 3.1 XML for you. You send the four blocks — origin, destination, what you transport, the vehicle, the operator — and the API returns the stamped CFDI de Traslado plus its UUID. Pick a PAC by SDK and docs fit; the pattern is portable.
The shape below is illustrative — every PAC names fields slightly differently, so check your provider’s docs for the exact payload. And the catalog values (config_vehicular, perm_sct, clave_unidad, bienes_transp) are examples too — pick the 2026 keys that match your actual vehicle, permit, and goods.
// ILLUSTRATIVE — confirm exact field names in your PAC's docs.
// Catalog values below are examples — pick the 2026 keys that
// match YOUR vehicle/permit/goods.
// Assemble + stamp a CFDI de Traslado with the Carta Porte 3.1 complement.
const stamped = await pac.cartaPorte.create({
tipo_comprobante: "T", // T = Traslado (your own goods, no sale)
emisor: { rfc: EMISOR_RFC, regimen_fiscal: "601" },
complemento_carta_porte: {
version: "3.1",
transp_internac: "No",
ubicaciones: [
{
tipo: "Origen",
rfc: EMISOR_RFC,
fecha_hora_salida: "2026-06-25T08:00:00",
domicilio: { codigo_postal: "72000", estado: "PUE", pais: "MEX" },
},
{
tipo: "Destino",
rfc: RECEPTOR_RFC,
fecha_hora_llegada: "2026-06-25T14:30:00",
distancia_recorrida: 132, // km — must be present & consistent
domicilio: { codigo_postal: "06000", estado: "CMX", pais: "MEX" },
},
],
mercancias: {
peso_bruto_total: 850,
unidad_peso: "KGM",
items: [{
bienes_transp: "24112700", // SAT ClaveProdServ — example, use 2026 catalog
descripcion: "Refacciones automotrices",
cantidad: 40,
clave_unidad: "H87", // example — pick the 2026 ClaveUnidad
peso_en_kg: 850,
material_peligroso: "No", // flag correctly when it applies
}],
},
autotransporte: {
perm_sct: "TPAF01", // example c_TipoPermiso — confirm yours
num_permiso_sct: "012345678",
placa_vm: "AB1234C",
config_vehicular: "C2", // example c_ConfigAutotransporte — confirm yours
seguro_resp_civil: "Aseguradora XYZ",
poliza_resp_civil: "POL-998877",
},
figura_transporte: [{
tipo_figura: "01", // operator
rfc: OPERADOR_RFC,
num_licencia: "PUE9988776",
}],
},
});
await db.shipments.update(shipmentId, {
uuid: stamped.uuid,
stamped_xml: stamped.xml, // the legal document — STORE it
});
Same disciplines as any CFDI: you stamp under your CSD (Certificado de Sello Digital — issued per-RFC by the SAT, so it’s still your RFC’s certificate even when the PAC custodies and uses the key material on your behalf), make the call idempotent so a retry doesn’t burn a duplicate UUID, and ack-then-queue if timbrado is slow. And store the stamped XML — that XML is the legal document that travels with the goods. The PDF representación is a courtesy rendering, not the thing the inspector checks.
This is the same backbone as the Mexican payment-gateway integrations I write about — a clean REST call, idempotency, and durable storage of the artifact that matters.
End to end: issuing a Carta Porte 3.1 via a PAC
Here’s the full pipeline, in order:
- Collect the business data — origin and destination, the goods, the vehicle, the operator. The four blocks, sourced from your own systems.
- Validate origin/destination códigos postales against the SAT CP catalog UP FRONT. This is the single biggest avoidable rejection. The SAT CP catalog does not include every postal code in Mexico — new and rural CPs can be missing, and a missing CP rejects timbrado after you’ve already assembled everything.
- Assemble the four complement blocks with current 2026 catalog keys (
ClaveProdServ,ClaveUnidad,ConfigVehicular, etc.). - PAC timbra — sends to the SAT, validates against the 2026 catálogos, returns the seal plus UUID.
- Receive the stamped CFDI de Traslado XML and STORE it. That XML accompanies the goods in transit.
DistanciaRecorrida and MaterialPeligroso are computed and flagged here — and they’re two of the most common rejection points — so get them right before you call timbra. Here’s the CP validation from step 2; do it before assembling the payload:
// Validate origin/destination CPs against the SAT código-postal catalog
// BEFORE stamping. The SAT catalog is incomplete — fail fast, fail cheap.
async function assertCpExists(cp) {
if (!/^\d{5}$/.test(cp)) {
throw new Error(`CP malformed: ${cp}`);
}
// satCpCatalog = the current SAT c_CodigoPostal set, loaded into your store.
const row = await satCpCatalog.findByCp(cp);
if (!row) {
// Common with new/rural CPs — block before burning a timbre.
throw new Error(`CP ${cp} not in SAT catalog — verify before stamping`);
}
return row; // gives you estado/municipio to cross-check the domicilio
}
async function validateShipmentCps({ origenCp, destinoCp }) {
await Promise.all([assertCpExists(origenCp), assertCpExists(destinoCp)]);
}
Keep that catalog fresh. PACs auto-update their validators to the 2026 set; if your local copy lags, you’ll validate “pass” locally and still get rejected at timbrado.
- 1. Collect the dataOrigin/destination, goods, vehicle, operator
- 2. Validate the CPsOrigin + destination against the SAT CP catalog, up front
- 3. Assemble the four blocksWith current 2026 catalog keys
- 4. PAC timbra + storeReceive the stamped XML and persist it
The rejection traps that page you at the dock
The truck is loaded, the driver is waiting, and timbrado fails. Here are the four traps that cause it — each with the one-line fix.
- Missing postal codes. The SAT código-postal catalog does not include every CP in Mexico; new and rural CPs are frequently absent. Fix: validate origin and destination CPs against the catalog up front, before you assemble anything.
- Stale catálogos. The SAT refreshed the Carta Porte catálogos in January 2026 (the substantive update landed 13/01/2026, expanding the pedimentos relationships). PACs reject outdated keys and auto-update their validators. Fix: keep your catalog data current and confirm the set against the SAT — don’t trust a copy you cached last year.
- DistanciaRecorrida. A common rejection point. It must be present and consistent across the Ubicaciones. Fix: compute it once, attach it to the destination, and make sure it agrees with the route.
- MaterialPeligroso. Flagging hazardous materials (and the related fields) wrong is a frequent rejection. Fix: set the flag correctly when it applies — and only when it applies.
And the operational reminder that sits behind all of them: store the stamped XML, because it’s the legal document that travels with the shipment.
FAQ: tipo T, who issues, sandbox, and fines
When do I issue tipo T vs Ingreso? Tipo T (CFDI de Traslado) when you move your own goods with no sale. Ingreso when a carrier charges for the freight — and in that case the carrier issues it, not you.
If I hire a transportista, who issues the Carta Porte? The carrier. They emit the CFDI de Ingreso with the complement; you receive it as receptor.
Can I test without burning timbres? Yes. The dev-first PACs give you a sandbox or test key. Validate your payloads against the 2026 catálogos there before going live — that’s where you’ll catch the CP and catálogo problems cheaply. This is the same testing posture I use for Conekta’s OXXO/SPEI webhook integrations.
What’s the fine for non-compliance? It’s a range, not a single number: roughly $400–$17,030 MXN for failing to issue or accompany the transport document, with a higher band when the CFDI lacks required requisitos (Anexo 5 of the current RMF — confirm the current amounts). Worse than the peso fine: moving goods without a valid Carta Porte can trigger a presunción de contrabando, a criminal exposure, and the goods in transit are exposed. Defer the exact figure that applies to your contador.
Do I need my own CSD? Yes — you stamp under your RFC’s CSD. It’s issued per-RFC by the SAT and stays your certificate, so you remain responsible even though many PAC APIs custody and use it on your behalf, meaning you rarely touch the key files directly.
Ship the freight layer, defer the fiscal calls
The whole flow in one line: pick Traslado (tipo T) or Ingreso → collect the data → validate CPs against the SAT catalog → assemble the four blocks with 2026 keys → PAC timbra → store the stamped XML.
The pattern is portable across PACs — choose by SDK and docs fit, not by marketing. And keep the boundary honest: I’m an engineer, not a contador. For catalog edge cases, material peligroso, pedimentos, and the exact fine that applies, loop in your contador and confirm the current 2026 SAT rules.
Sources to bookmark: the SAT’s complemento Carta Porte minisite (canonical for version-in-force and catálogos), Anexo 5 of the RMF 2026 for the fine amounts, Fiscalapi’s Carta Porte guide, and Facturama’s Carta Porte docs. For more payments-and-fiscal engineering for Mexico, see my integrations guides.