Timbrado CFDI 4.0

Facturación por Jonathan Fonseca
96.1%nivel de servicio (30 d)
1,425 mslatencia promedio
255llamadas (30 d)

Timbra, consulta, cancela y genera el PDF de CFDI 4.0 ante el SAT. Sube tu CSD y factura en minutos

Empieza a consumir

Todas las peticiones van al gateway de idoo.dev con tu API key:

curl "https://idoo.dev/v1/timbrado-cfdi-4-0/{ruta}" \ -H "Authorization: Bearer {tu_api_key}"
Planes
Prueba Gratis
Gratis
  • 10 llamadas/mes
  • 60 peticiones/minuto
Inicia sesión para suscribirte
Basico
$150.00/mes
  • 30 llamadas/mes
  • 60 peticiones/minuto
Inicia sesión para suscribirte
Pro
$300.00/mes
  • 300 llamadas/mes
  • 60 peticiones/minuto
Inicia sesión para suscribirte
Ultra
$3,000.00/mes
  • 2,799 llamadas/mes
  • 180 peticiones/minuto
Inicia sesión para suscribirte
Ultra +
$10,000.00/mes
  • 12,500 llamadas/mes
  • 360 peticiones/minuto
Inicia sesión para suscribirte
Documentación

Timbrado CFDI 4.0

API para timbrar, consultar, cancelar y generar el PDF de Comprobantes Fiscales Digitales por Internet (CFDI 4.0) ante el SAT a través de PAC autorizado, así como administrar los emisores (RFC + CSD) asociados a tu cuenta.

URL base y autenticación

Todas las peticiones van al gateway de idoo.dev con tu API key:

https://api.idoo.dev/v1/timbrado-cfdi-4-0/{modulo}/{accion}
curl "https://api.idoo.dev/v1/timbrado-cfdi-4-0/emisores/listado" \
  -H "Authorization: Bearer {tu_api_key}"

También se acepta el header X-API-KEY: {tu_api_key}. Las keys se crean en tu panel de idoo.dev.

Ambientes: sandbox vs producción

El ambiente lo determina tu API key, no un parámetro:

Key Ambiente Comportamiento
idoo_sk_test_... Sandbox Timbra contra el ambiente de pruebas del PAC. Ilimitado y gratis: no consume la cuota de tu plan.
idoo_sk_live_... Producción Timbra ante el SAT. Cada timbrado exitoso descuenta 1 timbre de tu plan mensual.

Los emisores (CSD) y las facturas están separados por ambiente: un emisor cargado en sandbox no existe en producción, y viceversa.

Los seis endpoints de timbrado —facturas/timbrar, facturas/timbrar_arreglo, facturas/timbrar_complemento, facturas/timbrar_complemento_arreglo, facturas/timbrar_nomina_arreglo y facturas/timbrar_carta_porte_arreglo— descuentan cuota, y solo cuando el timbrado es exitoso: un CFDI rechazado por validación no te cuesta. Ten presente que un complemento de pago consume un timbre igual que una factura, así que una venta a crédito pagada en tres parcialidades gasta cuatro timbres en total.

Consultas, PDF, cancelaciones y administración de emisores son libres (aplican únicamente al rate limit de tu plan). El header X-Idoo-Cuota-Restante de cada respuesta cobrable te dice cuántos timbres te quedan en el periodo.

Formato de respuesta

Todas las respuestas (éxito o error) usan el mismo sobre JSON:

{
  "valid": true,
  "status": 200,
  "message": "Factura timbrada exitosamente.",
  "data": { "...": "..." },
  "timestamp": "2026-07-24 12:00:00"
}

Cuando algo merece señalarse sin haber impedido la operación, data trae además un arreglo advertencias.

Reintentos: no gastas el timbre dos veces

Si nos vuelves a mandar el mismo comprobante —mismos datos y misma Fecha, de modo que el XML sellado sea idéntico— no se timbra otra vez: recuperamos el timbrado original del PAC y te devolvemos 200 con el mismo uuid y esta advertencia:

{
  "advertencias": ["Este comprobante ya estaba timbrado: se recuperó el timbre original en vez de gastar otro."]
}

Eso cubre el caso feo de una petición que se corta después de que el PAC ya timbró. Reintentar es seguro: manda otra vez el mismo body. Si cambias la Fecha, el folio o cualquier importe, es un comprobante distinto y sí consume un timbre nuevo.

Excepción: POST facturas/pdf devuelve el binario del PDF directamente (Content-Type: application/pdf), no el sobre JSON. Trátalo como descarga de archivo.

Códigos de estado

Código Significado
200 Operación exitosa.
202 Cancelación enviada, pendiente de aceptación del receptor.
400 Petición inválida: falta un campo o el PAC rechazó el timbrado (detalle en data).
401 No se envió API key.
402 Cuota de timbres del plan agotada (solo producción).
403 API key inválida o sin suscripción activa a esta API.
404 El recurso (emisor, factura) no existe o no pertenece a tu cuenta.
405 Método HTTP no permitido.
409 Conflicto — p. ej. cancelar una factura ya cancelada.
422 El CFDI no pasó las validaciones del esquema/reglas del SAT (detalle legible en data).
429 Excediste el rate limit por minuto de tu plan (Retry-After indica cuándo reintentar).
500 Error interno o falla de comunicación con el PAC.

Catálogo de endpoints

Método Endpoint Descripción ¿Descuenta cuota?
GET emisores/listado Lista los emisores (RFC + CSD) de tu cuenta. No
POST emisores/subir_csd Registra o actualiza el CSD de un emisor. No
POST emisores/eliminar Elimina un emisor y sus archivos. No
POST facturas/timbrar Timbra un CFDI ya construido y sellado (XML en base64).
POST facturas/timbrar_arreglo Construye, sella y timbra un CFDI desde JSON: facturas, notas de crédito y demás egresos.
POST facturas/timbrar_complemento_arreglo Construye, sella y timbra un complemento de pago (REP) desde JSON.
POST facturas/timbrar_complemento Timbra un complemento de pago que envías como XML.
POST facturas/timbrar_nomina_arreglo Construye, sella y timbra un recibo de nómina 1.2 desde JSON.
POST facturas/timbrar_carta_porte_arreglo Construye, sella y timbra un CFDI con complemento Carta Porte 3.1 desde JSON.
GET facturas/consultar Estatus de una factura (local + SAT). No
POST facturas/pdf Representación impresa (PDF) del CFDI. No
POST facturas/cancelar Cancela un CFDI ante el SAT. No

GET emisores/listado

Lista los emisores registrados en tu cuenta, ordenados por vencimiento del certificado.

Respuesta 200:

{
  "data": {
    "total": 1,
    "emisores": [
      {
        "rfc": "EKU9003173C9",
        "razon_social": "ESCUELA KEMPER URGATE",
        "regimen_fiscal": "601",
        "fecha_vencimiento": "2027-05-18",
        "dias_restantes": 318,
        "estatus": "activo"
      }
    ]
  }
}

estatus es calculado: vencido si dias_restantes <= 0, por_vencer si faltan 30 días o menos, activo en cualquier otro caso. Monitorea por_vencer para renovar CSDs a tiempo.

POST emisores/subir_csd

Registra (o actualiza, si el RFC ya existe en tu cuenta) el Certificado de Sello Digital de un emisor. Es requisito previo para timbrar con ese RFC. El certificado se valida técnicamente y se registra ante el PAC antes de guardarse.

Body:

{
  "archivo_cer_base64": "MIIF...",
  "archivo_key_base64": "MIIE...",
  "contrasena_csd": "contraseña-del-csd",
  "rfc": "EKU9003173C9",
  "razon_social": "ESCUELA KEMPER URGATE",
  "regimen_fiscal": "601"
}
Campo Tipo Obligatorio Descripción
archivo_cer_base64 string Contenido binario del .cer en Base64.
archivo_key_base64 string Contenido binario del .key en Base64.
contrasena_csd string Contraseña de la llave privada.
rfc string RFC del emisor; debe coincidir con el del certificado.
razon_social string No Razón social a mostrar.
regimen_fiscal string No Clave de régimen fiscal SAT (ej. 601).

Respuesta 200:

{
  "data": { "rfc": "EKU9003173C9", "razon_social": "ESCUELA KEMPER URGATE", "vence": "2027-05-18 18:08:08", "tipo": "CSD" },
  "message": "Emisor guardado exitosamente."
}

Errores comunes: 400 si falta un campo, el RFC no coincide con el certificado, o .cer/.key/contraseña no embonan; 502 si el PAC no pudo registrar el CSD.

POST emisores/eliminar

Body: { "rfc": "EKU9003173C9" }

Respuesta 200: { "data": { "rfc": "EKU9003173C9" }, "message": "Emisor y archivos asociados eliminados correctamente" }

