Reconciliacion de pagos en tiempo real: arquitectura y lecciones
El problema que nadie quiere resolver manualmente
La reconciliacion de pagos es el proceso de verificar que cada cobro recibido corresponde a una factura emitida, y que los importes cuadran. Suena simple. No lo es.
En una empresa que gestiona 500 facturas mensuales con 3 metodos de pago (transferencia bancaria, tarjeta via Stripe, y domiciliacion), la reconciliacion manual consume entre 40 y 60 horas al mes. Lo sabemos porque lo hemos medido en nuestras propias operaciones antes de automatizarlo. Un contable que abre la banca online, busca la factura correspondiente en el ERP, compara importes, anota la referencia, marca como cobrada, y pasa a la siguiente. Multiplicado por 500. Con los habituales falsos amigos: pagos parciales, pagos agrupados de varias facturas, diferencias de centimos por comisiones bancarias, y clientes que ponen referencia incorrecta (o ninguna).
Este articulo documenta la arquitectura que construimos para resolver este problema en el sector fintech, las decisiones de diseno que tomamos, y las lecciones que aprendimos en produccion.
La arquitectura: event-driven matching
El sistema tiene cuatro componentes principales.
Ingestion de pagos. Cada fuente de pagos genera eventos normalizados. Stripe emite webhooks que capturamos y transformamos. La banca envia extractos via API (usamos la API de Qonto para la cuenta principal y scraping de Open Banking para cuentas en otros bancos). Las domiciliaciones generan ficheros SEPA que se parsean. Todos confluyen en un topic de Kafka (payments.received) con un schema comun:
{
"payment_id": "pay_abc123",
"source": "stripe",
"amount": 1250.00,
"currency": "EUR",
"reference": "FAC-2025-0342",
"payer_name": "Empresa XYZ SL",
"payer_account": "ES91 2100 0418 4502 0005 1332",
"timestamp": "2025-09-18T10:23:45Z",
"raw_data": { ... }
}
Ingestion de facturas. El ERP (Holded en nuestro caso) emite eventos cuando se crea o modifica una factura. Capturamos estos eventos via la API de Holded con polling cada 5 minutos (Holded no tiene webhooks nativos, asi que simulamos el evento con polling y deteccion de cambios). Los eventos van al topic invoices.issued con un schema que incluye ID, importe, cliente, fecha de vencimiento y referencia.
Motor de matching. Es el nucleo del sistema. Un consumidor de Kafka que lee ambos topics y ejecuta un algoritmo de matching con tres niveles de confianza:
- Match exacto (confianza 100%): La referencia del pago coincide exactamente con el numero de factura, y el importe coincide. Caso ideal. Ocurre en el 55-60% de los pagos.
- Match probable (confianza 70-95%): La referencia es similar pero no exacta (fuzzy matching con Levenshtein distance <3), o el importe coincide con una factura pendiente del mismo cliente. Ocurre en el 25-30% de los pagos.
- Match incierto (confianza <70%): No hay coincidencia clara de referencia ni de importe. El sistema busca combinaciones (un pago que podria cubrir dos facturas, diferencias por comisiones). Ocurre en el 10-15% de los pagos.
Los matches exactos se ejecutan automaticamente: la factura se marca como cobrada en Holded, se genera el asiento contable, y se archiva. Los matches probables van a una cola de revision donde un operador confirma o corrige con un click. Los matches inciertos van a una cola de investigacion con toda la informacion contextual prepoblada.
Audit trail. Cada decision del sistema (match automatico, match aprobado, match rechazado, match corregido) se registra en una tabla de auditoria inmutable con: payment_id, invoice_id, tipo de decision, confianza, timestamp, y usuario (si aplica). Esta tabla es la fuente de verdad para cualquier discrepancia contable posterior.
El algoritmo de matching en detalle
El matching no es un simple IF referencia == factura_id. Es un pipeline de reglas ejecutadas en orden de especificidad.
Regla 1: Referencia exacta + importe exacto. El caso trivial. Si la referencia del pago coincide con un numero de factura y el importe es identico, match directo. Verificacion adicional: que la factura este pendiente de cobro (evitar marcar como cobrada una factura ya cobrada).
Regla 2: Referencia exacta + importe con tolerancia. Si la referencia coincide pero el importe difiere en menos de 2 EUR, match con confianza 90%. Las diferencias de centimos son habituales: comisiones bancarias, redondeos de tipo de cambio para pagos internacionales, o retenciones fiscales que el pagador aplica y no comunica.
Regla 3: Sin referencia + match por importe y cliente. Cuando el pago no tiene referencia (o tiene “transferencia” como referencia, que es lo mismo que nada), buscamos facturas pendientes del mismo cliente (identificado por nombre del pagador o cuenta bancaria) con el mismo importe. Si hay exactamente una coincidencia, match con confianza 85%. Si hay varias, confianza 60% y a la cola de revision.
Regla 4: Pago agrupado. Un pago cuyo importe no coincide con ninguna factura individual pero si con la suma de varias facturas del mismo cliente. Esto es comun en B2B: el cliente paga 3 facturas con una sola transferencia. El algoritmo busca subconjuntos de facturas pendientes cuya suma coincida con el pago (con tolerancia de 2 EUR). Si encuentra exactamente un subconjunto, match con confianza 80%.
Regla 5: Pago parcial. Un pago que coincide con una factura pero por un importe menor. Registra un cobro parcial y mantiene la factura en estado “parcialmente cobrada” con el saldo pendiente. Confianza 75%.
Regla 6: Fallback. Si ninguna regla produce un match, el pago va a la cola de investigacion. El sistema proporciona las 5 facturas mas probables (por similitud de importe, cliente y fecha) para facilitar la resolucion manual.
En produccion, la distribucion es: regla 1 cubre el 55% de pagos, regla 2 el 5%, regla 3 el 15%, regla 4 el 8%, regla 5 el 2%, y el 15% restante va a fallback. La tasa de match automatico total es del 85%, lo que significa que de 500 pagos mensuales, solo 75 requieren intervencion humana.
Gestion de excepciones
Las excepciones son el alma del sistema. Sin un buen manejo de excepciones, el 15% que no hace match automatico se convierte en un cuello de botella que invalida toda la automatizacion.
Cola de revision. Interfaz web con dos columnas: pago a la izquierda, facturas candidatas a la derecha. Un click para confirmar el match sugerido, un buscador para encontrar la factura correcta si la sugerencia es erronea. Tiempo medio de resolucion: 15 segundos por pago (medido en produccion). Comparado con los 3-5 minutos de la reconciliacion manual completa, es una mejora de 12x.
Excepciones recurrentes. Si un cliente siempre paga sin referencia, el sistema aprende el patron y eleva la confianza del match por importe+cliente para ese pagador. Implementado como una tabla de “overrides” por cliente que ajusta los umbrales del motor de matching. Esto reduce progresivamente el volumen de la cola de revision.
Diferencias contables. Cuando un pago y una factura no cuadran exactamente (tipicamente por comisiones bancarias), el sistema genera automaticamente un asiento de ajuste por la diferencia. Para diferencias menores a 5 EUR, el ajuste es automatico contra una cuenta de “diferencias de redondeo.” Para diferencias mayores, requiere aprobacion.
Pagos huerfanos. Pagos recibidos sin factura correspondiente. Comun cuando un cliente paga un anticipo o un deposito sin factura previa. Se registran como cobros pendientes de imputar y generan una alerta al equipo de administracion.
Integracion con contabilidad
La reconciliacion no termina cuando el pago se vincula a la factura. Tiene que llegar a contabilidad.
Nuestro sistema genera asientos contables automaticos en Holded para cada match confirmado:
- Pago completo: Cargo en la cuenta del banco, abono en la cuenta de clientes. Referencia cruzada con la factura.
- Pago parcial: Mismo asiento por el importe recibido. La factura queda parcialmente cobrada.
- Diferencia de redondeo: Asiento adicional contra la cuenta 668 (diferencias negativas de cambio) o 768 (diferencias positivas).
- Comision bancaria: Si Stripe cobra comision, asiento separado contra la cuenta 626 (servicios bancarios).
La integracion con Holded se hace via su API REST. Cada asiento incluye la referencia del pago y la factura para trazabilidad. Al final del mes, el balance de la cuenta de clientes en Holded cuadra automaticamente con los extractos bancarios. El cierre contable mensual que antes tomaba 2 dias ahora toma 2 horas (la mayor parte dedicada a verificar excepciones, no a reconciliar).
Lecciones de produccion
Idempotencia es obligatoria. Los webhooks de Stripe se duplican. Los extractos bancarios a veces se solapan. El polling de Holded puede leer la misma factura dos veces. Cada evento debe tener un ID unico, y el sistema debe ignorar duplicados. Sin idempotencia, generas asientos contables duplicados, y eso es peor que no tener automatizacion.
El fuzzy matching necesita calibracion continua. Los umbrales que funcionan cuando tienes 100 clientes no funcionan cuando tienes 500. La probabilidad de falsos positivos (dos facturas del mismo importe de distintos clientes) crece con el volumen. Recalibramos los umbrales trimestralmente basandonos en la tasa de correcciones humanas.
Los datos bancarios son un desastre. El campo “concepto” de una transferencia bancaria es texto libre. Hemos visto: “factura 342”, “FAC342”, “fra. n 342”, “pedido 342”, “pago factura tres cuatro dos”, y, en un caso memorable, “transferencia para mj.” El parser de referencias necesita mas logica de fuzzy matching que el motor de matching principal.
El ROI es inmediato y medible. 40-60 horas mensuales de trabajo manual reducidas a 5-8 horas de revision de excepciones. A 25 EUR/hora de coste laboral, son 800-1.300 EUR mensuales de ahorro directo. El sistema se amortizan en 4-6 meses. Pero el beneficio real no es el ahorro en horas; es la eliminacion de errores. Un cobro mal imputado manualmente puede tardar meses en detectarse. Con el sistema automatizado, cada discrepancia se detecta en tiempo real.
Empieza con un metodo de pago. No intentes integrar Stripe, banca y domiciliaciones simultaneamente. Empieza con el canal que mayor volumen tenga (normalmente transferencias bancarias), valida que el matching funciona, y anade los demas progresivamente. En nuestro caso, empezamos con Stripe (porque tenia la mejor API y los datos mas limpios), luego anadimos Qonto, y finalmente el resto de cuentas bancarias.
Lo que queda por hacer
El sistema actual resuelve el 85% de los pagos automaticamente. Nuestro objetivo es llegar al 95%. Las mejoras que estamos explorando:
- NLP para referencias. Usar un modelo de lenguaje para interpretar las referencias de pago en texto libre y extraer el numero de factura con mayor precision.
- Matching predictivo. Usar el historial de pagos de un cliente para predecir que factura esta pagando antes de buscarla. Si un cliente siempre paga la factura mas antigua primero, el sistema prioriza ese match.
- Reconciliacion multi-moneda. Actualmente solo reconciliamos en EUR. Para clientes con pagos en USD o GBP, necesitamos incorporar tipos de cambio historicos y tolerancias de conversion.
Este tipo de arquitectura event-driven es un ejemplo concreto de lo que describimos en nuestra guia practica de pipelines en tiempo real. Si necesitas construir un sistema similar, nuestro equipo de ingenieria de datos tiene experiencia directa en este patron. La reconciliacion de pagos es uno de esos problemas que no parece importante hasta que calculas cuantas horas y cuantos errores genera. Y una vez que lo automatizas, te preguntas como sobreviviste sin el.
Etiquetas
Sobre el autor
abemon engineering
Equipo de ingenieria
Equipo multidisciplinar de ingenieria, datos e IA con sede en Canarias. Construimos, desplegamos y operamos soluciones de software a medida para empresas de cualquier escala.
