Skip to content
Documentation sections

Create a CFDI

You send the voucher as JSON and the API builds the XML, seals it with your account's CSD, validates it against the SAT schema and stamps it. Field names are the ones in Anexo 20, so you can use it as a guide.

POST/invoice/create

The request

The voucher goes as JSON text in the json parameter of a form (application/x-www-form-urlencoded). A body sent with Content-Type: application/json isn't read. Send Accept: application/json or no Accept at all: with any other value the response could come back in another format.

curl
# FD_API=https://sandbox-app.facturadigital.com.mx/api/v5
curl -sS -X POST "$FD_API/invoice/create" \
  -H "X-Api-Key: $FD_API_KEY" \
  -H "Accept: application/json" \
  --data-urlencode "json@factura.json"
factura.json
{
  "Version": "4.0",
  "Serie": "A",
  "Folio": "1001",
  "Fecha": "AUTO",
  "FormaPago": "03",
  "MetodoPago": "PUE",
  "Moneda": "MXN",
  "TipoDeComprobante": "I",
  "Exportacion": "01",
  "LugarExpedicion": "64000",
  "SubTotal": "2501.00",
  "Total": "2901.16",
  "Emisor": { "RegimenFiscal": "601" },
  "Receptor": {
    "Rfc": "XIA190128J61",
    "Nombre": "XENON INDUSTRIAL ARTICLES",
    "DomicilioFiscalReceptor": "76343",
    "RegimenFiscalReceptor": "601",
    "UsoCFDI": "G03"
  },
  "Conceptos": [
    {
      "ClaveProdServ": "01010101",
      "NoIdentificacion": "SKU-001",
      "Cantidad": "2",
      "ClaveUnidad": "H87",
      "Unidad": "Pieza",
      "Descripcion": "Producto de prueba",
      "ValorUnitario": "1250.50",
      "Importe": "2501.00",
      "ObjetoImp": "02",
      "Impuestos": {
        "Traslados": [
          { "Base": "2501.00", "Impuesto": "002", "TipoFactor": "Tasa", "TasaOCuota": "0.160000", "Importe": "400.16" }
        ]
      }
    }
  ],
  "Impuestos": {
    "TotalImpuestosTrasladados": "400.16",
    "Traslados": [
      { "Base": "2501.00", "Impuesto": "002", "TipoFactor": "Tasa", "TasaOCuota": "0.160000", "Importe": "400.16" }
    ]
  }
}

Send values as text ("2501.00"), as in the example. JSON numbers work too, but they lose trailing zeros (2501.00 gets written as 2501). Each stamped CFDI uses 1 folio; no error before stamping charges one. To check your JSON without stamping, use the JSON validator.

Comprobante (root)

The attributes of the CFDI root. An empty attribute (null, "") isn't written to the XML; if the SAT requires it, the schema rejects it with a 702 before stamping.

Versionstringrequired
Exactly the text "4.0". The number 4.0 doesn't pass.
Seriestringconditional
Optional, except with Folio: "AUTO".
Foliostringoptional
Your folio, or "AUTO" to take the next one in your range (see Factura Digital fields).
Fechastringrequired
AAAA-MM-DDThh:mm:ss, or "AUTO" for the current time at your place of issue. The SAT accepts up to 72 hours back.
FormaPagostringconditional
c_FormaPago key. With MetodoPago PPD it's 99. In type P, T or N the API removes it.
CondicionesDePagostringoptional
Free text.
SubTotalstringrequired
Sum of the line items' Importe.
Descuentostringoptional
Sum of the line items' discounts. The API writes it with 2 decimals.
Monedastringrequired
c_Moneda key: MXN, USD… XXX in payment and transfer vouchers.
TipoCambiostringconditional
Required if the currency isn't MXN or XXX. With MXN it's 1 or you leave it out.
Totalstringrequired
SubTotal − Descuento + transferred taxes − withheld taxes. The API doesn't recalculate it.
TipoDeComprobantestringrequired
I income, E egreso (credit note), T transfer, N payroll or P payment.
Exportacionstringrequired
01 if it isn't an export.
MetodoPagostringconditional
PUE or PPD. In type P or T the API removes it.
LugarExpedicionstringrequired
Postal code where you issue. It also sets the time zone for Fecha: "AUTO".
Confirmacionstringoptional
Confirmation key from the stamping provider, for amounts or exchange rates out of range.

The API fills in NoCertificado, Certificado and Sello with your account's CSD: anything you send in those keys is ignored.

Issuer (Emisor)