404 si el RFC no existe en tu cuenta (en el ambiente de tu key).

POST facturas/timbrar

Timbra un CFDI que tú ya construiste y sellaste (trae Sello y NoCertificado). Si prefieres que el servicio construya y selle por ti, usa timbrar_arreglo.

Para un complemento de pago usa facturas/timbrar_complemento: acepta el XML sin sellar y revisa que los saldos cuadren antes de gastar el timbre.

Body:

{
  "xml_base64": "PD94bWwgdmVyc2lvbj0iMS4wIi...",
  "CondicionPago": "30 días"
}
Campo Tipo Obligatorio Descripción
xml_base64 string XML del CFDI 4.0 ya sellado, en Base64.
CondicionPago string No Se inyecta como atributo del nodo Comprobante (sobrescribe el existente).

Respuesta 200:

{
  "data": {
    "uuid": "E9BED1E5-9920-5C45-A4BF-D96CD2BC37AB",
    "rfc_emisor": "EKU9003173C9",
    "xml_base64": "PD94bWwgdmVyc2lvbj0iMS4wIi..."
  },
  "message": "Factura timbrada exitosamente."
}

400 si el Base64 es inválido o el PAC reporta alertas (detalle como arreglo de strings "[código] mensaje" en data).

POST facturas/timbrar_arreglo

La forma recomendada de integrarse. Construye el CFDI 4.0 desde un JSON, lo sella con el CSD del emisor guardado, lo valida contra los esquemas del SAT, lo timbra y devuelve XML + PDF.

Body (ejemplo completo):

{
  "rfc_emisor": "EKU9003173C9",
  "comprobante": {
    "Serie": "A",
    "Folio": "1024",
    "FormaPago": "01",
    "MetodoPago": "PUE",
    "Moneda": "MXN",
    "TipoDeComprobante": "I",
    "LugarExpedicion": "20928",
    "Exportacion": "01"
  },
  "receptor": {
    "Rfc": "MASO451221PM4",
    "Nombre": "MARIA OLIVIA MARTINEZ SAGAZ",
    "DomicilioFiscalReceptor": "80290",
    "RegimenFiscalReceptor": "605",
    "UsoCFDI": "S01"
  },
  "conceptos": [
    {
      "ClaveProdServ": "10101502",
      "Cantidad": "1",
      "ClaveUnidad": "KGM",
      "Descripcion": "MAIZ",
      "ValorUnitario": "1000.00",
      "Importe": "1000.00",
      "ObjetoImp": "02",
      "traslados": [
        { "Base": "1000.00", "Impuesto": "002", "TipoFactor": "Tasa", "TasaOCuota": "0.160000", "Importe": "160.00" }
      ],
      "retenciones": []
    }
  ]
}
Campo Tipo Obligatorio Descripción
rfc_emisor string RFC del emisor; debe existir en tu cuenta con CSD activo y vigente.
comprobante object Atributos del nodo Comprobante (Serie, Folio, FormaPago, MetodoPago, Moneda, TipoDeComprobante, LugarExpedicion...). Si no envías Fecha, se genera con la hora local de tu LugarExpedicion.
receptor object Atributos del nodo Receptor (Rfc, Nombre, DomicilioFiscalReceptor, RegimenFiscalReceptor, UsoCFDI).
conceptos array Conceptos con atributos estándar del nodo Concepto, más arreglos opcionales traslados y retenciones.
informacion_global object No { "Periodicidad": "04", "Meses": "07", "Año": "2026" } para una factura global. Ver Factura global abajo.
relacionados object | array No Uno o varios grupos { "TipoRelacion": "01", "uuids": ["..."] }. Ver Relacionar el CFDI con otros abajo.
CondicionesDePago string No Se agrega como atributo del comprobante.

Los valores deben cumplir los catálogos oficiales del SAT para CFDI 4.0; el servicio solo rellena Fecha y Exportacion (default 01) por ti.

Sobre la Fecha: el SAT la exige en la hora local del lugar de expedición y no admite comprobantes fechados en el futuro. Si la omites, se genera con el huso que corresponde a tu LugarExpedicion —Sinaloa, Sonora, BCS, Baja California, Nayarit, Quintana Roo y Ciudad Juárez no van con la hora del centro—. Si prefieres mandarla tú, mándala en la hora local de ese código postal.

Si emites con MetodoPago: "PPD", recuerda que la FormaPago debe ser 99 y que tendrás que emitir un complemento de pago con facturas/timbrar_complemento_arreglo por cada cobro que recibas.

Factura global (ventas al público en general)

Cuando amparas en un solo comprobante las ventas de un periodo a clientes que no pidieron factura, agrega el nodo InformacionGlobal con el periodo que cubre:

{
  "comprobante": {
    "Serie": "G", "Folio": "501",
    "FormaPago": "01", "MetodoPago": "PUE",
    "Moneda": "MXN", "TipoDeComprobante": "I", "LugarExpedicion": "80290",
    "InformacionGlobal": { "Periodicidad": "04", "Meses": "07", "Año": "2026" }
  }
}
Campo Descripción
Periodicidad 01 diario, 02 semanal, 03 quincenal, 04 mensual, 05 bimestral
Meses 0112 para un mes; 1318 para un bimestre (13 Enero-Febrero … 18 Noviembre-Diciembre)
Año Cuatro dígitos. También se acepta escribirlo Anio

El receptor no lo mandas: manda "receptor": {} (u omítelo) y se completa con lo que el SAT fija para una global —RFC XAXX010101000, nombre PUBLICO EN GENERAL, régimen 616 y uso S01—, con el DomicilioFiscalReceptor igual a tu LugarExpedicion. Si envías alguno de esos campos con otro valor, se rechaza con 422 antes de gastar el timbre; también se rechaza una periodicidad o un mes fuera de catálogo, un bimestre declarado como mes (o al revés) y un año que todavía no ocurre.

El nodo se acepta dentro de comprobante o en la raíz del body como informacion_global. El PDF imprime el periodo en un bloque propio ("Mensual · Julio 2026").

Relacionar el CFDI con otros

Lo necesitas en dos casos habituales: una nota de crédito (TipoDeComprobante: "E"), que se relaciona con la factura que corrige (TipoRelacion 01 o 03), y una sustitución (TipoRelacion: "04"), obligatoria después de cancelar un CFDI con motivo 01.

{
  "relacionados": { "TipoRelacion": "01", "uuids": ["81B3033F-6F7B-5D24-AE32-1F1DE4ABE354"] }
}

El SAT admite más de un tipo de relación por comprobante; para eso manda un arreglo de grupos:

{
  "relacionados": [
    { "TipoRelacion": "01", "uuids": ["81B3033F-6F7B-5D24-AE32-1F1DE4ABE354"] },
    { "TipoRelacion": "04", "uuids": ["8179B395-B827-5F92-92FF-BC38DD268C64"] }
  ]
}

El campo se acepta en la raíz del body o dentro de comprobante como CfdiRelacionados, que es donde vive el nodo en el XML del SAT. Los folios pueden ir bajo uuids o CfdiRelacionado, y como ["UUID"] o [{ "UUID": "..." }]. Si repites el mismo TipoRelacion en dos grupos se fusionan en un solo nodo, porque el SAT no admite el nodo duplicado.

Valores de TipoRelacion (catálogo c_TipoRelacion):

Clave Significado
01 Nota de crédito de los documentos relacionados
02 Nota de débito de los documentos relacionados
03 Devolución de mercancía sobre facturas o traslados previos
04 Sustitución de los CFDI previos
05 Traslados de mercancías facturados previamente
06 Factura generada por los traslados previos
07 CFDI por aplicación de anticipo

Reglas que se aplican antes de llamar al PAC, todas con 422 y sin gastar timbre:

  • Un TipoDeComprobante: "E" debe traer relacionados. Una nota de crédito que no dice qué corrige es lo primero que observa el SAT en una revisión.
  • TipoRelacion tiene que estar en el catálogo y los folios tener forma de UUID.
  • El folio relacionado no puede ser de otro emisor de tu cuenta.
  • Un CFDI cancelado solo se puede relacionar con TipoRelacion: "04" (sustitución); con cualquier otro tipo se rechaza.

Si el folio no aparece en tu historial de timbrado —porque la factura original se emitió con otro sistema— no se bloquea: se timbra y la respuesta trae un arreglo advertencias diciéndolo. El estatus que se consulta es el que tenemos registrado; si cancelaste el original por fuera y nadie ha llamado a facturas/consultar, aquí seguirá como vigente.

Respuesta 200:

