Documentación / API / Facturas

Timbrar una factura (CFDI 4.0 de ingreso)

POSThttps://api.cfdi.express/v1/invoicesAPI keyIdempotency-Key

Timbra un CFDI 4.0 de ingreso: nominativo PUE o PPD, o global al público en general. El motor de impuestos calcula IVA e IEPS por ti.

El timbrado es síncrono pero mediado por cola: la petición espera hasta 20 segundos por el UUID del SAT y responde 201 con la factura timbrada. Bajo carga extrema puede degradar a 202; en ese caso el recurso queda en status: "stamping" y consultas GET /v1/invoices/{id} hasta que pase a stamped.

El header Idempotency-Key es obligatorio. Usa un valor estable por operación lógica (el id de tu pedido, por ejemplo): si reintentas, recibes la factura original sin gastar otro timbre.

Reglas del SAT que validamos por ti: PUE exige una forma de pago real (no 99); PPD exige forma de pago 99 y deja la factura con saldoPendiente para sus complementos de pago.

Desde la v1.28.0 cada concepto acepta pedimentos cuando vendes de primera mano mercancía importada: un arreglo opcional de 1 a 100 números de pedimento. La guía está en cómo facturar con pedimento.

Desde la v1.36.0 puedes mandar fecha para que el CFDI salga con una fecha anterior. Si no la mandas, se usa la hora de timbrado. El mismo campo está en POST /v1/credit_notes y, por MCP, en create_invoice y create_credit_note.

Desde la v1.37.0 puedes mandar serie (1 a 25 caracteres, sin |). Si la omites, se timbra la serie del comercio. El ejemplo es B. El folio numérico interno se comparte entre series: para una numeración propia manda también folio (serie B con folio 1 queda como B-1). Una serie vacía, de más de 25 caracteres o con | responde 400 validation_error. create_invoice acepta el mismo campo.

Autenticación
Authorization: Bearer sk_test_... | sk_live_...

El header x-account-id indica la cuenta. Si no eres miembro, la respuesta es 403.

Parámetros

Headers

CampoTipoDescripción
Idempotency-KeyreqstringLlave única por operación lógica, 1 a 255 caracteres. Las respuestas repetidas traen Idempotency-Replayed: true.

Cuerpo (JSON)

