Todo sobre facturación CFDI
Aprende cómo automatizar y facilitar la facturación CFDI de ventas de tu negocio.
Cómo emitir un REP para varias facturas PPD con un solo pago (CFDI 4.0, Pagos 2.0)
Cuando un mismo pago abona varias facturas PPD, el SAT admite un solo CFDI de pago (complemento Pagos 2.0) con un DoctoRelacionado por factura.
Desde la v1.46.0 de la API de CFDI Express, ese CFDI se timbra con POST /v1/payments. El arreglo documents lleva cada factura y el importePagado que le aplicas. El REP cuesta un timbre, no uno por factura.
La guía de complementos en la app está en Complementos de pago. Este post es el contrato de la API.
Cuándo usar un solo REP
Respuesta corta: cuando un solo pago recibido liquida o abona varias facturas PPD del mismo emisor, el mismo RFC receptor y la misma moneda.
Cada factura ya tiene que estar timbrada, en PPD y con saldo pendiente. En documents mandas de 1 a 100 facturas. El caso de este post es el pago que cubre varias.
Si el pago es de una sola factura, POST /v1/invoices/{id}/payments sigue igual y ahora pasa por la misma ruta por dentro. POST /v1/payments también acepta un solo documento.
Reglas del pago
Todas se revisan antes de gastar el timbre.
- Mismo emisor. Todas las facturas salen del mismo comercio. Si no:
Invoice X was issued by a different merchant than Y. - Mismo RFC receptor. La comparación no distingue mayúsculas. Si no:
Invoice X has a different receiver RFC than Y. - Misma moneda. Un REP tiene una sola
MonedaP. Si mezclas:Invoice X is in USD; Y is in MXN. El request no trae tipo de cambio niEquivalenciaDR: en el XML ese atributo va fijo en"1", porque todas las facturas comparten la moneda del pago. - De 1 a 100 documentos. Cada uno es
{ invoiceId, importePagado, numParcialidad? }.invoiceIdes una facturainv_…timbrada y PPD.importePagadoes elImpPagadode esa factura: mayor a cero y no mayor a su saldo pendiente (tolerancia de 0.005). - Sin repetir factura.
Invoice X appears more than once in documents. - Parcialidades por factura. Cada factura lleva su propio
numParcialidad. Si lo omites, se asigna. Si lo mandas, se valida contra la secuencia de esa factura. Un REP que relaciona varias facturas cuenta en la secuencia de cada una. - Un timbre por REP. Cancelar el REP también cuesta un timbre. Al cancelarlo, el saldo pendiente de cada factura recupera lo que ese REP le había aplicado.
formaPago es obligatorio: de 2 o 3 dígitos, clave de c_FormaPago, y no puede ser 99. El ejemplo de la documentación es "03" (transferencia). El catálogo está en formas de pago.
fechaPago es opcional, en hora de Ciudad de México: AAAA-MM-DDTHH:mm:ss. Si lo omites, es el momento del request.
monto es opcional y positivo. Si lo mandas, tiene que ser igual a la suma de documents[].importePagado, con tolerancia de ±0.005. Si lo omites, el total es esa suma.
Ejemplo: $5.80 = 2.32 + 2.32 + 1.16
El header Idempotency-Key es obligatorio (de 1 a 255 caracteres). Los invoiceId de abajo son el ejemplo publicado en la referencia; los importes son los del pago de $5.80.
curl https://api.cfdi.express/v1/payments \
-H "Authorization: Bearer sk_test_..." \
-H "Idempotency-Key: payment_88240" \
-H "Content-Type: application/json" \
-d '{
"formaPago": "03",
"fechaPago": "2026-10-30T12:00:00",
"monto": 5.80,
"documents": [
{ "invoiceId": "inv7k3q9x2m4v1t8p6d0n5cb", "importePagado": 2.32 },
{ "invoiceId": "inv2p8d4n6c0b5rzmkq2f8v3", "importePagado": 2.32 },
{ "invoiceId": "inv5c0btpay9x2m6q4k1v3p8", "importePagado": 1.16 }
]
}'
numParcialidad no va en el ejemplo: el servidor asigna la que sigue en cada factura. Puedes mandarla en la entrada de esa factura si ya la conoces.
Si el SAT timbra en la misma petición, la respuesta es 201. Si la cola sigue timbrando, es 202: consultas GET /v1/payments/{id} hasta que status sea stamped.
En el ejemplo, monto queda en 5.80 y documents trae tres entradas. Cada una sale con numParcialidad 1 y saldoInsoluto 0: el pago cubre el saldo completo de las tres, así que saldoAnterior coincide con importePagado. Este JSON es el ejemplo publicado en la referencia; los ids, el UUID y las fechas son de muestra:
{
"id": "pay3v1t8p6d0n5cbinv7k3q9x",
"object": "payment",
"livemode": false,
"status": "stamped",
"invoiceId": "inv7k3q9x2m4v1t8p6d0n5cb",
"merchantId": "mkq2f8v3x1t7p9d4n6c0b5rz",
"uuid": "33d7d158-5f9c-49ef-b1a1-b9c5557efea0",
"numParcialidad": 1,
"monto": 5.80,
"formaPago": "03",
"fechaPago": "2026-10-30T18:00:00.000Z",
"saldoAnterior": 2.32,
"saldoInsoluto": 0,
"currency": "MXN",
"documents": [
{
"invoiceId": "inv7k3q9x2m4v1t8p6d0n5cb",
"numParcialidad": 1,
"importePagado": 2.32,
"saldoAnterior": 2.32,
"saldoInsoluto": 0
},
{
"invoiceId": "inv2p8d4n6c0b5rzmkq2f8v3",
"numParcialidad": 1,
"importePagado": 2.32,
"saldoAnterior": 2.32,
"saldoInsoluto": 0
},
{
"invoiceId": "inv5c0btpay9x2m6q4k1v3p8",
"numParcialidad": 1,
"importePagado": 1.16,
"saldoAnterior": 1.16,
"saldoInsoluto": 0
}
],
"files": { "status": "ready" },
"cancellation": null,
"stampedAt": "2026-10-30T18:05:12.006Z",
"createdAt": "2026-10-30T18:05:10.554Z"
}
invoiceId, numParcialidad, saldoAnterior y saldoInsoluto en la raíz son los de la primera factura. monto es el total del pago. El detalle de cada una está en documents[]: invoiceId, numParcialidad, importePagado, saldoAnterior y saldoInsoluto.
fechaPago del request es hora local de Ciudad de México (2026-10-30T12:00:00). En el recurso de este ejemplo queda el instante en UTC (2026-10-30T18:00:00.000Z).
El mismo documents[] viene en el GET, en el listado y en los webhooks payment.*. GET /v1/payments?invoiceId= encuentra el REP con cualquiera de sus facturas, no solo con la primera.
En el XML de este ejemplo hay un Pago y tres DoctoRelacionado. Cada documento usa el UUID de su factura en IdDocumento, ImpPagado con el importePagado, y sus saldos. EquivalenciaDR es "1". Los impuestos de cada documento se prorratean desde los de su factura. ImpuestosP y Totales son la suma, en centavos. Con estas tres facturas de IVA 16% (bases 2.00, 2.00 y 1.00), el TrasladoP queda con base 5.00 e IVA 0.80, y los totales traen MontoTotalPagos 5.80, TotalTrasladosBaseIVA16 5.00 y TotalTrasladosImpuestoIVA16 0.80.
En el CFDI, el primer documento es la primera factura de documents y el resto va ordenado por id.
Mandar monto 6 contra estos importes responde 400: monto 6.00 must equal the sum of documents[].importePagado (5.80).
Para probar, una llave sk_test_ en dash.cfdi.express. La referencia interactiva está en api.cfdi.express/docs.
Cómo se cuentan las parcialidades
Respuesta corta: por factura, no por REP.
La siguiente numParcialidad de una factura es uno más que sus REP en stamping o stamped. Un REP de varias facturas entra en la cuenta de cada factura que relaciona.
Si la factura A ya tiene dos REP en stamping o stamped y la B no tiene ninguno, en el mismo POST /v1/payments la parcialidad de A es 3 y la de B es 1. Si mandas otro número, la API responde 400: Invoice {id}: numParcialidad {n} is out of sequence — expected {N}.
Errores de validación
Estas respuestas llegan antes de gastar un timbre. Las de 400 usan el código validation_error. La factura que no existe es 404 not_found. Los textos con prefijo Invoice {id}: son los de un REP con más de un documento.
| Situación | HTTP | Qué responde |
|---|---|---|
Factura repetida en documents |
400 | Invoice X appears more than once in documents |
| Factura que no existe en la cuenta | 404 not_found |
No invoice X |
| No es PPD | 400 | Invoice X: Payment complements (REP) only apply to PPD invoices |
| No está timbrada | 400 | Invoice X: Invoice is {status} — REPs require a stamped invoice |
| Saldo pendiente en 0 | 400 | Invoice X: Invoice is fully paid (saldo pendiente is 0) |
importePagado mayor al saldo |
400 overpay |
Invoice X: Payment of {importe} exceeds the outstanding balance {saldo}. El cuerpo trae subcode overpay, saldoPendiente e invoiceId |
numParcialidad fuera de secuencia |
400 | expected N |
| Otro emisor | 400 | Invoice X was issued by a different merchant than Y |
| Otro RFC receptor | 400 | Invoice X has a different receiver RFC than Y |
| Otra moneda | 400 | Invoice X is in USD; Y is in MXN |
monto distinto de la suma |
400 | monto A must equal the sum of documents[].importePagado (B) |
formaPago que no existe |
400 | Unknown forma de pago "{clave}" |
formaPago 99 |
400 | A REP requires the real forma de pago (not 99 'Por definir') |
Y en los mensajes de emisor, RFC y moneda es la primera factura del arreglo.
Otros códigos del endpoint: 402 insufficient_credits si no hay saldo para el timbre, y 422 si el SAT rechaza el complemento o si se reutiliza la llave de idempotencia.
Cancelar el REP y cancelar la factura
Respuesta corta: cancelar el REP devuelve el saldo de todas sus facturas. Una factura ligada a un REP vigente no se cancela.
POST /v1/payments/{id}/cancel usa los mismos motivos 01–04 que una factura. La guía está en cancelar complemento de pago. En el ejemplo de $5.80, la cancelación aceptada (respuesta 202, cancellation.status accepted) deja los saldos pendientes otra vez en 2.32, 2.32 y 1.16: cada factura recupera su importePagado.
Un REP anterior, de una sola factura y sin renglones de documento, recupera el monto del pago.
Cancelar una factura PPD que tenga un REP vigente —stamping, stamped o cancel_pending— responde 400 con subcode cancel_reps_first. Eso incluye las facturas que no son la primera del REP. Primero se cancela el complemento; después, la factura.
Desde el servidor MCP
create_payment en el servidor MCP timbra el mismo REP. Para varias facturas el argumento es documents (cada entrada con invoiceId, importePagado y, si la conoces, numParcialidad). monto y fechaPago siguen siendo opcionales; formaPago es el real, no 99.
Para una sola factura el argumento es invoiceId más monto. Las dos formas no se combinan. Si mandas invoiceId junto con documents, la herramienta responde: Pass either invoiceId (one invoice) or documents (several invoices), not both. Si con documents pones numParcialidad en la raíz, responde: With documents, set numParcialidad on each document entry instead.
El número de herramientas no cambia. cancel_payment restaura el saldo de cada factura que el REP liquidaba. El sandbox del MCP está en https://api.cfdi.express/mcp/test.
Una instrucción que corresponde a este pago:
Registra un pago con forma de pago 03 del 2026-10-30T12:00:00 que liquida tres facturas PPD: 2.32, 2.32 y 1.16, en un solo REP
Preguntas frecuentes
¿Cuándo emito un solo REP para varias facturas?
Cuando un solo pago abona varias facturas PPD del mismo emisor, el mismo RFC receptor y la misma moneda. El endpoint es POST /v1/payments, con un DoctoRelacionado por factura.
¿Cuántas facturas acepta?
De 1 a 100 en documents. Cada invoiceId va una sola vez. importePagado es mayor a cero y no pasa el saldo pendiente de esa factura.
¿Cuántos timbres cuesta? Uno por REP, no uno por factura. Cancelar el REP cuesta otro timbre.
¿Las parcialidades se numeran por el pago o por la factura?
Por factura. La siguiente es uno más que los REP de esa factura en stamping o stamped. Un REP de varias facturas cuenta en cada una. Si omites numParcialidad, se asigna; si la mandas y no sigue la secuencia, responde 400 con expected N.
¿Qué saldos trae la respuesta?
documents[] trae, por factura, numParcialidad, importePagado, saldoAnterior y saldoInsoluto. En la raíz, invoiceId, numParcialidad, saldoAnterior y saldoInsoluto son los de la primera factura, y monto es el total.
¿Qué pasa si cancelo el REP?
Cada factura recupera el importePagado que ese REP le aplicó. En el ejemplo de $5.80 los saldos vuelven a 2.32, 2.32 y 1.16.
¿Puedo cancelar una factura que ya está en un REP?
Si el REP está vigente (stamping, stamped o cancel_pending), la cancelación de la factura responde 400 con subcode cancel_reps_first, aunque esa factura no sea la primera del REP.
¿Puedo pedirlo desde un agente?
Sí. create_payment acepta documents. No se combina con invoiceId y monto. Con documents, numParcialidad va en cada entrada.
Timbrar el REP de varias facturas
- Junta las facturas PPD timbradas del mismo emisor, el mismo RFC y la misma moneda.
- Manda
POST /v1/paymentsconIdempotency-Key,formaPagoydocuments(invoiceIdeimportePagadopor factura). El ejemplo de $5.80 usaformaPago"03". - Lee
documents[]:saldoAnterior,saldoInsolutoynumParcialidadde cada factura. - Contrasta el cuerpo con Timbrar un complemento de pago de varias facturas.
El contexto de la API está en el anuncio de lanzamiento y en la landing.
¿Dudas de integración? Agenda una demo o escribe a hola@cfdi.express.