{
  "data": {
    "uuid": "E9BED1E5-9920-5C45-A4BF-D96CD2BC37AB",
    "rfc_emisor": "EKU9003173C9",
    "total": "1160.00",
    "xml_base64": "PD94bWwgdmVyc2lvbj0iMS4wIi...",
    "pdf_base64": "JVBERi0xLjcK..."
  },
  "message": "Factura timbrada exitosamente."
}

data.advertencias aparece solo cuando hay algo que señalar sin haber impedido el timbrado, por ejemplo un folio relacionado que no está en tu historial.

Lo que se revisa antes de gastar el timbre

  • El Importe de cada concepto debe corresponder a Cantidad × ValorUnitario, con la misma tolerancia que aplica el SAT: una unidad de la última posición decimal de la moneda (0.01 en pesos).
  • Los grupos de relacionados: TipoRelacion dentro del catálogo y folios con forma de UUID.
  • Que en comprobante, receptor y cada concepto no viaje una lista u objeto donde el XML espera un atributo de texto (salvo traslados y retenciones, que sí son nodos hijos).

Todo esto devuelve 422 con el campo señalado, sin llamar al PAC.

Errores:

  • 400 — faltan campos, el emisor no existe/está inactivo, o su CSD está vencido.
  • 422 — el CFDI no pasó las validaciones del SAT o las previas; data trae mensajes legibles por campo:
[
  "[CFDI40101] Campo 'FormaPago': el valor '99a' no es válido. Valores permitidos: 01, 02, 03, ...",
  "Concepto 1 ('MAIZ'): el Importe declarado (100.50) no corresponde a Cantidad × ValorUnitario (3 × 33.33 = 99.99). La diferencia máxima permitida en MXN es de 0.01."
]
  • 402 — cuota de timbres agotada (producción). Mejora tu plan para desbloquear al instante.
  • 500 — el PAC rechazó el timbrado u otro error interno.

POST facturas/timbrar_complemento_arreglo

Emite un CFDI de tipo P: el complemento de recepción de pagos 2.0 (REP), el comprobante que acredita que cobraste una factura emitida a crédito.

Lo necesitas cuando facturaste con MetodoPago: "PPD" (pago en parcialidades o diferido). El SAT te obliga a emitir un REP por cada cobro que recibas, a más tardar el quinto día natural del mes siguiente al que recibiste el pago. Si tu venta fue de contado (PUE) no hace falta ninguno.

Igual que timbrar_arreglo, este endpoint construye el XML, lo sella con el CSD guardado del emisor, lo valida y lo timbra. Solo tienes que enviarle lo que únicamente tú sabes: cuándo, cómo y cuánto te pagaron, y contra qué facturas se aplica.

Body (ejemplo completo):

{
  "rfc_emisor": "EKU9003173C9",
  "comprobante": {
    "Serie": "PAG",
    "Folio": "310",
    "LugarExpedicion": "80290"
  },
  "receptor": {
    "Rfc": "MASO451221PM4",
    "Nombre": "MARIA OLIVIA MARTINEZ SAGAZ",
    "DomicilioFiscalReceptor": "80290",
    "RegimenFiscalReceptor": "605"
  },
  "pagos": [
    {
      "FechaPago": "2026-08-01T10:00:00",
      "FormaDePagoP": "03",
      "MonedaP": "MXN",
      "NumOperacion": "REF-88451203",
      "RfcEmisorCtaOrd": "BBA830831LJ2",
      "CtaOrdenante": "0123456789",
      "RfcEmisorCtaBen": "BMN930209927",
      "CtaBeneficiario": "9876543210",
      "documentos": [
        {
          "IdDocumento": "C1855BEE-324E-55EB-9154-91D890F31469",
          "Serie": "FAC",
          "Folio": "20458",
          "NumParcialidad": "1",
          "ImpSaldoAnt": "11600.00",
          "ImpPagado": "5800.00",
          "traslados": [
            { "BaseDR": "5000.00", "ImpuestoDR": "002", "TipoFactorDR": "Tasa", "TasaOCuotaDR": "0.160000", "ImporteDR": "800.00" }
          ]
        }
      ]
    }
  ]
}

Campos

Campo Tipo Obligatorio Descripción
rfc_emisor string RFC del emisor; debe existir en tu cuenta con CSD activo y vigente.
comprobante object Serie, Folio y LugarExpedicion. Si no envías Fecha, se genera con la hora local de tu LugarExpedicion.
receptor object Rfc, Nombre, DomicilioFiscalReceptor y RegimenFiscalReceptor del cliente que pagó.
pagos array Uno o más pagos recibidos. Cada uno puede aplicarse a varias facturas.
relacionados object | array No { "TipoRelacion": "04", "uuids": ["..."] } — úsalo cuando este REP sustituye a otro cancelado. Acepta las mismas formas que en timbrar_arreglo; si omites TipoRelacion se asume 04.

Cada elemento de pagos:

Campo Obligatorio Descripción
FechaPago AAAA-MM-DDThh:mm:ss. No puede ser posterior a la fecha del comprobante.
FormaDePagoP Clave del catálogo c_FormaPago (03 transferencia, 01 efectivo, 04 tarjeta de crédito...). No se admite 99: aquí ya sabes cómo te pagaron.
MonedaP Moneda en que recibiste el dinero (MXN, USD...).
TipoCambioP Condicional Obligatorio si MonedaP no es MXN. Si es MXN se pone 1 solo.
Monto No Importe recibido. Si lo omites se calcula sumando lo aplicado a cada factura.
NumOperacion Recomendado Referencia bancaria, folio de la transferencia o autorización de la tarjeta.
RfcEmisorCtaOrd, CtaOrdenante, NomBancoOrdExt No Banco y cuenta de quien pagó.
RfcEmisorCtaBen, CtaBeneficiario No Banco y cuenta donde recibiste.
documentos Facturas que se liquidan con este pago (al menos una).

Cada elemento de documentos:

Campo Obligatorio Descripción
IdDocumento UUID de la factura que estás cobrando.
Serie, Folio No Serie y folio de esa factura, para que se lean en el PDF.
NumParcialidad Número de pago sobre esa factura: 1 el primero, 2 el segundo... Si lo omites se asume 1.
ImpSaldoAnt Saldo pendiente antes de este pago. En la primera parcialidad es el total de la factura.
ImpPagado Cuánto se aplica de este pago a esa factura.
ImpSaldoInsoluto No Saldo que queda. Se calcula como ImpSaldoAnt − ImpPagado si no lo envías.
MonedaDR No Moneda de la factura. Se asume la del pago.
EquivalenciaDR Condicional Tipo de cambio entre la moneda de la factura y la del pago. Obligatorio si difieren; si coinciden se pone 1.
ObjetoImpDR No 02 si la factura causa impuestos, 01 si no. Se deduce de si mandaste impuestos.
traslados Condicional IVA/IEPS trasladados, en proporción a lo pagado, no al total de la factura.
retenciones No ISR/IVA retenidos, también en proporción.

Cada traslado lleva BaseDR, ImpuestoDR, TipoFactorDR, TasaOCuotaDR e ImporteDR; los exentos (TipoFactorDR: "Exento") omiten los dos últimos. Las retenciones llevan los cinco siempre.

Lo que no tienes que enviar

Se arma solo, y mandarlo es error: TipoDeComprobante (P), Moneda (XXX), SubTotal y Total (cero — el importe real vive en el complemento), Exportacion, UsoCFDI (CP01), el concepto único obligatorio (clave 84111506, "Pago"), el nodo Totales del complemento con sus totales por tasa, y los impuestos agregados del pago.

Un CFDI de pago no admite FormaPago, MetodoPago, CondicionesDePago, Descuento ni TipoCambio en el comprobante; si los envías recibes un 422. La forma de pago va dentro de cada pago, como FormaDePagoP.

Los saldos los llevas tú

El servicio no lleva el estado de cuenta de tus facturas: NumParcialidad, ImpSaldoAnt e ImpSaldoInsoluto los envías tú, porque la cuenta por cobrar vive en tu sistema. Lo que sí hacemos es revisar que la aritmética cuadre antes de mandar nada al PAC, para que un descuadre te salga gratis en lugar de costarte un timbre:

  • ImpSaldoInsoluto debe ser exactamente ImpSaldoAnt − ImpPagado.
  • ImpPagado no puede exceder el saldo anterior, y debe ser mayor que cero.
  • ImporteDR de cada traslado debe ser BaseDR × TasaOCuotaDR.
  • FechaPago no puede ser posterior a la fecha del comprobante.
  • El IdDocumento debe tener forma de UUID.

Respuesta 200:

{
  "data": {
    "uuid": "DA025970-C9AB-5666-A090-D469BB6AB19D",
    "rfc_emisor": "EKU9003173C9",
    "monto_total_pagos": "5800.00",
    "xml_base64": "PD94bWwgdmVyc2lvbj0iMS4wIi...",
    "pdf_base64": "JVBERi0xLjcK..."
  },
  "message": "Complemento de pago timbrado exitosamente."
}