CampoTipoDescripción
merchantIdreqstringId del emisor que factura.
fechastringFecha de emisión del CFDI (Fecha del comprobante), en hora local de Ciudad de México. Formato YYYY-MM-DDTHH:mm:ss, sin zona: por ejemplo 2026-10-13T09:30:00. CDMX es UTC-6 todo el año desde 2022; no mandes Z ni -06:00. Si la omites, se usa la hora de timbrado. Puede ir hasta 70 horas atrás (el instante de hace exactamente 70 horas sí pasa). El SAT acepta 72 horas antes de la certificación; el margen de 2 horas es porque el timbrado es asíncrono. No puede ser futura: la hora actual sí. El CSD del emisor tiene que haber estado vigente en ese instante. Desde la v1.36.0.
metodoPagostringPUE o PPD. Default PUE.
formaPagoreqstringClave de c_FormaPago (03 transferencia, 04 tarjeta de crédito…). Con PPD debe ser 99.
usoCfdireqstringClave de c_UsoCFDI (G03 gastos en general, S01 sin efectos fiscales…).
seriestringSerie que se timbra en el CFDI. Opcional, de 1 a 25 caracteres, sin el carácter |. Ejemplo: B. Si la omites, se usa la serie del comercio. El folio numérico interno se comparte entre series; para numeración propia por serie manda también folio (serie B con folio 1 queda como B-1). Desde la v1.37.0. Vacía, de más de 25 caracteres o con | responde 400 validation_error.
foliostringFolio visible que se imprime en el CFDI, por ejemplo el nombre del pedido (#1001). El folio numérico interno se sigue asignando y se comparte entre series.
receiver.rfcreqstringRFC del receptor. Para facturas globales, XAXX010101000.
receiver.namereqstringRazón social exacta de la Constancia de Situación Fiscal, en mayúsculas.
receiver.zipreqstringCódigo postal del domicilio fiscal del receptor.
receiver.regimenFiscalreqstringRégimen fiscal del receptor (clave de 3 dígitos).
items[]reqarrayDe 1 a 1,000 conceptos.
items[].productCodereqstringClave del producto/servicio del SAT, 8 dígitos.
items[].unitCodereqstringClave de unidad del SAT (E48, H87, KGM…).
items[].descriptionreqstringDescripción del concepto, hasta 1000 caracteres.
items[].quantityreqnumberCantidad, mayor a cero.
items[].unitPricereqnumberPrecio unitario, mayor a cero.
items[].unitstringUnidad en texto libre (Pieza, Servicio…).
items[].noIdentificacionstringSKU, número de serie o número de parte del concepto (NoIdentificacion del Anexo 20). De 1 a 100 caracteres, sin el carácter |. Se imprime tal cual debajo de la descripción.
items[].skustringObsoleto: alias de items[].noIdentificacion. Si envías ambos, gana noIdentificacion.
items[].pedimentosstring[]Números de pedimento de la mercancía importada que vendes de primera mano (InformacionAduanera del Anexo 20). Opcional. De 1 a 100 cadenas. Cada una son 15 dígitos — año (2), aduana (2), patente (4) y consecutivo (7) — con o sin espacios; ejemplo: "25 47 3807 5001234". Se emite en el formato de 21 caracteres del SAT, con dos espacios entre grupos. Un nodo por pedimento; los duplicados se descartan. También se imprime en el PDF.
items[].taxablebooleanMarca el concepto como exento cuando es false.
items[].ivaRatenumber0, 0.08 o 0.16. Default 0.16.
items[].iepsRatenumberTasa de IEPS entre 0 y 3 cuando aplica.
shipping.amountnumberCosto de envío como concepto adicional.
shipping.ivaRatenumber0, 0.08 o 0.16 para el envío.
shipping.productCodestringClave SAT del envío. Default la de servicios de flete.
shipping.unitCodestringClave de unidad del envío.
pricesIncludeTaxbooleantrue (default) si tus precios ya traen impuestos incluidos, como en Shopify.
cartDiscountnumberDescuento global a prorratear entre los conceptos.
informacionGlobal.periodicidadstringSólo facturas globales: 01 diario, 02 semanal, 03 quincenal, 04 mensual, 05 bimestral.
informacionGlobal.mesesstringSólo facturas globales: 01–12, o 13–18 para bimestres.
informacionGlobal.aniointegerSólo facturas globales: año del periodo.

Ejemplos

cURL — factura nominativa PUE
curl -X POST https://api.cfdi.express/v1/invoices \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: order_1001" \
  -H "Content-Type: application/json" \
  -d '{
    "merchantId": "mkq2f8v3x1t7p9d4n6c0b5rz",
    "metodoPago": "PUE",
    "formaPago": "03",
    "usoCfdi": "G03",
    "folio": "#1001",
    "receiver": {
      "rfc": "XIA190128J61",
      "name": "XENON INDUSTRIAL ARTICLES",
      "zip": "01160",
      "regimenFiscal": "601"
    },
    "items": [
      {
        "productCode": "43201808",
        "unitCode": "H87",
        "description": "Teclado mecánico 75%",
        "noIdentificacion": "KB-75-BLK",
        "quantity": 2,
        "unitPrice": 1740.00,
        "ivaRate": 0.16
      }
    ],
    "shipping": { "amount": 190.00 },
    "pricesIncludeTax": true
  }'
