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.

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

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.
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…).
foliostringFolio visible que se imprime en el CFDI, por ejemplo el nombre del pedido (#1001). El folio numérico interno se sigue asignando.
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 500 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
  }'
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",
  "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,
  "files": { "status": "pending" },
  "stampedAt": null,
  "createdAt": "2026-08-18T17:42:08.907Z"
}
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.
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.

Continuar