Emitir CFDI
Complemento de pago (REP)
Registrar cobros de facturas PPD y timbrar el CFDI de tipo Pago que exige el SAT.
Cuando emites una factura con metodoPago: "PPD", el SAT te obliga a emitir un
Recibo Electrónico de Pago por cada cobro que recibas. Es un CFDI de tipo
Pago: consume un timbre y solo se deshace cancelando.
Scopes: payments.create, más invoices.read, accounts.read y payments.read
para armarlo. Para cancelarlo, invoices.cancel.
Las reglas antes de escribir código
Un REP solo procede si:
- Cada factura relacionada está timbrada (
status: "stamped", conuuid). - Cada factura es PPD. Una PUE jamás lleva complemento.
- Todas las facturas de un mismo pago son del mismo cliente.
- La moneda del pago coincide con la de la cuenta bancaria destino.
- Los saldos cuadran por documento, a 2 decimales:
importeSaldoAnterior − importePagado = importeSaldoInsoluto. - El
montodel pago es la suma de todos losimportePagado, a 2 decimales. numParcialidadva consecutivo desde 1, por factura.
El receptor, el UsoCFDI (CP01) y la serie/folio del REP los pone Facty. Tú no
los mandas.
Paso 1 — encuentra las facturas por cobrar
curl -s "$FACTY_HOST/api/authenticated/$FACTY_ORG/clients/clx9aa…/ppd-invoices" \
-H "X-API-Key: $FACTY_KEY" | jq
O con el filtro general:
curl -s "$FACTY_HOST/api/authenticated/$FACTY_ORG/invoices?status=stamped&clientId=clx9aa…" \
-H "X-API-Key: $FACTY_KEY" | jq
Paso 2 — lee el saldo real. No lo calcules.
GET /api/authenticated/{org}/invoices/{invoiceId}/balance
{
"balance": {
"total": "11600.00",
"totalPagado": "0.00",
"saldoPendiente": "11600.00",
"parcialidadCount": 0,
"estadoPago": "outstanding" // outstanding | partial | paid | cancelled
},
"complementos": [ /* pagos ya aplicados a esta factura */ ]
}
De aquí sale todo lo que va en el documento del pago:
| Campo del documento | De dónde |
|---|---|
importeSaldoAnterior | balance.saldoPendiente |
numParcialidad | balance.parcialidadCount + 1 |
importeSaldoInsoluto | saldoPendiente − importePagado |
Si estadoPago ya es "paid", no hay nada que complementar.
Paso 3 — elige la cuenta destino
curl -s "$FACTY_HOST/api/authenticated/$FACTY_ORG/accounts" \
-H "X-API-Key: $FACTY_KEY" | jq
Scope: accounts.read. Usa una cuenta no archivada y con la misma moneda
que el pago. Filtros: ?kind=bank|cash, ?includeArchived=true.
Paso 4 — registra y timbra en una sola llamada
POST /api/authenticated/{org}/payments
{
"idempotencyKey": "mi-erp:prod:pago:9931",
"accountId": "clx3f8…",
"fechaPago": "2026-08-27",
"formaPago": "03",
"moneda": "MXN",
"monto": 5800,
"numOperacion": "TRX-4471",
"stamp": true,
"documents": [
{
"invoiceId": "clx9aa…",
"numParcialidad": 1,
"importeSaldoAnterior": 11600,
"importePagado": 5800,
"importeSaldoInsoluto": 5800
}
]
}
| Campo | Requerido | Notas |
|---|---|---|
accountId | sí | Cuenta bancaria o de efectivo donde se recibió el dinero. |
fechaPago | sí | "YYYY-MM-DD". Un ISO con offset se normaliza a UTC. |
formaPago | sí | Clave de 2 dígitos de c_FormaPago. Aquí no puede ser 99. |
monto | sí | Suma de los importePagado. |
documents | sí | Al menos uno. |
idempotencyKey | sí | 8–128 caracteres, derivada de tu registro de pago. |
moneda | no | Default "MXN". Si no es MXN, manda tipoCambio. |
numOperacion | no | Referencia bancaria. Opcional pero muy recomendable. |
stamp | no | Default true: registra y timbra. Con false solo registra. |
Respuesta 201:
{
"paymentId": "clxp1…",
"paymentInvoiceId": "clxi7…",
"uuid": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890"
}
uuid es el folio fiscal del REP. Con stamp: false viene null.
Sobre taxes
Cada documento admite un arreglo taxes para desglosar los ImpuestosDR.
Omítelo. Facty los deriva de los conceptos de la factura original y los
escala proporcionalmente al importe pagado, que es lo que exige el SAT.
Mándalo solo si el usuario pide explícitamente un desglose distinto. Y cuidado:
[] no significa "calcúlalo tú", significa "sin impuestos" (ObjetoImpDR 01), lo
cual provoca un rechazo del PAC si la factura sí traía IVA.
{
"taxes": [
{ "name": "IVA", "rate": 0.16, "base": 5000, "total": 800 }
]
}
rate: null expresa un traslado exento (sin tasa ni importe).
isRetention: true marca una retención.
Timbrar después
Si registraste con stamp: false:
POST /api/authenticated/{org}/payments/{paymentId}/stamp
{ "idempotencyKey": "mi-erp:prod:pago:9931:stamp" }
Solo aplica a un pago en estado registered y no cancelado. Si ya tiene REP,
409.
Un pago que cubre varias facturas
Un solo pago puede aplicar a varias facturas del mismo cliente:
{
"idempotencyKey": "mi-erp:prod:pago:9932",
"accountId": "clx3f8…",
"fechaPago": "2026-08-27",
"formaPago": "03",
"monto": 20000,
"documents": [
{ "invoiceId": "clxA…", "numParcialidad": 2, "importeSaldoAnterior": 11600, "importePagado": 11600, "importeSaldoInsoluto": 0 },
{ "invoiceId": "clxB…", "numParcialidad": 1, "importeSaldoAnterior": 15000, "importePagado": 8400, "importeSaldoInsoluto": 6600 }
]
}
Listar pagos
GET /api/authenticated/{org}/payments?limit=25&cursor=…
Scope: payments.read
{
"payments": [
{
"id": "clxp1…",
"fechaPago": "2026-08-27T00:00:00.000Z",
"formaPago": "03",
"moneda": "MXN",
"monto": 5800,
"numOperacion": "TRX-4471",
"status": "registered",
"uuid": "A1B2C3D4-…",
"documentsCount": 1,
"createdAt": "2026-08-27T16:11:03.000Z"
}
],
"nextCursor": null
}
Descargar el REP
Es un CFDI como cualquier otro. Usa el paymentInvoiceId:
GET /api/authenticated/{org}/invoices/{paymentInvoiceId}/pdf
GET /api/authenticated/{org}/invoices/{paymentInvoiceId}/xml
GET /api/authenticated/{org}/invoices/{paymentInvoiceId}/cfdi
Cancelar un REP
POST /api/authenticated/{org}/payments/{paymentId}/cancel
{ "motivo": "02" }
Scope: invoices.cancel — no payments.create. Cancelar aquí es anular un CFDI
ante el SAT y consume un timbre; debe requerir la misma autoridad que cancelar
cualquier otro comprobante. El motivo 01 exige además folioSustitucion con el
UUID del complemento que lo sustituye.
Errores frecuentes
code / HTTP | Causa habitual |
|---|---|
422 | Los saldos no cuadran, la factura es PUE, hay clientes distintos en un mismo pago, o la moneda no coincide con la cuenta. |
409 | Ese pago ya tiene REP, o la llave de idempotencia está en vuelo. |
402 INSUFFICIENT_TIMBRES | Sin timbres. Manda al usuario a comprar. |
412 | Falta perfil fiscal o CSD. |