monto_total_pagos es la suma de lo cobrado (el Total del CFDI siempre es cero en un comprobante de pago).

Errores:

  • 400 — faltan rfc_emisor, receptor o pagos; el emisor no existe o su CSD está vencido; el PAC rechazó el timbrado.
  • 422 — el complemento no cuadra o incluye campos prohibidos. data trae un mensaje por problema, señalando la posición exacta:
[
  "pagos[0].documentos[1].ImpSaldoInsoluto (1000.00) no cuadra: ImpSaldoAnt − ImpPagado = 0.00.",
  "pagos[0].FormaDePagoP: el complemento de pagos no admite la clave 99 (Por definir)."
]
  • 402 — cuota de timbres agotada (producción).
  • 404 — el emisor no está registrado en tu cuenta.

POST facturas/timbrar_complemento

La misma emisión de REP, pero tú generas el XML. Úsalo si ya tienes un generador de CFDI y solo quieres el timbrado; si no, timbrar_complemento_arreglo es bastante más cómodo.

Acepta el XML con o sin sello:

  • Sin sellar — lo sellamos con el CSD que tienes registrado. Es el caso normal cuando subiste tu CSD con emisores/subir_csd. Además validamos el comprobante contra los esquemas del SAT antes de timbrarlo.
  • Ya sellado — lo mandamos tal cual al PAC, sin tocar nada (cualquier reescritura invalidaría tu sello). Requiere que conserves tu llave privada de tu lado.

En ambos casos verificamos antes de gastar el timbre que sea de tipo P, que traiga complemento de pagos 2.0, que el RFC emisor esté registrado en tu cuenta y que los saldos cuadren.

Body:

{ "xml_base64": "PD94bWwgdmVyc2lvbj0iMS4wIi..." }
Campo Tipo Obligatorio Descripción
xml_base64 string XML del CFDI de tipo P con complemento Pagos 2.0, en Base64. Sellado u opcionalmente sin sellar.

Respuesta 200:

{
  "data": {
    "uuid": "2AAD6C0D-2440-5E71-9344-BDBDF75DA0C5",
    "rfc_emisor": "EKU9003173C9",
    "monto_total_pagos": "5800.00",
    "sellado_por_api": true,
    "xml_base64": "PD94bWwgdmVyc2lvbj0iMS4wIi..."
  },
  "message": "Complemento de pago timbrado exitosamente."
}

sellado_por_api te dice si el XML llegó sin sello y lo sellamos nosotros. Este endpoint no devuelve PDF; obtenlo después con facturas/pdf usando el uuid.

Errores:

  • 400 — Base64 o XML inválido; el comprobante no es de tipo P; no trae complemento de pagos; el PAC lo rechazó.
  • 404 — el RFC emisor del XML no está registrado en tu cuenta.
  • 422 — los saldos no cuadran, o el XML sin sellar no pasó las validaciones del SAT.

POST facturas/timbrar_nomina_arreglo

Emite un CFDI de tipo N: el recibo de nómina 1.2, el comprobante de lo que le pagas a un trabajador en un periodo.

Como en el resto de endpoints _arreglo, solo mandas lo que únicamente tú sabes —el periodo, el empleado, y lo que le pagas y le retienes—. Los totales del complemento, los del comprobante y el concepto único se calculan aquí.

Body (ejemplo completo):

{
  "rfc_emisor": "EKU9003173C9",
  "comprobante": { "Serie": "NOM", "Folio": "4471", "LugarExpedicion": "80290" },
  "receptor": {
    "Rfc": "MASO451221PM4",
    "Nombre": "MARIA OLIVIA MARTINEZ SAGAZ",
    "DomicilioFiscalReceptor": "80290",
    "RegimenFiscalReceptor": "605"
  },
  "nomina": {
    "TipoNomina": "O",
    "FechaPago": "2026-08-13",
    "FechaInicialPago": "2026-07-30",
    "FechaFinalPago": "2026-08-13",
    "NumDiasPagados": "15",
    "emisor": { "RegistroPatronal": "B5510768108" },
    "receptor": {
      "Curp": "MASO451221MSLRGL05",
      "NumSeguridadSocial": "04099312345",
      "FechaInicioRelLaboral": "2019-03-01",
      "Antigüedad": "P356W",
      "TipoContrato": "01",
      "Sindicalizado": "No",
      "TipoJornada": "01",
      "TipoRegimen": "02",
      "NumEmpleado": "EMP-0042",
      "Departamento": "Administración",
      "Puesto": "Coordinadora de operaciones",
      "RiesgoPuesto": "1",
      "PeriodicidadPago": "04",
      "SalarioBaseCotApor": "1233.33",
      "SalarioDiarioIntegrado": "1290.00",
      "ClaveEntFed": "SIN"
    },
    "percepciones": [
      { "TipoPercepcion": "001", "Clave": "P001", "Concepto": "Sueldos, salarios y rayas", "ImporteGravado": "18500.00" },
      { "TipoPercepcion": "021", "Clave": "P021", "Concepto": "Prima vacacional", "ImporteGravado": "700.00", "ImporteExento": "500.00" }
    ],
    "deducciones": [
      { "TipoDeduccion": "002", "Clave": "D002", "Concepto": "ISR", "Importe": "3120.45" },
      { "TipoDeduccion": "001", "Clave": "D001", "Concepto": "Seguridad social", "Importe": "640.18" }
    ],
    "incapacidades": [
      { "DiasIncapacidad": "2", "TipoIncapacidad": "02", "ImporteMonetario": "0.00" }
    ]
  }
}

Campos

Campo Tipo Obligatorio Descripción
rfc_emisor string RFC del patrón; debe existir en tu cuenta con CSD activo y vigente.
comprobante object Serie, Folio y LugarExpedicion. Si no envías Fecha, se genera con la hora local de tu LugarExpedicion.
receptor object Datos fiscales del trabajador: Rfc, Nombre, DomicilioFiscalReceptor y RegimenFiscalReceptor (normalmente 605).
nomina object El recibo en sí (tabla siguiente).
relacionados object | array No Para sustituir un recibo cancelado; si omites TipoRelacion se asume 04.

Dentro de nomina:

Campo Obligatorio Descripción
TipoNomina O ordinaria, E extraordinaria.
FechaPago, FechaInicialPago, FechaFinalPago AAAA-MM-DD (solo la fecha, sin hora). El periodo no puede ir al revés.
NumDiasPagados Días que ampara el recibo; mayor que cero.
emisor Recomendado RegistroPatronal, y EntidadSNCF si aplica.
receptor Datos laborales del trabajador: Curp, NumSeguridadSocial, TipoContrato, TipoRegimen, PeriodicidadPago, SalarioBaseCotApor, ClaveEntFed
percepciones Condicional Lista de {TipoPercepcion, Clave, Concepto, ImporteGravado, ImporteExento}. El importe que omitas se toma como 0.00. Admite horas_extra y acciones_o_titulos por percepción.
deducciones No Lista de {TipoDeduccion, Clave, Concepto, Importe}.
otros_pagos Condicional Lista de {TipoOtroPago, Clave, Concepto, Importe}. El subsidio para el empleo (002) lleva además "subsidio": { "SubsidioCausado": "..." }.
incapacidades No Lista de {DiasIncapacidad, TipoIncapacidad, ImporteMonetario}.
separacion_indemnizacion, jubilacion_pension_retiro Condicional Obligatorios si pagas percepciones de esos tipos (ver abajo).

Necesitas al menos una percepción o un otro pago.

Lo que no tienes que enviar

Se arma solo, y mandarlo es error: TipoDeComprobante (N), Moneda (MXN), FormaPago (99), MetodoPago (PUE), Exportacion, UsoCFDI (CN01), el concepto único obligatorio (clave 84111505, "Pago de nómina"), el SubTotal, el Descuento y el Total del comprobante, y todos los totales del complemento:

  • TotalPercepciones, TotalDeducciones y TotalOtrosPagos del nodo Nomina.
  • TotalGravado y TotalExento, y el total por tipo de pago: TotalSueldos, TotalSeparacionIndemnizacion y TotalJubilacionPensionRetiro, según la clave de cada percepción.
  • TotalImpuestosRetenidos (el ISR, clave de deducción 002) y TotalOtrasDeducciones (todo lo demás).

Puedes enviar TotalPercepciones, TotalDeducciones o TotalOtrosPagos si quieres que verifiquemos tu cálculo: si no cuadran con la suma de los conceptos, recibes 422 sin gastar el timbre.

