Timbrar un complemento de pago de varias facturas
Registra un solo pago que cubre varias facturas PPD y timbra un solo complemento de pago, con un documento relacionado por factura.
Desde la v1.46.0. Si recibes un pago que liquida o abona varias facturas PPD del mismo cliente, el SAT permite un solo CFDI de pago con un DoctoRelacionado por factura. Este endpoint timbra ese CFDI: consume un timbre en lugar de uno por factura.
En documents manda cada factura con el importePagado que le aplicas. Cada factura lleva su propia numParcialidad, su saldoAnterior y su saldoInsoluto, calculados en el servidor igual que en POST /v1/invoices/{id}/payments. Los impuestos de cada documento se prorratean con los de su factura, y los totales del pago (ImpuestosP y Totales) son la suma exacta de los documentos.
Todas las facturas tienen que ser PPD, estar timbradas y ser del mismo emisor, el mismo RFC receptor y la misma moneda. Los sobrepagos, las facturas repetidas y las parcialidades fuera de secuencia se rechazan con 400 antes de gastar un timbre.
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
| Campo | Tipo | Descripción |
|---|---|---|
| Idempotency-Keyreq | string | Llave única por pago, por ejemplo el id de la transacción en tu sistema. |
Cuerpo (JSON)
| Campo | Tipo | Descripción |
|---|---|---|
| formaPagoreq | string | Clave de c_FormaPago con la que se recibió el pago (03 transferencia, 04 tarjeta…). No puede ser 99. |
| fechaPago | string | Fecha y hora del pago en formato AAAA-MM-DDTHH:MM:SS, hora de Ciudad de México. Default: ahora. |
| monto | number | Importe total recibido. Opcional: si lo mandas, tiene que ser igual a la suma de documents[].importePagado; si lo omites, es esa suma. |
| documentsreq | object[] | De 1 a 100 facturas que liquida el pago. |
| documents[].invoiceIdreq | string | Id de una factura PPD timbrada. Sin repetir. |
| documents[].importePagadoreq | number | Parte del pago que se aplica a esa factura (ImpPagado). Mayor a cero y no mayor a su saldo pendiente. |
| documents[].numParcialidad | integer | Parcialidad de esa factura. Se asigna automáticamente si la omites; si la mandas, se valida contra la secuencia. |
Ejemplos
curl -X POST 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 }
]
}'const res = await fetch("https://api.cfdi.express/v1/payments", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CFDI_API_KEY}`,
"Idempotency-Key": `payment_${transaction.id}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
formaPago: "03",
// Una entrada por factura que cubre el pago.
documents: transaction.allocations.map((a) => ({
invoiceId: a.invoiceId,
importePagado: a.amount,
})),
}),
});
const rep = await res.json();
for (const doc of rep.documents) {
console.log(doc.invoiceId, "saldo insoluto:", doc.saldoInsoluto);
}Respuesta
{
"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"
}Errores
| Status | code | Cuándo aparece |
|---|---|---|
| 400 | validation_error | Alguna factura no es PPD, no está timbrada, ya está liquidada, recibe más que su saldo (subcode overpay, con invoiceId) o viene repetida. Las facturas son de otro emisor, otro RFC receptor u otra moneda. monto no es la suma de documents[].importePagado. Una parcialidad está fuera de secuencia. formaPago es 99 o no existe. |
| 401 | unauthorized | Falta el header Authorization o la llave es inválida. |
| 402 | insufficient_credits | No hay saldo suficiente para el timbre. |
| 404 | not_found | Alguna factura no existe en tu cuenta. |
| 422 | sat_rejected | El SAT rechazó el complemento. |
Notas
invoiceId,numParcialidad,saldoAnteriorysaldoInsolutodel recurso son los de la primera factura;montoes el total del pago. El detalle por factura está endocuments.- El CFDI lleva los documentos con la primera factura de
documentsal inicio y el resto ordenado por id. - GET /v1/payments?invoiceId= encuentra el REP con cualquiera de sus facturas. Ninguna de ellas se puede cancelar mientras el REP esté vigente; cancelar el REP restaura el saldo de todas.
- Por MCP,
create_paymentacepta el mismo arreglodocumentsen lugar deinvoiceIdymonto.