cURL — fecha anterior (v1.36.0)
curl -X POST https://api.cfdi.express/v1/invoices \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: order_1001_fecha" \
  -H "Content-Type: application/json" \
  -d '{
    "merchantId": "mkq2f8v3x1t7p9d4n6c0b5rz",
    "fecha": "2026-10-13T09:30:00",
    "metodoPago": "PUE",
    "formaPago": "03",
    "usoCfdi": "G03",
    "folio": "#1001",
    "receiver": {
      "rfc": "XIA190128J61",
      "name": "XENON INDUSTRIAL ARTICLES",
      "zip": "01160",
      "regimenFiscal": "601"
    },
    "items": [
      {
        "productCode": "43201808",
        "unitCode": "H87",
        "description": "Teclado mecánico 75%",
        "quantity": 2,
        "unitPrice": 1740.00,
        "ivaRate": 0.16
      }
    ],
    "pricesIncludeTax": true
  }'
Node.js
const res = await fetch("https://api.cfdi.express/v1/invoices", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CFDI_API_KEY}`,
    "Idempotency-Key": `order_${order.id}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    merchantId,
    metodoPago: "PUE",
    formaPago: "03",
    usoCfdi: "G03",
    folio: order.name,
    receiver: {
      rfc: "XIA190128J61",
      name: "XENON INDUSTRIAL ARTICLES",
      zip: "01160",
      regimenFiscal: "601",
    },
    items: order.lineItems.map((li) => ({
      productCode: li.satCode ?? "01010101",
      unitCode: "H87",
      description: li.title,
      noIdentificacion: li.sku,
      quantity: li.quantity,
      unitPrice: li.price,
    })),
    pricesIncludeTax: true,
  }),
});

const invoice = await res.json();

if (invoice.status === "stamping") {
  // 202 bajo carga extrema: consulta hasta que quede "stamped"
}
cURL — factura global
curl -X POST https://api.cfdi.express/v1/invoices \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: global_2026_07" \
  -H "Content-Type: application/json" \
  -d '{
    "merchantId": "mkq2f8v3x1t7p9d4n6c0b5rz",
    "metodoPago": "PUE",
    "formaPago": "01",
    "usoCfdi": "S01",
    "receiver": {
      "rfc": "XAXX010101000",
      "name": "PUBLICO EN GENERAL",
      "zip": "42501",
      "regimenFiscal": "616"
    },
    "items": [
      {
        "productCode": "01010101",
        "unitCode": "ACT",
        "description": "Venta al público en general",
        "quantity": 1,
        "unitPrice": 128450.00
      }
    ],
    "informacionGlobal": {
      "periodicidad": "04",
      "meses": "07",
      "anio": 2026
    }
  }'

Respuesta

201 Created
{
  "id": "inv7k3q9x2m4v1t8p6d0n5cb",
  "object": "invoice",
  "livemode": false,
  "status": "stamped",
  "kind": "nominal",
  "uuid": "9f1b2c3d-4e5f-4a6b-8c9d-0e1f2a3b4c5d",
  "serie": "A",
  "folio": 128,
  "folioText": "#1001",
  "fecha": "2026-08-18T11:37:10",
  "metodoPago": "PUE",
  "formaPago": "03",
  "usoCfdi": "G03",
  "currency": "MXN",
  "subtotal": 3163.79,
  "discount": 0,
  "total": 3670.00,
  "saldoPendiente": null,
  "receiver": {
    "rfc": "XIA190128J61",
    "name": "XENON INDUSTRIAL ARTICLES",
    "zip": "01160",
    "regimenFiscal": "601"
  },
  "merchantId": "mkq2f8v3x1t7p9d4n6c0b5rz",
  "files": { "status": "ready" },
  "cancellation": null,
  "stampedAt": "2026-08-18T17:42:10.512Z",
  "createdAt": "2026-08-18T17:42:08.907Z"
}
202 Accepted
{
  "id": "inv7k3q9x2m4v1t8p6d0n5cb",
  "object": "invoice",
  "livemode": false,
  "status": "stamping",
  "kind": "nominal",
  "uuid": null,
  "fecha": null,
  "files": { "status": "pending" },
  "stampedAt": null,
  "createdAt": "2026-08-18T17:42:08.907Z"
}
400 — fecha fuera de rango
{
  "type": "https://api.cfdi.express/docs/errors/validation_error",
  "title": "Validation error",
  "status": 400,
  "code": "validation_error",
  "detail": "fecha cannot be in the future",
  "subcode": "fecha_out_of_range",
  "errors": [
    { "path": "fecha", "message": "fecha cannot be in the future" }
  ]
}
400 — formato de fecha
{
  "type": "https://api.cfdi.express/docs/errors/validation_error",
  "title": "Validation error",
  "status": 400,
  "code": "validation_error",
  "detail": "Request validation failed",
  "errors": [
    {
      "path": "fecha",
      "message": "fecha must be local Mexico City time: YYYY-MM-DDTHH:mm:ss"
    }
  ]
}
422 — rechazo del SAT
{
  "type": "https://api.cfdi.express/docs/errors/sat_rejected",
  "title": "Rejected by SAT validation",
  "status": 422,
  "code": "sat_rejected",
  "detail": "El nombre del receptor no coincide con el registrado ante el SAT",
  "satCode": "CFDI40147"
}