RegimenFiscalstringrequired
Your regime, from c_RegimenFiscal, the one that applies to this transaction.
FacAtrAdquirentestringoptional
Operation number to invoice on behalf of an acquirer.
Rfc, Nombrestringoptional
You don't need to send them: the API uses the RFC and name of the key's account. You can't issue under another RFC.

Recipient (Receptor)

Rfcstringrequired
12 characters (company) or 13 (individual). XAXX010101000 for the general public.
Nombrestringrequired
As it appears on your customer's Constancia de Situación Fiscal, without the capital regime.
DomicilioFiscalReceptorstringrequired
Postal code of their tax address.
RegimenFiscalReceptorstringrequired
Their regime, from c_RegimenFiscal.
UsoCFDIstringrequired
c_UsoCFDI key that applies to their regime.
ResidenciaFiscal, NumRegIdTribstringconditional
Only with the generic foreign RFC XEXX010101000.

The SAT checks the RFC, name, postal code and regime against its taxpayer list. The keys it can respond with and the generic RFC fallback are in Recipient errors.

For your PDF you can also send Calle, NoExt, NoInt, Colonia, Localidad, Referencia, Municipio, Estado, Pais, CodigoPostal and a DomicilioEntrega object with those same fields. They don't go into the XML.

Line items (Conceptos)

Conceptos is a list with at least one line item.

ClaveProdServstringrequired
Eight-digit c_ClaveProdServ key.
NoIdentificacionstringoptional
Your SKU or internal code.
Cantidadstringrequired
Greater than zero.
ClaveUnidadstringrequired
c_ClaveUnidad key (H87 piece, E48 service…).
Unidadstringoptional
Your name for the unit.
Descripcionstringrequired
Up to 1,000 characters. The API cuts at 999 bytes: with accented characters a little less fits.
ValorUnitariostringrequired
Unit price before taxes.
Importestringrequired
Cantidad × ValorUnitario. The API doesn't recalculate it.
Descuentostringoptional
The line item's discount.
ObjetoImpstringrequired
01 not subject to tax, 02 subject to tax (carries Impuestos), 03 subject to tax with no breakdown required, and the rest of c_ObjetoImp.
extra1 … extra15stringoptional
Free fields for your PDF.

Conceptos[].Impuestos

Trasladosarrayconditional
Each one with Base, Impuesto (002 VAT, 003 IEPS), TipoFactor (Tasa, Cuota or Exento), TasaOCuota and Importe. With Exento, no TasaOCuota or Importe.
Retencionesarrayconditional
Same fields; 001 ISR, 002 VAT.

The API writes TasaOCuota with six decimals: "0.16" comes out as 0.160000.

Other line item nodes

ACuentaTercerosobjectoptional
RfcACuentaTerceros, NombreACuentaTerceros, RegimenFiscalACuentaTerceros and DomicilioFiscalACuentaTerceros.
InformacionAduaneraarrayoptional
List of { "NumeroPedimento": "…" }.
CuentaPredialobjectoptional
{ "Numero": "…" }. One per line item.
ComplementoConceptoobjectoptional
iedu (educational institutions) or HidroYPetro (hydrocarbons and petroleum products).

Taxes (Impuestos)

The voucher totals. Group the line items' transferred taxes by tax, factor type and rate: one row per combination, with the sum of its bases and amounts. Withholdings go one row per tax. If the CFDI has no taxes, leave the node out.

TotalImpuestosTrasladadosstringconditional
Sum of the Importe values in Traslados.
TotalImpuestosRetenidosstringconditional
Sum of the Importe values in Retenciones. At zero, the API leaves it out.
Trasladosarrayconditional
Base, Impuesto, TipoFactor, TasaOCuota and Importe.
Retencionesarrayconditional
Impuesto and Importe.

Related CFDIs

With a single relation type (for example 04 substitution or 01 credit note):

JSON
"CfdiRelacionadosTipoRelacion": "04",
"CfdiRelacionados": [
  { "UUID": "6F1A2B3C-4D5E-4F60-8172-93A4B5C6D7E8" }
]

With several relation types in the same CFDI, use CfdiRelacionadosMultiple, one group per type:

JSON
"CfdiRelacionadosMultiple": [
  { "TipoRelacion": "01", "Relacionados": [ { "UUID": "3C9B7A12-4E5D-4F60-B8A1-9D2C6E0F7B34" } ] },
  { "TipoRelacion": "07", "Relacionados": [ { "UUID": "0B7C2E51-8A4D-4E0F-A1C3-7D9E6F2B5A10" } ] }
]