Lo que se revisa antes de gastar el timbre

  • El periodo: fechas en formato AAAA-MM-DD y FechaInicialPago no posterior a FechaFinalPago.
  • Que cada percepción, deducción, otro pago e incapacidad traiga su clave, su concepto y un importe no negativo.
  • Que los totales que declaraste cuadren con la suma de los conceptos.
  • Que las deducciones no superen a lo pagado: el total de un recibo no puede ser negativo.
  • Que las percepciones de separación o indemnización (022, 023, 025) traigan separacion_indemnizacion, y las de jubilación o pensión (039, 044) traigan jubilacion_pension_retiro.
  • Que el subsidio para el empleo (TipoOtroPago: "002") traiga su SubsidioCausado.

Respuesta 200:

{
  "data": {
    "uuid": "9F0C1A44-6C34-5E52-9A2D-27C10E5F0B31",
    "rfc_emisor": "EKU9003173C9",
    "total_percepciones": "19700.00",
    "total_deducciones": "3760.63",
    "total_otros_pagos": "0.00",
    "neto_pagado": "15939.37",
    "xml_base64": "PD94bWwgdmVyc2lvbj0iMS4wIi...",
    "pdf_base64": "JVBERi0xLjcK..."
  },
  "message": "Recibo de nómina timbrado exitosamente."
}

El PDF trae el desglose de percepciones, deducciones, otros pagos e incapacidades, y etiqueta el total como "Neto a pagar".

Errores:

  • 400 — faltan rfc_emisor, receptor o nomina; el emisor no existe o su CSD está vencido; el PAC rechazó el timbrado.
  • 422 — el recibo no cuadra o incluye campos que se calculan solos; data trae un mensaje por problema señalando la posición exacta (nomina.percepciones[0].Concepto: es obligatorio.).
  • 402 — cuota de timbres agotada (producción).
  • 404 — el emisor no está registrado en tu cuenta.

POST facturas/timbrar_carta_porte_arreglo

Emite un CFDI con complemento Carta Porte 3.1, el que ampara el traslado de mercancía por carretera. Cubre los dos casos reales, y el tipo de comprobante es lo único que los distingue:

Caso TipoDeComprobante Quién lo emite
Traslado T El dueño de la mercancía, que la mueve con sus propios medios. No hay cobro: moneda XXX y totales en cero.
Ingreso I El transportista que factura el flete. Los conceptos y sus impuestos son los del servicio.

Esta versión cubre autotransporte federal. Marítimo, aéreo y ferroviario devuelven 422 diciéndolo, en vez de dejar que el PAC rechace el comprobante a mitad de la operación.

Body (traslado de mercancía propia):

{
  "rfc_emisor": "EKU9003173C9",
  "comprobante": { "TipoDeComprobante": "T", "Serie": "CP", "Folio": "120", "LugarExpedicion": "80290" },
  "receptor": { "DomicilioFiscalReceptor": "26670" },
  "carta_porte": {
    "TranspInternac": "No",
    "ubicaciones": [
      {
        "TipoUbicacion": "Origen",
        "RFCRemitenteDestinatario": "EKU9003173C9",
        "FechaHoraSalidaLlegada": "2026-08-14T08:00:00",
        "domicilio": { "Calle": "Carretera a Navolato km 4", "Municipio": "006", "Estado": "SIN", "Pais": "MEX", "CodigoPostal": "80290" }
      },
      {
        "TipoUbicacion": "Destino",
        "RFCRemitenteDestinatario": "MASO451221PM4",
        "FechaHoraSalidaLlegada": "2026-08-14T18:00:00",
        "DistanciaRecorrida": "842.5",
        "domicilio": { "Calle": "Avenida Vallarta 1200", "Municipio": "039", "Estado": "JAL", "Pais": "MEX", "CodigoPostal": "44100" }
      }
    ],
    "mercancias": [
      {
        "BienesTransp": "11121900", "Descripcion": "Maíz blanco a granel",
        "Cantidad": "20", "ClaveUnidad": "TNE", "PesoEnKg": "20000.000",
        "MaterialPeligroso": "No", "ValorMercancia": "180000.00", "Moneda": "MXN"
      }
    ],
    "autotransporte": {
      "PermSCT": "TPAF01", "NumPermisoSCT": "A2C3D4E5F6",
      "identificacion_vehicular": { "ConfigVehicular": "C2R2", "PesoBrutoVehicular": "12.5", "PlacaVM": "AB123CD", "AnioModeloVM": "2021" },
      "seguros": { "AseguraRespCivil": "Quálitas Compañía de Seguros", "PolizaRespCivil": "POL-99887766" },
      "remolques": [ { "SubTipoRem": "CTR004", "Placa": "XY9876Z" } ]
    },
    "figuras": [
      { "TipoFigura": "01", "NombreFigura": "JUAN PEREZ RAMIREZ", "RFCFigura": "MASO451221PM4", "NumLicencia": "B4523187" }
    ]
  }
}

Para facturar el flete (TipoDeComprobante: "I") el body es el mismo, más FormaPago, MetodoPago, Moneda, el receptor de tu cliente y los conceptos del servicio de transporte, exactamente como en timbrar_arreglo.

Campos

Campo Tipo Obligatorio Descripción
rfc_emisor string RFC del emisor con CSD activo.
comprobante object TipoDeComprobante (T por defecto), Serie, Folio y LugarExpedicion.
receptor object Condicional En un traslado se deriva del emisor; solo manda DomicilioFiscalReceptor si tu domicilio fiscal no coincide con el lugar de expedición. En un ingreso es tu cliente.
conceptos array Solo en I El servicio de transporte que cobras. En un traslado se generan solos, uno por mercancía y sin valor.
carta_porte object El traslado en sí (tablas siguientes).
relacionados object | array No Para sustituir un traslado cancelado.

Dentro de carta_porte:

Campo Obligatorio Descripción
TranspInternac "No" o "Sí". Con "Sí" se vuelven obligatorios EntradaSalidaMerc (Entrada/Salida), PaisOrigenDestino, ViaEntradaSalida y regimenes_aduaneros.
ubicaciones Al menos un Origen y un Destino, cada uno con RFCRemitenteDestinatario, FechaHoraSalidaLlegada y domicilio (mínimo Estado, Pais, CodigoPostal). Cada destino lleva su DistanciaRecorrida en kilómetros.
mercancias Cada una con BienesTransp (clave c_ClaveProdServCP), Descripcion, Cantidad, ClaveUnidad y PesoEnKg. Admite documentacion_aduanera, guias_identificacion, cantidad_transporta y detalle_mercancia.
autotransporte PermSCT, NumPermisoSCT, identificacion_vehicular (configuración, peso bruto, placa y año), seguros (responsabilidad civil obligatoria) y remolques opcionales.
figuras Quienes intervienen en el traslado. Debe ir al menos el operador (TipoFigura: "01") con su NumLicencia.
UnidadPeso No Unidad del peso total; se asume KGM.

Lo que no tienes que enviar

Se calcula solo, y mandarlo mal es un 422:

  • IdCCP, el folio de 36 caracteres que identifica la carta porte ante el SAT. Se genera aquí y se te devuelve en data.id_ccp. Solo mándalo si estás re-timbrando un traslado con su folio original.
  • TotalDistRec: la suma de las distancias de los destinos.
  • PesoBrutoTotal y NumTotalMercancias: salen de las mercancías.
  • IDUbicacion: se numeran solas (OR000001, DE000001…).
  • En un traslado: TipoDeComprobante T, Moneda XXX, los totales en cero, los conceptos y el receptor.

Si envías TotalDistRec, PesoBrutoTotal o NumTotalMercancias, se contrastan con lo calculado y el descuadre se rechaza antes de gastar el timbre.

Lo que se revisa antes de gastar el timbre

Origen y destino presentes; distancias y pesos mayores que cero; fechas coherentes (la salida no puede ser posterior a la llegada); domicilios con estado, país y código postal; el operador con licencia; los catálogos chicos (c_TipoPermiso, c_ConfigAutotransporte, c_SubTipoRem, c_ClaveUnidadPeso, c_TipoEmbalaje, c_RegimenAduanero, c_FiguraTransporte); el material peligroso con su clave y embalaje; y el traslado internacional con sus datos aduaneros.

Lo que sí valida el PAC y no nosotros —porque son catálogos de decenas de miles de entradas—: la clave BienesTransp, la clave de material peligroso y las colonias y municipios. Dos reglas de esos catálogos sorprenden seguido:

  • Hay claves de BienesTransp marcadas como "material peligroso condicional": en ellas hay que declarar MaterialPeligroso explícitamente ("Sí" o "No").
  • No toda configuración vehicular admite remolque: si mandas remolques, la ConfigVehicular debe ser una que los acepte (por ejemplo C2R2, no C2).

Respuesta 200:

{
  "data": {
    "uuid": "A741FCDD-FC43-54AB-8673-AEA651D0F174",
    "id_ccp": "CCC7e8d7-169b-483d-8383-e139638e1b97",
    "rfc_emisor": "EKU9003173C9",
    "tipo_comprobante": "I",
    "total": "33060.00",
    "peso_bruto_total": "20350.500",
    "num_total_mercancias": "2",
    "total_distancia_km": "842.50",
    "xml_base64": "PD94bWwgdmVyc2lvbj0iMS4wIi...",
    "pdf_base64": "JVBERi0xLjcK..."
  },
  "message": "CFDI con carta porte timbrado exitosamente."
}

El PDF incluye el bloque de carta porte que exige el SAT en la representación impresa: IdCCP, ubicaciones con domicilios y distancias, mercancías, datos del vehículo y del seguro, remolques y figuras del transporte.

Errores:

  • 400 — faltan rfc_emisor o carta_porte; el emisor no existe o su CSD está vencido; el PAC rechazó el timbrado.
  • 422 — la carta porte no cuadra o un catálogo no coincide; data señala la posición exacta (carta_porte.ubicaciones[1].DistanciaRecorrida: …).
  • 402 — cuota de timbres agotada (producción).

GET facturas/consultar

Consulta el estatus de una factura timbrada. Si no está cancelada localmente, verifica además el estatus real ante el SAT y actualiza el registro si cambió.

GET facturas/consultar?uuid=E9BED1E5-9920-5C45-A4BF-D96CD2BC37AB

Respuesta 200:

{
  "data": {
    "factura": {
      "emisor_rfc": "EKU9003173C9",
      "uuid": "E9BED1E5-9920-5C45-A4BF-D96CD2BC37AB",
      "rfc_receptor": "MASO451221PM4",
      "total": "1160.000000",
      "tipo_comprobante": "I",
      "estatus_sat": "vigente"
    },
    "sat_check": { "...": "estatus devuelto por el SAT, o null si no se pudo consultar" }
  },
  "message": "Consulta exitosa."
}

POST facturas/pdf

Genera la representación impresa de un CFDI timbrado.

Body: { "uuid": "E9BED1E5-9920-5C45-A4BF-D96CD2BC37AB" }

Respuesta 200 — importante: es el binario del PDF (Content-Type: application/pdf, Content-Disposition: inline), no JSON. Guárdalo como archivo (response.blob() en JS, --output en curl). Los errores (400, 404, 500) sí usan el sobre JSON.

POST facturas/cancelar

Cancela un CFDI ante el SAT a través del PAC.

Body:

{
  "uuid": "E9BED1E5-9920-5C45-A4BF-D96CD2BC37AB",
  "motivo": "02",
  "uuid_reemplaza": ""
}
Campo Tipo Obligatorio Descripción
uuid string UUID del CFDI a cancelar.
motivo string Motivo SAT (tabla siguiente).
uuid_reemplaza string Solo si motivo = "01" UUID del CFDI que sustituye al cancelado.

Motivos de cancelación:

Clave Significado
01 Emitido con errores con relación (requiere uuid_reemplaza).
02 Emitido con errores sin relación.
03 No se llevó a cabo la operación.
04 Operación nominativa relacionada con una factura global.

Respuestas:

  • 200 — cancelado (o ya estaba cancelado ante el SAT):
{ "data": { "uuid": "...", "estatus": "cancelado", "estatus_documento": "201", "motivo": "02" } }
  • 202 — el receptor debe aceptar o rechazar la cancelación (según reglas del SAT).
  • 400 — motivo inválido, falta uuid_reemplaza, o el PAC la rechazó (detalle en data).
  • 409 — ya estaba marcada como cancelada.

Flujo recomendado de integración

  1. Integra en sandbox con tu key idoo_sk_test_... — es gratis e ilimitado. Puedes usar el CSD de pruebas del SAT (RFC EKU9003173C9).
  2. Registra tu(s) emisor(es) una sola vez con emisores/subir_csd y monitorea vencimientos con emisores/listado.
  3. Timbra con facturas/timbrar_arreglo (recomendado) o facturas/timbrar si generas tu propio XML sellado.
  4. Si facturas a crédito (PPD), emite un facturas/timbrar_complemento_arreglo por cada cobro que recibas. Guarda el saldo de cada factura en tu sistema: la API no lo lleva.
  5. Guarda el uuid: con él consultas, descargas el PDF, cancelas y relacionas correcciones. Para corregir a la baja (devoluciones, descuentos, bonificaciones) emite una nota de crédito con TipoDeComprobante: "E" y relacionados; para errores que no se corrigen con eso, cancela con motivo 01 y timbra la sustituta con TipoRelacion: "04".
  6. Maneja 422/400 mostrando los mensajes de data a tu usuario — ya vienen en lenguaje natural.
  7. Cuando estés listo, crea tu key de producción, sube el CSD real y elige el plan según tu volumen de timbres.

¿PUE o PPD? La decisión que determina si necesitas complementos

Situación MetodoPago FormaPago ¿Necesitas REP?
Te pagan al momento de facturar PUE La forma real (01, 03, 04...) No
Facturas ahora y te pagan después PPD 99 (Por definir) Sí, uno por cada cobro
Te pagan en parcialidades PPD 99 Sí, uno por cada parcialidad

En una factura PPD la forma de pago tiene que ser 99: todavía no sabes cómo te van a pagar. La forma real se declara después, en el FormaDePagoP de cada complemento.


Ejercicios guiados

Todos corren en sandbox y no consumen cuota. Sustituye {tu_api_key} por tu key idoo_sk_test_....

Ejercicio 1 — Tu primer CFDI de contado

Factura de $1,000 más IVA, pagada en efectivo al momento.

curl -X POST "https://api.idoo.dev/v1/timbrado-cfdi-4-0/facturas/timbrar_arreglo" \
  -H "Authorization: Bearer {tu_api_key}" \
  -H "Content-Type: application/json" \
  -d '{
    "rfc_emisor": "EKU9003173C9",
    "comprobante": {
      "Serie": "A", "Folio": "1001",
      "FormaPago": "01", "MetodoPago": "PUE",
      "Moneda": "MXN", "TipoDeComprobante": "I",
      "LugarExpedicion": "80290"
    },
    "receptor": {
      "Rfc": "MASO451221PM4",
      "Nombre": "MARIA OLIVIA MARTINEZ SAGAZ",
      "DomicilioFiscalReceptor": "80290",
      "RegimenFiscalReceptor": "605",
      "UsoCFDI": "S01"
    },
    "conceptos": [{
      "ClaveProdServ": "10101502", "ClaveUnidad": "KGM", "Cantidad": "1",
      "Descripcion": "MAIZ", "ValorUnitario": "1000.00", "Importe": "1000.00",
      "ObjetoImp": "02",
      "traslados": [
        { "Base": "1000.00", "Impuesto": "002", "TipoFactor": "Tasa", "TasaOCuota": "0.160000", "Importe": "160.00" }
      ]
    }]
  }'

Guarda el uuid de la respuesta y descarga el PDF:

curl -X POST "https://api.idoo.dev/v1/timbrado-cfdi-4-0/facturas/pdf" \
  -H "Authorization: Bearer {tu_api_key}" \
  -H "Content-Type: application/json" \
  -d '{"uuid":"EL-UUID-QUE-TE-DEVOLVIO"}' \
  --output factura.pdf

Al ser PUE, aquí terminas. No hay complemento que emitir.

Ejercicio 2 — Venta a crédito y su complemento de pago

Paso 1. Emite la factura a crédito. Fíjate en MetodoPago: "PPD" y FormaPago: "99":

curl -X POST "https://api.idoo.dev/v1/timbrado-cfdi-4-0/facturas/timbrar_arreglo" \
  -H "Authorization: Bearer {tu_api_key}" \
  -H "Content-Type: application/json" \
  -d '{
    "rfc_emisor": "EKU9003173C9",
    "comprobante": {
      "Serie": "FAC", "Folio": "20458",
      "FormaPago": "99", "MetodoPago": "PPD",
      "Moneda": "MXN", "TipoDeComprobante": "I",
      "LugarExpedicion": "80290"
    },
    "CondicionesDePago": "Crédito a 30 días",
    "receptor": {
      "Rfc": "MASO451221PM4",
      "Nombre": "MARIA OLIVIA MARTINEZ SAGAZ",
      "DomicilioFiscalReceptor": "80290",
      "RegimenFiscalReceptor": "605",
      "UsoCFDI": "G03"
    },
    "conceptos": [{
      "ClaveProdServ": "43211508", "ClaveUnidad": "H87", "Cantidad": "1",
      "Descripcion": "Servidor rack 2U", "ValorUnitario": "10000.00", "Importe": "10000.00",
      "ObjetoImp": "02",
      "traslados": [
        { "Base": "10000.00", "Impuesto": "002", "TipoFactor": "Tasa", "TasaOCuota": "0.160000", "Importe": "1600.00" }
      ]
    }]
  }'