Errores

StatuscodeCuándo aparece
400validation_errorDatos inválidos. El arreglo errors trae el path de cada campo. Con subcode fecha_out_of_range y errors[{path:'fecha'}], la fecha es inválida, futura, de más de 70 horas o el CSD no estaba vigente. Un formato distinto de YYYY-MM-DDTHH:mm:ss es el mismo code, sin ese subcode. Una serie vacía, de más de 25 caracteres o con | es el mismo code.
401unauthorizedFalta el header Authorization o la llave es inválida.
402insufficient_creditsNo hay saldo suficiente para el timbre. Recarga y reintenta.
409request_in_flightOtra petición con el mismo Idempotency-Key está en proceso.
422idempotency_key_reuseReusaste la llave de idempotencia con un cuerpo distinto.
422sat_rejectedEl SAT rechazó el comprobante. El detalle trae el código del SAT.
503pac_unavailableEl PAC no está disponible. Reintenta con la misma llave de idempotencia.

Notas

  • El XML, el PDF y el ZIP se generan justo después del timbrado. Cuando files.status es ready, GET /v1/invoices/{id} incluye files.xmlUrl, files.pdfUrl y files.zipUrl firmadas por 15 minutos.
  • Ante una falla ambigua con el PAC nunca reintentamos a ciegas: el documento se marca con needsReconciliation: true y tu saldo queda reservado hasta resolverlo.
  • items[].pedimentos queda en el XML como Concepto/InformacionAduanera/@NumeroPedimento y también se imprime en el PDF. El servidor MCP lo acepta igual: create_invoice y create_credit_note llevan el mismo arreglo, así que un agente de IA puede timbrar con el pedimento de cada concepto.
  • El recurso trae fecha (string o null) en hora local de Ciudad de México, YYYY-MM-DDTHH:mm:ss, sin zona. Ya con el CFDI armado es la Fecha del comprobante; antes, la que pediste, o null si no mandaste fecha. GET /v1/invoices/{id} devuelve el mismo campo.
  • Si omites fecha, el worker usa la hora de timbrado y le resta 5 minutos por desfase de reloj del PAC. Si la fecha que mandaste queda a menos de 5 minutos de ese momento, la ajusta a ese margen. En el ejemplo 201, stampedAt 2026-08-18T17:42:10.512Z son las 11:42:10 en Ciudad de México y fecha queda 5 minutos antes.
  • Otros detail de fecha_out_of_range: fecha de calendario imposible (fecha 2026-02-30T10:00:00 is not a valid date), futura (fecha cannot be in the future), más de 70 horas (fecha can be at most 70 hours in the past (the SAT allows 72 hours before stamping)) y CSD no vigente (The merchant's CSD was not valid on fecha). Un formato con Z, offset, solo la fecha o un espacio en lugar de T responde validation_error sin ese subcode, con message fecha must be local Mexico City time: YYYY-MM-DDTHH:mm:ss.

Continuar