Saltar al contenido

Reconciliación de pagos en tiempo real: arquitectura y lecciones

A
abemon
| | 11 min de lectura | Escrito por profesionales
Compartir

El problema que nadie quiere resolver manualmente

La reconciliación 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 métodos de pago (transferencia bancaria, tarjeta vía Stripe, y domiciliación), la reconciliación 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 céntimos por comisiones bancarias, y clientes que ponen referencia incorrecta (o ninguna).

Este artículo documenta la arquitectura que construimos para resolver este problema en el sector fintech, las decisiones de diseño que tomamos, y las lecciones que aprendimos en producción.

La arquitectura: event-driven matching

El sistema tiene cuatro componentes principales.

Ingestión de pagos. Cada fuente de pagos genera eventos normalizados. Stripe emite webhooks que capturamos y transformamos. La banca envía extractos vía 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 común:

{
  "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": { ... }
}

Ingestión de facturas. El ERP (Holded en nuestro caso) emite eventos cuando se crea o modifica una factura. Capturamos estos eventos vía la API de Holded con polling cada 5 minutos (Holded no tiene webhooks nativos, así que simulamos el evento con polling y detección 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 núcleo 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 número 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 podría cubrir dos facturas, diferencias por comisiones). Ocurre en el 10-15% de los pagos.

Los matches exactos se ejecutan automáticamente: la factura se marca como cobrada en Holded, se genera el asiento contable, y se archiva. Los matches probables van a una cola de revisión donde un operador confirma o corrige con un click. Los matches inciertos van a una cola de investigación con toda la información contextual prepoblada.

Audit trail. Cada decisión del sistema (match automático, match aprobado, match rechazado, match corregido) se registra en una tabla de auditoría inmutable con: payment_id, invoice_id, tipo de decisión, 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 número de factura y el importe es idéntico, match directo. Verificación 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 céntimos 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 revisión.

Regla 4: Pago agrupado. Un pago cuyo importe no coincide con ninguna factura individual pero sí con la suma de varias facturas del mismo cliente. Esto es común 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 investigación. El sistema proporciona las 5 facturas más probables (por similitud de importe, cliente y fecha) para facilitar la resolución manual.

En producción, la distribución 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 automático total es del 85%, lo que significa que de 500 pagos mensuales, solo 75 requieren intervención humana.

Gestión de excepciones

Las excepciones son el alma del sistema. Sin un buen manejo de excepciones, el 15% que no hace match automático se convierte en un cuello de botella que invalida toda la automatización.

Cola de revisión. 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 errónea. Tiempo medio de resolución: 15 segundos por pago (medido en producción). Comparado con los 3-5 minutos de la reconciliación manual completa, es una mejora de 12x.

Excepciones recurrentes. Si un cliente siempre paga sin referencia, el sistema aprende el patrón 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 revisión.

Diferencias contables. Cuando un pago y una factura no cuadran exactamente (típicamente por comisiones bancarias), el sistema genera automáticamente un asiento de ajuste por la diferencia. Para diferencias menores a 5 EUR, el ajuste es automático contra una cuenta de “diferencias de redondeo.” Para diferencias mayores, requiere aprobación.

Pagos huérfanos. Pagos recibidos sin factura correspondiente. Común cuando un cliente paga un anticipo o un depósito sin factura previa. Se registran como cobros pendientes de imputar y generan una alerta al equipo de administración.

Integración con contabilidad

La reconciliación no termina cuando el pago se vincula a la factura. Tiene que llegar a contabilidad.

Nuestro sistema genera asientos contables automáticos 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).
  • Comisión bancaria: Si Stripe cobra comisión, asiento separado contra la cuenta 626 (servicios bancarios).

La integración con Holded se hace vía 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 automáticamente con los extractos bancarios. El cierre contable mensual que antes tomaba 2 días ahora toma 2 horas (la mayor parte dedicada a verificar excepciones, no a reconciliar).

Lecciones de producción

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 único, y el sistema debe ignorar duplicados. Sin idempotencia, generas asientos contables duplicados, y eso es peor que no tener automatización.

El fuzzy matching necesita calibración 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 basándonos 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 más lógica 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 revisión 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 eliminación 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 método de pago. No intentes integrar Stripe, banca y domiciliaciones simultáneamente. Empieza con el canal que mayor volumen tenga (normalmente transferencias bancarias), valida que el matching funciona, y añade los demás progresivamente. En nuestro caso, empezamos con Stripe (porque tenía la mejor API y los datos más limpios), luego añadimos Qonto, y finalmente el resto de cuentas bancarias.

Lo que queda por hacer

El sistema actual resuelve el 85% de los pagos automáticamente. 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 número de factura con mayor precisión.
  • Matching predictivo. Usar el historial de pagos de un cliente para predecir qué factura está pagando antes de buscarla. Si un cliente siempre paga la factura más antigua primero, el sistema prioriza ese match.
  • Reconciliación multi-moneda. Actualmente solo reconciliamos en EUR. Para clientes con pagos en USD o GBP, necesitamos incorporar tipos de cambio históricos y tolerancias de conversión.

Este tipo de arquitectura event-driven es un ejemplo concreto de lo que describimos en nuestra guía práctica de pipelines en tiempo real. Si necesitas construir un sistema similar, nuestro equipo de ingeniería de datos tiene experiencia directa en este patrón. Para una visión completa del dominio — desde los protocolos SEPA hasta la trazabilidad contable — la guía completa de reconciliación de pagos en tiempo real cubre todos los aspectos del proceso. La reconciliación de pagos es uno de esos problemas que no parece importante hasta que calculas cuántas horas y cuántos errores genera. Y una vez que lo automatizas, te preguntas cómo sobreviviste sin él.

Sobre el autor

A

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.

Síguenos: LinkedIn GitHub