Cuando emites facturas con método de pago PPD (Pago en Parcialidades o Diferido), no basta con la factura inicial. Cada vez que recibes un pago, debes generar un Complemento de Pago — un CFDI que documenta la transacción y liga el dinero con la factura original.
Esta guía técnica te enseña a automatizar la generación de complementos de pago usando la API de gigstack, desde casos simples hasta escenarios complejos con múltiples facturas y monedas.
El Complemento de Pago (también llamado Recibo Electrónico de Pago o REP) es un CFDI de tipo "P" (Pago) que registra un pago recibido vinculado a una o más facturas PPD previas.
Un Complemento de Pago contiene:
La API ofrece un endpoint dedicado para registrar pagos y generar complementos automáticamente.
POST https://api.gigstack.io/v2/payments/register
Authorization: Bearer tu_jwt_token
Content-Type: application/json
{
"invoice_uuid": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890",
"amount": 5800.00,
"payment_date": "2026-02-15",
"payment_form": "03",
"operation_number": "REF123456",
"source_bank_rfc": "BNET930701UJ5",
"source_account": "0123456789",
"destination_bank_rfc": "BMN930209927",
"destination_account": "9876543210"
}
{
"message": "Payment registered successfully",
"data": {
"payment_id": "pay_xyz789",
"complement_uuid": "B2C3D4E5-F6A7-8901-BCDE-F12345678901",
"complement_xml_url": "https://storage.gigstack.io/...",
"complement_pdf_url": "https://storage.gigstack.io/...",
"invoice_balance": {
"previous": 11600.00,
"paid": 5800.00,
"remaining": 5800.00
}
}
}
import client from './gigstack-client.js';
async function registerPayment(paymentData) {
const response = await client.post('/payments/register', {
invoice_uuid: paymentData.invoiceUuid,
amount: paymentData.amount,
payment_date: paymentData.date,
payment_form: paymentData.paymentForm,
operation_number: paymentData.reference || null,
source_bank_rfc: paymentData.sourceBankRfc || null,
source_account: paymentData.sourceAccount || null,
destination_bank_rfc: paymentData.destBankRfc || null,
destination_account: paymentData.destAccount || null
});
return response.data;
}
// Uso básico
const payment = await registerPayment({
invoiceUuid: 'A1B2C3D4-E5F6-7890-ABCD-EF1234567890',
amount: 5800.00,
date: '2026-02-15',
paymentForm: '03' // Transferencia electrónica
});
console.log('Complemento generado:', payment.complement_uuid);
Los códigos más comunes para el parámetro payment_form:
async function pagoUnicoCompleto() {
// 1. Crear factura PPD
const invoice = await createInvoice({
clientId: 'cli_abc123',
items: [{
description: 'Servicio de consultoría',
quantity: 1,
unitPrice: 10000.00,
claveSAT: '80111501',
claveUnidad: 'E48'
}],
formaPago: '99', // Por definir
metodoPago: 'PPD', // Pago en parcialidades o diferido
usoCFDI: 'G03'
});
console.log('Factura PPD creada:', invoice.uuid);
console.log('Total:', invoice.total); // 11,600 (con IVA)
// 2. Cuando llega el pago, registrarlo
const payment = await registerPayment({
invoiceUuid: invoice.uuid,
amount: 11600.00, // Monto total
date: '2026-02-20',
paymentForm: '03'
});
console.log('Complemento emitido:', payment.complement_uuid);
console.log('Saldo restante:', payment.invoice_balance.remaining); // 0
}
async function pagosEnParcialidades() {
// Factura por $100,000 + IVA = $116,000
const invoice = await createInvoice({
clientId: 'cli_proyecto_grande',
items: [{
description: 'Desarrollo de sistema a medida',
quantity: 1,
unitPrice: 100000.00,
claveSAT: '43232301',
claveUnidad: 'E48'
}],
formaPago: '99',
metodoPago: 'PPD',
usoCFDI: 'G03'
});
// Pago 1: Anticipo 30% (entrega del proyecto)
const pago1 = await registerPayment({
invoiceUuid: invoice.uuid,
amount: 34800.00, // 30% de 116,000
date: '2026-02-01',
paymentForm: '03',
reference: 'ANTICIPO-001'
});
console.log('Pago 1 - Saldo restante:', pago1.invoice_balance.remaining);
// Saldo: 81,200
// Pago 2: Segundo pago 40% (avance del proyecto)
const pago2 = await registerPayment({
invoiceUuid: invoice.uuid,
amount: 46400.00, // 40% de 116,000
date: '2026-03-01',
paymentForm: '03',
reference: 'AVANCE-001'
});
console.log('Pago 2 - Saldo restante:', pago2.invoice_balance.remaining);
// Saldo: 34,800
// Pago 3: Finiquito 30% (entrega final)
const pago3 = await registerPayment({
invoiceUuid: invoice.uuid,
amount: 34800.00, // 30% restante
date: '2026-04-01',
paymentForm: '03',
reference: 'FINIQUITO-001'
});
console.log('Pago 3 - Saldo restante:', pago3.invoice_balance.remaining);
// Saldo: 0 (factura liquidada)
return { invoice, pagos: [pago1, pago2, pago3] };
}
async function pagoMultiplesFacturas() {
// El cliente tiene 3 facturas pendientes y hace un solo pago
const facturasPendientes = [
{ uuid: 'UUID-FACTURA-1', saldo: 5000.00 },
{ uuid: 'UUID-FACTURA-2', saldo: 3000.00 },
{ uuid: 'UUID-FACTURA-3', saldo: 2000.00 }
];
const montoTotal = 10000.00; // Pago total recibido
// Registrar el pago distribuyéndolo entre facturas
const payments = await client.post('/payments/register-multiple', {
payment_date: '2026-02-15',
payment_form: '03',
total_amount: montoTotal,
documents: facturasPendientes.map(f => ({
uuid: f.uuid,
amount: f.saldo // Paga el saldo completo de cada una
}))
});
// Se genera UN complemento que incluye las 3 facturas
console.log('Complemento único:', payments.complement_uuid);
return payments;
}
async function pagoEnDolares() {
// Factura emitida en USD
const invoice = await createInvoice({
clientId: 'cli_internacional',
items: [{
description: 'Consultoría internacional',
quantity: 10,
unitPrice: 500.00, // USD
claveSAT: '80111501',
claveUnidad: 'E48'
}],
formaPago: '99',
metodoPago: 'PPD',
usoCFDI: 'G03',
currency: 'USD'
});
console.log('Total USD:', invoice.total); // 5,800 USD
// El cliente paga en USD
// gigstack obtiene el tipo de cambio de la fecha del pago
const payment = await registerPayment({
invoiceUuid: invoice.uuid,
amount: 5800.00,
date: '2026-02-20',
paymentForm: '03',
currency: 'USD'
});
console.log('Tipo de cambio usado:', payment.exchange_rate);
console.log('Equivalente MXN:', payment.amount_mxn);
}
Si usas Stripe para cobrar, puedes automatizar completamente los complementos:
// webhook-stripe.js
import Stripe from 'stripe';
import { registerPayment } from './payments.js';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
// Escucha eventos de Stripe
app.post('/webhooks/stripe', async (req, res) => {
const event = stripe.webhooks.constructEvent(
req.body,
req.headers['stripe-signature'],
process.env.STRIPE_WEBHOOK_SECRET
);
if (event.type === 'payment_intent.succeeded') {
const paymentIntent = event.data.object;
// Busca la factura relacionada (guardaste el UUID en metadata)
const invoiceUuid = paymentIntent.metadata.gigstack_invoice_uuid;
if (invoiceUuid) {
// Genera el complemento automáticamente
const payment = await registerPayment({
invoiceUuid: invoiceUuid,
amount: paymentIntent.amount / 100, // Stripe usa centavos
date: new Date().toISOString().split('T')[0],
paymentForm: mapStripeToSATPaymentForm(paymentIntent.payment_method_types[0]),
reference: paymentIntent.id
});
console.log('Complemento auto-generado:', payment.complement_uuid);
}
}
res.json({ received: true });
});
function mapStripeToSATPaymentForm(stripeMethod) {
const map = {
'card': '04', // Tarjeta de crédito
'card_present': '04',
'bank_transfer': '03', // Transferencia
'oxxo': '01' // Efectivo
};
return map[stripeMethod] || '99';
}
try {
await registerPayment({
invoiceUuid: 'UUID-123',
amount: 15000.00, // Más del saldo pendiente
date: '2026-02-15',
paymentForm: '03'
});
} catch (error) {
if (error.message.includes('exceeds balance')) {
// Obtener saldo actual
const invoice = await getInvoice('UUID-123');
console.log('Saldo pendiente:', invoice.balance);
// Ajustar monto o notificar al usuario
}
}
try {
await registerPayment({
invoiceUuid: 'UUID-FACTURA-PUE', // Factura con PUE
amount: 5000.00,
date: '2026-02-15',
paymentForm: '03'
});
} catch (error) {
if (error.message.includes('PPD')) {
console.log('Solo facturas PPD requieren complemento de pago');
// Las facturas PUE ya están pagadas por definición
}
}
// El complemento debe emitirse dentro del mes siguiente al pago
try {
await registerPayment({
invoiceUuid: 'UUID-123',
amount: 5000.00,
date: '2025-12-01', // Fecha muy antigua
paymentForm: '03'
});
} catch (error) {
if (error.message.includes('date')) {
console.log('La fecha del pago está fuera del rango permitido');
}
}
async function getInvoicePayments(uuid) {
const response = await client.get('/invoices/' + uuid + '/payments');
return response.data.map(payment => ({
id: payment.id,
complementUuid: payment.complement_uuid,
amount: payment.amount,
date: payment.payment_date,
form: payment.payment_form
}));
}
// Uso
const payments = await getInvoicePayments('UUID-FACTURA');
console.log('Total de pagos:', payments.length);
console.log('Monto total pagado:', payments.reduce((sum, p) => sum + p.amount, 0));
async function getInvoicesWithBalance(clientId) {
const response = await client.get('/invoices?client=' + clientId + '&has_balance=true');
return response.data.filter(inv => inv.payment_method === 'PPD');
}
// Útil para mostrar estado de cuenta al cliente
const pendientes = await getInvoicesWithBalance('cli_abc123');
pendientes.forEach(inv => {
console.log(inv.folio + ': Pendiente $' + inv.balance + ' de $' + inv.total);
});
Aunque tienes hasta el mes siguiente, registra pagos inmediatamente para evitar acumulación y errores.
Incluye número de operación y cuentas bancarias. Facilita conciliación y auditorías.
async function validateAndRegisterPayment(invoiceUuid, amount, date, form) {
// Obtener factura y validar
const invoice = await getInvoice(invoiceUuid);
if (invoice.payment_method !== 'PPD') {
throw new Error('Factura no requiere complemento de pago');
}
if (amount > invoice.balance) {
throw new Error('Monto (' + amount + ') excede saldo pendiente (' + invoice.balance + ')');
}
// Registrar
return await registerPayment({ invoiceUuid, amount, date, paymentForm: form });
}
async function registerPaymentWithNotification(paymentData) {
const payment = await registerPayment(paymentData);
// Notificar al cliente
await sendEmail({
to: payment.client_email,
subject: 'Complemento de pago emitido',
body: 'Se ha registrado tu pago por $' + payment.amount,
attachments: [
{ url: payment.complement_pdf_url, name: 'complemento_pago.pdf' },
{ url: payment.complement_xml_url, name: 'complemento_pago.xml' }
]
});
// Notificar internamente
await slack.send('#cobranza', 'Pago registrado: ' + paymentData.invoiceUuid);
return payment;
}
Sí, puedes registrar pagos con fecha anterior (backdating) siempre que esté dentro del periodo permitido por el SAT. gigstack obtiene automáticamente el tipo de cambio de la fecha del pago para facturas en moneda extranjera.
La API rechazará el registro si el monto excede el saldo pendiente. Debes registrar solo el saldo pendiente y manejar el excedente como anticipo para futuras facturas o devolución.
Sí, los complementos de pago se pueden cancelar como cualquier CFDI. Al cancelar, el saldo de la factura original se restaura automáticamente.
El complemento debe emitirse a más tardar el día 17 del mes siguiente a la fecha del pago. Por ejemplo, un pago del 15 de febrero debe tener su complemento antes del 17 de marzo.
Automatizar los complementos de pago elimina uno de los procesos más tediosos de la facturación PPD. Con la API de gigstack, cada pago se convierte automáticamente en un CFDI válido, manteniendo tu contabilidad sincronizada y al SAT contento.