Total de la factura: $11,600.00. Anota su uuid — lo vas a necesitar.

Paso 2. Un mes después el cliente te transfiere los $11,600. Emite el complemento:

curl -X POST "https://api.idoo.dev/v1/timbrado-cfdi-4-0/facturas/timbrar_complemento_arreglo" \
  -H "Authorization: Bearer {tu_api_key}" \
  -H "Content-Type: application/json" \
  -d '{
    "rfc_emisor": "EKU9003173C9",
    "comprobante": { "Serie": "PAG", "Folio": "1", "LugarExpedicion": "80290" },
    "receptor": {
      "Rfc": "MASO451221PM4",
      "Nombre": "MARIA OLIVIA MARTINEZ SAGAZ",
      "DomicilioFiscalReceptor": "80290",
      "RegimenFiscalReceptor": "605"
    },
    "pagos": [{
      "FechaPago": "2026-09-01T10:00:00",
      "FormaDePagoP": "03",
      "MonedaP": "MXN",
      "NumOperacion": "REF-88451203",
      "documentos": [{
        "IdDocumento": "UUID-DE-LA-FACTURA-DEL-PASO-1",
        "Serie": "FAC", "Folio": "20458",
        "NumParcialidad": "1",
        "ImpSaldoAnt": "11600.00",
        "ImpPagado": "11600.00",
        "traslados": [
          { "BaseDR": "10000.00", "ImpuestoDR": "002", "TipoFactorDR": "Tasa", "TasaOCuotaDR": "0.160000", "ImporteDR": "1600.00" }
        ]
      }]
    }]
  }'

No enviaste ImpSaldoInsoluto (se calcula: 11600 − 11600 = 0.00), ni TipoCambioP, ni MonedaDR, ni EquivalenciaDR, ni ObjetoImpDR, ni el Monto del pago. Todo eso se deriva.

Ejercicio 3 — Cobro en tres parcialidades

La misma factura de $11,600, pagada en tres exhibiciones. Un complemento por cada cobro, y en cada uno el saldo avanza:

Parcialidad NumParcialidad ImpSaldoAnt ImpPagado ImpSaldoInsoluto BaseDR ImporteDR
1 11600.00 5800.00 5800.00 5000.00 800.00
2 5800.00 3480.00 2320.00 3000.00 480.00
3 2320.00 2320.00 0.00 2000.00 320.00

Dos reglas que se rompen seguido:

  • El ImpSaldoAnt de cada parcialidad es el ImpSaldoInsoluto de la anterior. Guárdalo en tu base de datos al timbrar cada complemento.
  • La base del impuesto es proporcional a lo cobrado, no el total de la factura. Si cobras la mitad, la base es la mitad. Un ImporteDR que no sea BaseDR × TasaOCuotaDR se rechaza con 422 antes de gastar el timbre.

Cuerpo de la segunda parcialidad:

{
  "rfc_emisor": "EKU9003173C9",
  "comprobante": { "Serie": "PAG", "Folio": "2", "LugarExpedicion": "80290" },
  "receptor": {
    "Rfc": "MASO451221PM4",
    "Nombre": "MARIA OLIVIA MARTINEZ SAGAZ",
    "DomicilioFiscalReceptor": "80290",
    "RegimenFiscalReceptor": "605"
  },
  "pagos": [{
    "FechaPago": "2026-10-01T10:00:00",
    "FormaDePagoP": "03",
    "MonedaP": "MXN",
    "NumOperacion": "REF-88451999",
    "documentos": [{
      "IdDocumento": "UUID-DE-LA-FACTURA",
      "Serie": "FAC", "Folio": "20458",
      "NumParcialidad": "2",
      "ImpSaldoAnt": "5800.00",
      "ImpPagado": "3480.00",
      "traslados": [
        { "BaseDR": "3000.00", "ImpuestoDR": "002", "TipoFactorDR": "Tasa", "TasaOCuotaDR": "0.160000", "ImporteDR": "480.00" }
      ]
    }]
  }]
}

Ejercicio 4 — Un pago que liquida dos facturas, con retenciones

Tu cliente te hace una sola transferencia que cubre dos facturas: una parcialidad de mercancía y unos honorarios con retención de ISR e IVA. Va un solo complemento con dos documentos dentro del mismo pago.

Los honorarios: base $10,000, IVA $1,600, retención de ISR 10% ($1,000) y de IVA 10.6667% ($1,066.67) → te depositan $9,533.33. Más los $5,800 de la otra factura, la transferencia es de $15,333.33.

{
  "rfc_emisor": "EKU9003173C9",
  "comprobante": { "Serie": "PAG", "Folio": "3", "LugarExpedicion": "80290" },
  "receptor": {
    "Rfc": "MASO451221PM4",
    "Nombre": "MARIA OLIVIA MARTINEZ SAGAZ",
    "DomicilioFiscalReceptor": "80290",
    "RegimenFiscalReceptor": "605"
  },
  "pagos": [{
    "FechaPago": "2026-08-01T10:00:00",
    "FormaDePagoP": "03",
    "MonedaP": "MXN",
    "NumOperacion": "REF-88451203",
    "RfcEmisorCtaOrd": "BBA830831LJ2",
    "CtaOrdenante": "0123456789",
    "RfcEmisorCtaBen": "BMN930209927",
    "CtaBeneficiario": "9876543210",
    "documentos": [
      {
        "IdDocumento": "C1855BEE-324E-55EB-9154-91D890F31469",
        "Serie": "FAC", "Folio": "20458",
        "NumParcialidad": "2",
        "ImpSaldoAnt": "11600.00",
        "ImpPagado": "5800.00",
        "traslados": [
          { "BaseDR": "5000.00", "ImpuestoDR": "002", "TipoFactorDR": "Tasa", "TasaOCuotaDR": "0.160000", "ImporteDR": "800.00" }
        ]
      },
      {
        "IdDocumento": "8179B395-B827-5F92-92FF-BC38DD268C64",
        "Serie": "HON", "Folio": "118",
        "NumParcialidad": "1",
        "ImpSaldoAnt": "9533.33",
        "ImpPagado": "9533.33",
        "traslados": [
          { "BaseDR": "10000.00", "ImpuestoDR": "002", "TipoFactorDR": "Tasa", "TasaOCuotaDR": "0.160000", "ImporteDR": "1600.00" }
        ],
        "retenciones": [
          { "BaseDR": "10000.00", "ImpuestoDR": "001", "TipoFactorDR": "Tasa", "TasaOCuotaDR": "0.100000", "ImporteDR": "1000.00" },
          { "BaseDR": "10000.00", "ImpuestoDR": "002", "TipoFactorDR": "Tasa", "TasaOCuotaDR": "0.106667", "ImporteDR": "1066.67" }
        ]
      }
    ]
  }]
}

Un solo timbre cubre los dos documentos. monto_total_pagos te regresa 15333.33, y el PDF lista ambas facturas con sus impuestos y el desglose de retenciones.

Ejercicio 5 — Emitiste un complemento con datos equivocados

Un CFDI timbrado no se edita: se cancela y se sustituye.

Paso 1. Cancela el complemento incorrecto con motivo 02 (emitido con errores sin relación):

curl -X POST "https://api.idoo.dev/v1/timbrado-cfdi-4-0/facturas/cancelar" \
  -H "Authorization: Bearer {tu_api_key}" \
  -H "Content-Type: application/json" \
  -d '{"uuid":"UUID-DEL-COMPLEMENTO-MALO","motivo":"02"}'

Paso 2. Emite el complemento correcto relacionándolo con el cancelado:

{
  "rfc_emisor": "EKU9003173C9",
  "comprobante": { "Serie": "PAG", "Folio": "4", "LugarExpedicion": "80290" },
  "relacionados": { "TipoRelacion": "04", "uuids": ["UUID-DEL-COMPLEMENTO-MALO"] },
  "receptor": { "...": "..." },
  "pagos": [ "..." ]
}

La cancelación del complemento no cancela la factura original: son comprobantes distintos. Si además hay que corregir la factura, cancélala aparte con motivo 01 y su uuid_reemplaza.

Ejercicio 6 — Te devolvieron parte de la mercancía (nota de crédito)

Facturaste 10 costales de maíz a 250.00 (FAC, total 2,900.00 con IVA) y el cliente devuelve 2. No se cancela la factura: se emite una nota de crédito por lo devuelto, relacionada con ella. Es el mismo endpoint de siempre, facturas/timbrar_arreglo, con dos diferencias: TipoDeComprobante: "E" y el bloque relacionados.

Paso 1. Emite la factura original —el Ejercicio 1 sirve tal cual— y anota su uuid.