Without CfdiRelacionadosTipoRelacion, the UUIDs in CfdiRelacionados aren't written. A CFDI in your account that another one relates to can no longer be cancelled from the platform.

Global invoice

InformacionGlobal

Periodicidadstringrequired
c_Periodicidad key: 01 daily, 02 weekly, 04 monthly…
Mesesstringrequired
c_Meses key.
Año (or Ano)stringrequired
Year of the transactions. Ano writes the same thing and avoids trouble with the ñ.

It goes with the recipient XAXX010101000, name PUBLICO EN GENERAL, regime 616 and use S01. For an invoice to a single customer with no RFC, don't use this node: put their name in Receptor.Nombre.

Factura Digital fields

They aren't from the SAT. They go at the root of the JSON, at the same level as Serie and Folio.

Folio: "AUTO"stringoptional
Takes the next folio from the range in Configuración › Series y folios (Settings › Series and folios) for that Serie and TipoDeComprobante; both are required. Only the exact text AUTO. The assigned folio comes back in the response, in cfdi.Serie and cfdi.Folio.
Fecha: "AUTO"stringoptional
The current time in your account's time zone or, if you didn't set one, in the time zone of the LugarExpedicion postal code.
OmitirTimbradostringoptional
"si" builds, seals and validates the XML and responds without stamping it. It requires a CSD and balance, uses no folio and saves nothing. With Folio: "AUTO" it does take a folio from the range.
plantilla_idintegeroptional
Id of your PDF template (Configuración › Diseño de PDF). If it doesn't exist or isn't active, it responds 400 before stamping.
enviar_mailobjectoptional
Emails the CFDI when it's stamped. See below.
LeyendaFoliostringoptional
Document title on the PDF.
Observacionesstringoptional
Free text on the PDF.
NumeroOrdenCompra, FechaOrdenComprastringoptional
Your customer's purchase order, for the PDF.
fechalimitepagostringoptional
Payment due date, for the PDF (lowercase).
EstatusCobranzaintegeroptional
0 unpaid (the default for income vouchers), 1 partly paid, 2 paid.
MontoCobrado, SaldoInsolutostringoptional
Amount collected and amount still owed. By default, 0 and the Total.
extra1 … extra30stringoptional
Free fields for your PDF.
Decimales, DecimalesItemintegeroptional
Decimals for the totals and for the line item amounts on the PDF.
Addendaobjectoptional
G500 or TERNIUM with the variables of its template. It's added to the saved XML after stamping.

Email the CFDI when it's stamped

JSON
"enviar_mail": {
  "destinatarios": "cliente@facturadigital.com.mx",
  "bcc": "cobranza@facturadigital.com.mx",
  "mensaje": "Gracias por tu compra."
}

destinatarios is required inside the object: addresses separated by , or ;, up to 500 characters. bcc takes up to 500 and mensaje up to 2,000. It's validated before stamping and the email goes out in under a minute. In the sandbox, emails don't reach the recipient. To resend later, use POST /invoice/send.

Complements

They go inside "Complemento": { "<llave>": { … } } at the root. The key is case-sensitive. The SAT schema validates them before stamping.

ComplementKeyNotes
Payments 2.0 (REP)Pagos20With TipoDeComprobante: "P". Guide: Payment complement.
Carta Porte 3.1CartaPorte31Version exactly "3.1": with any other value the complement isn't written. The SAT's CP rules are checked before the folio is taken. Guide: Carta Porte 3.1.
Payroll 1.2Nomina12With TipoDeComprobante: "N". Overtime also goes in as earning 019.
Foreign Trade 2.0ComercioExterior20With Exportacion other than 01.
Local taxesImpuestosLocalesRate as a percentage ("3.00"). The API calculates the totals from the rows.
Retail (Detallista)detallistaLowercase.
INEINE
Donees (Donatarias)Donatarias
Currency exchange (Divisas)Divisas
AirlinesAerolineas
Public notariesNotariosPublicosA list.
Payment in kindPagoEnEspecie
Used vehicleVehiculoUsado
Partial construction servicesParcialesConstruccion
Hydrocarbon incomeIngresosHidrocarburos
Works of art and antiquesobrasarteantiguedadesLowercase.

The Postman collection has full examples of Payments, Payroll, Carta Porte and Detallista.

The response

