Documentación / API / Complementos de pago (REP)

Timbrar un complemento de pago de varias facturas

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

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.

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 pago, por ejemplo el id de la transacción en tu sistema.

Cuerpo (JSON)

CampoTipoDescripción
formaPagoreqstringClave de c_FormaPago con la que se recibió el pago (03 transferencia, 04 tarjeta…). No puede ser 99.
fechaPagostringFecha y hora del pago en formato AAAA-MM-DDTHH:MM:SS, hora de Ciudad de México. Default: ahora.
montonumberImporte total recibido. Opcional: si lo mandas, tiene que ser igual a la suma de documents[].importePagado; si lo omites, es esa suma.
documentsreqobject[]De 1 a 100 facturas que liquida el pago.
documents[].invoiceIdreqstringId de una factura PPD timbrada. Sin repetir.
documents[].importePagadoreqnumberParte del pago que se aplica a esa factura (ImpPagado). Mayor a cero y no mayor a su saldo pendiente.
documents[].numParcialidadintegerParcialidad de esa factura. Se asigna automáticamente si la omites; si la mandas, se valida contra la secuencia.

Ejemplos

cURL
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 }
    ]
  }'
Node.js
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

201 Created
{
  "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

StatuscodeCuándo aparece
400validation_errorAlguna 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.
401unauthorizedFalta el header Authorization o la llave es inválida.
402insufficient_creditsNo hay saldo suficiente para el timbre.
404not_foundAlguna factura no existe en tu cuenta.
422sat_rejectedEl SAT rechazó el complemento.

Notas

  • invoiceId, numParcialidad, saldoAnterior y saldoInsoluto del recurso son los de la primera factura; monto es el total del pago. El detalle por factura está en documents.
  • El CFDI lleva los documentos con la primera factura de documents al 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_payment acepta el mismo arreglo documents en lugar de invoiceId y monto.

Continuar