Paso 2. Timbra la nota de crédito por los 2 costales devueltos (500.00 + IVA):

curl -X POST "https://api.idoo.dev/v1/timbrado-cfdi-4-0/facturas/timbrar_arreglo" \
  -H "Authorization: Bearer {tu_api_key}" \
  -H "Content-Type: application/json" \
  -d '{
    "rfc_emisor": "EKU9003173C9",
    "comprobante": {
      "TipoDeComprobante": "E",
      "Serie": "NC", "Folio": "18",
      "FormaPago": "03", "MetodoPago": "PUE",
      "Moneda": "MXN", "LugarExpedicion": "80290"
    },
    "relacionados": { "TipoRelacion": "03", "uuids": ["UUID-DE-LA-FACTURA-DEL-PASO-1"] },
    "receptor": {
      "Rfc": "MASO451221PM4",
      "Nombre": "MARIA OLIVIA MARTINEZ SAGAZ",
      "DomicilioFiscalReceptor": "80290",
      "RegimenFiscalReceptor": "605",
      "UsoCFDI": "S01"
    },
    "conceptos": [{
      "ClaveProdServ": "10101502", "ClaveUnidad": "KGM", "Cantidad": "2",
      "Descripcion": "Devolución de 2 costales de maíz de 50 kg",
      "ValorUnitario": "250.00", "Importe": "500.00", "ObjetoImp": "02",
      "traslados": [
        { "Base": "500.00", "Impuesto": "002", "TipoFactor": "Tasa", "TasaOCuota": "0.160000", "Importe": "80.00" }
      ]
    }]
  }'

Respuesta:

{
  "valid": true,
  "status": 200,
  "message": "Factura timbrada exitosamente.",
  "data": {
    "uuid": "62C13FEA-C0FB-5DF1-9CE4-BEA6F41857B2",
    "rfc_emisor": "EKU9003173C9",
    "total": "580.00",
    "xml_base64": "PD94bWwgdmVyc2lvbj0iMS4wIi...",
    "pdf_base64": "JVBERi0xLjcK..."
  }
}

El XML sale con el nodo relacionado y el PDF rotulado "Nota de crédito":

<cfdi:CfdiRelacionados TipoRelacion="03">
  <cfdi:CfdiRelacionado UUID="UUID-DE-LA-FACTURA-DEL-PASO-1"/>
</cfdi:CfdiRelacionados>

Paso 3. El pdf_base64 ya viene en la respuesta; si lo quieres después, con el uuid:

curl -X POST "https://api.idoo.dev/v1/timbrado-cfdi-4-0/facturas/pdf" \
  -H "Authorization: Bearer {tu_api_key}" \
  -H "Content-Type: application/json" \
  -d '{"uuid":"UUID-DE-LA-NOTA-DE-CREDITO"}' \
  --output nota_credito.pdf

Cuatro cosas que suelen costar un timbre:

  • Los importes van en positivo. El TipoDeComprobante: "E" ya indica que resta; no mandes cantidades negativas.
  • TipoRelacion: 03 si hubo devolución física de mercancía, 01 si es un descuento o bonificación posterior sin devolución, 07 si estás aplicando un anticipo.
  • FormaPago y MetodoPago son obligatorios en tipo E, aunque no haya movimiento de dinero. Lo natural es repetir los de la factura original.
  • UsoCFDI: G02 (devoluciones, descuentos o bonificaciones) es el uso propio de una nota de crédito, pero exige un régimen empresarial en el receptor. Con un receptor de régimen 605 (sueldos y salarios) el PAC lo rechaza y va S01.

Si la factura corregida era PPD y ya le emitiste complementos de pago, la nota de crédito reduce el saldo por cobrar: el siguiente REP arranca con el ImpSaldoAnt ya disminuido. El servicio no lleva ese saldo por ti.

Ejercicio 7 — Cancelar una factura y sustituirla

Cuando el error no se arregla con una nota de crédito (RFC equivocado, conceptos mal, importe de más), se cancela con motivo 01 y se emite la sustituta. El orden importa: primero timbras la sustituta, luego cancelas, porque el motivo 01 exige el UUID del comprobante que reemplaza.

Paso 1. Timbra la factura correcta relacionándola con la que vas a cancelar:

curl -X POST "https://api.idoo.dev/v1/timbrado-cfdi-4-0/facturas/timbrar_arreglo" \
  -H "Authorization: Bearer {tu_api_key}" \
  -H "Content-Type: application/json" \
  -d '{
    "rfc_emisor": "EKU9003173C9",
    "comprobante": {
      "TipoDeComprobante": "I", "Serie": "A", "Folio": "1002",
      "FormaPago": "01", "MetodoPago": "PUE",
      "Moneda": "MXN", "LugarExpedicion": "80290"
    },
    "relacionados": { "TipoRelacion": "04", "uuids": ["UUID-DE-LA-FACTURA-MALA"] },
    "receptor": { "...": "los datos ya corregidos" },
    "conceptos": [ "..." ]
  }'

Paso 2. Cancela la anterior apuntando a la nueva:

curl -X POST "https://api.idoo.dev/v1/timbrado-cfdi-4-0/facturas/cancelar" \
  -H "Authorization: Bearer {tu_api_key}" \
  -H "Content-Type: application/json" \
  -d '{
    "uuid": "UUID-DE-LA-FACTURA-MALA",
    "motivo": "01",
    "uuid_reemplaza": "UUID-DE-LA-FACTURA-DEL-PASO-1"
  }'

Si ya cancelaste antes de timbrar la sustituta, no pasa nada: relacionar un CFDI cancelado se permite justamente con TipoRelacion: "04". Con cualquier otro tipo de relación un folio cancelado se rechaza con 422.


Errores frecuentes al emitir notas de crédito

Síntoma Causa Solución
422 "Un CFDI de egreso debe indicar qué comprobante corrige" Mandaste TipoDeComprobante: "E" sin relacionados Agrega el bloque con el UUID de la factura y su TipoRelacion
422 "El 'TipoRelacion' no es válido" Clave fuera del catálogo c_TipoRelacion Usa 01, 03 o 07 según el caso
422 "no tiene el formato de un UUID del SAT" Pusiste la serie-folio en vez del folio fiscal Va el uuid que devolvió el timbrado, no FAC-20458
422 "Los campos 'TipoRelacion', 'uuids' van dentro de 'relacionados'" Mandaste los campos sueltos en la raíz Envuélvelos: "relacionados": { "TipoRelacion": "...", "uuids": [...] }
422 "está cancelado. Solo el TipoRelacion '04'…" Relacionaste con 01 una factura ya cancelada Una factura cancelada no se corrige con nota de crédito: se sustituye con 04
422 "lo emitió el RFC …, no …" El folio relacionado es de otro emisor de tu cuenta Timbra la nota con el mismo rfc_emisor de la factura original
422 "el Importe declarado no corresponde a Cantidad × ValorUnitario" Redondeo propio en el concepto Importe = Cantidad × ValorUnitario, con 0.01 de tolerancia en pesos
El PAC rechaza el UsoCFDI Usaste G02 con un receptor de régimen 605 G02 exige régimen empresarial; con 605 va S01
400 "Fecha y hora de generación fuera de rango" Mandaste Fecha con la hora de tu servidor y expides al oeste del centro Omítela (la ponemos con el huso de tu LugarExpedicion) o mándala en hora local de ese CP

Errores frecuentes al emitir complementos

Síntoma Causa Solución
422 "ImpSaldoInsoluto no cuadra" Arrastraste mal el saldo entre parcialidades ImpSaldoAnt de esta parcialidad = ImpSaldoInsoluto de la anterior
422 "ImporteDR no cuadra: BaseDR × TasaOCuotaDR" Usaste la base del total de la factura en un pago parcial La base es proporcional a lo cobrado
422 "no admite la clave 99" Copiaste el FormaPago de la factura PPD En el complemento va la forma real del cobro
422 "FechaPago no puede ser posterior" Fecha del pago mayor que la del comprobante Revisa la zona horaria; FechaPago es cuando entró el dinero
422 "Un CFDI de pago no admite estos campos" Mandaste FormaPago o MetodoPago en comprobante Quítalos; van dentro de pagos
422 "no tiene forma de folio fiscal (UUID)" Pusiste la serie-folio en IdDocumento Va el uuid que te devolvió timbrar_arreglo, no FAC-20458
422 "ImpPagado no puede exceder el saldo anterior" Aplicaste a una factura más de lo que debía Reparte el pago entre varios documentos
Emites REP de una factura PUE Confusión de método de pago Las PUE no llevan complemento
Reseñas

Esta API todavía no tiene reseñas.

Escribir una reseña

Inicia sesión y suscríbete para poder reseñar.

Iniciar sesión