200
{
  "message": "The document was generated successfully",
  "code": 200,
  "cfdi": {
    "NoCertificado": "30001000000500003416",
    "UUID": "6F1A2B3C-4D5E-4F60-8172-93A4B5C6D7E8",
    "FechaTimbrado": "2026-11-03T10:30:12",
    "RfcProvCertif": "SPR190613I52",
    "SelloCFD": "Xy…Base64…==",
    "NoCertificadoSAT": "30001000000500003456",
    "SelloSAT": "Ab…Base64…==",
    "CadenaOrigTFD": "||1.1|6F1A2B3C-4D5E-4F60-8172-93A4B5C6D7E8|2026-11-03T10:30:12|SPR190613I52|Xy…==|30001000000500003456||",
    "CadenaQR": "https://verificacfdi.facturaelectronica.sat.gob.mx/default.aspx?id=6F1A2B3C-…&re=EKU9003173C9&rr=XIA190128J61&tt=2,901.160000&fe=AbCd1234",
    "XmlBase64": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4…",
    "PDF": "https://sandbox-app.facturadigital.com.mx/docs/pdf/6F1A2B3C-4D5E-4F60-8172-93A4B5C6D7E8",
    "XML": "https://sandbox-app.facturadigital.com.mx/docs/xml/EKU9003173C9/6F1A2B3C-4D5E-4F60-8172-93A4B5C6D7E8"
  }
}

cfdi

UUIDstring
The fiscal folio.
XmlBase64string
The stamped XML in Base64. Store it: it's your voucher.
CadenaOrigTFDstring
Original string of the stamp, for your PDF.
CadenaQRstring
SAT verification URL for the QR code. Today the total (tt) comes out with a thousands comma; if you build your own QR, remove it.
PDF, XMLstring
Download links for the PDF and the XML.
Serie, Foliostring
Only with Folio: "AUTO": the folio that was assigned.
  • With ya_existia: true, the provider returned a UUID your account already had: no new one was saved and no extra folio was charged.
  • With OmitirTimbrado: "si", the response is { "message": "…(SIN TIMBRAR)", "xmlBase64": "…", "code": 200 }, with xmlBase64 starting in lowercase.
  • If something fails after stamping (saving the PDF, queuing the email), the response is still this 200: the CFDI already exists with the SAT.

Errors

codeWhenWhat to do
501The key is missing, doesn't exist or belongs to the other environment.Keys and environments.
602Your account has no active CSD. It's checked before the JSON is read.Upload your CSD in Configuración.
601Your account has no folios. This is also checked before the JSON is read.Buy folios.
400json is missing, it isn't a JSON object, a node has another type (json_type_validation), Version isn't "4.0", plantilla_id doesn't exist, enviar_mail has an invalid address, or Folio: "AUTO" has no series or no range.The message says what to fix; nothing was sent to the SAT.
400 + cartaporte_validationA Carta Porte 3.1 rule (CP1xx) isn't met.errors[].code carries the CP key.
702The XML doesn't meet the SAT schema.errors[] carries field and value; see the matrix.
706Your CSD couldn't be used (file or password).Upload your CSD again in Configuración.
400 + SAT keyThe SAT rejected the CFDI (CFDI40143, CRP20…, NOM…).Look it up in the error matrix.
400 + timbre_previoThat same XML was already stamped. context.uuid carries its UUID; there's no charge.Get the CFDI with that UUID.
5037The API couldn't connect to the stamping provider; nothing was sent.Retry as is.
5034The CFDI may have been stamped without a readable response. It carries Serie and Folio, and context.uuid if it could be read.Don't resend blindly: how to check.

Retry without duplicates

The API has no idempotency key. If a request gets cut off and you repeat it with Fecha: "AUTO" or Folio: "AUTO", the second one is a different XML and can produce another CFDI with another UUID.

  • If your system retries on its own, send a fixed Fecha and your own Folio. The same XML sent again responds timbre_previo with the UUID of the first one and isn't charged twice.
  • With 5037, retry: nothing went out.
  • With 5034, first check whether the CFDI exists (in your listings or with GET /validator/cfdi) and retry only if it doesn't.
  • Set a timeout of 150 to 180 seconds: the API waits up to 120 seconds for the stamping provider.

What the API adjusts on its own

WhatHow
Emisor.Rfc and Emisor.NombreAlways the ones of the key's account.
FormaPagoRemoved in type P, T and N.
MetodoPagoRemoved in type P and T.
TasaOCuotaSix decimals.
Root DescuentoTwo decimals.
DescripcionCut at 999 bytes.
TotalImpuestosRetenidos at zeroLeft out.
Fecha with a spaceThe space becomes T.
Recipient RFC, name and postal code, ClaveProdServ, NoIdentificacionNo leading or trailing spaces.

Checked against the production API v5 on 2026-11-03.