Reach us out
Reach out directly to our team*
- Email hi@kleva.co
- WhatsApp +1 704-816-9059
- Office Miami, Florida
Guía técnica completa de webhooks para plataformas de cobranza: eventos, payloads, seguridad, casos de uso y mejores prácticas de integración.
May 26, 2026 11 min read
|Los webhooks son el mecanismo técnico que permite a las plataformas de cobranza automatizada integrarse profundamente con el ecosistema tecnológico de una empresa. A diferencia de las integraciones tradicionales que requieren polling constante ("¿hay algo nuevo?", "¿hay algo nuevo?"), los webhooks funcionan con un modelo push: la plataforma de cobranza notifica a tus sistemas en tiempo real cuando ocurren eventos relevantes.
En Kleva, procesamos miles de eventos diarios —pagos recibidos, promesas de pago obtenidas, disputas detectadas, casos escalados— y nuestros clientes necesitan recibir estas notificaciones inmediatamente para actualizar sus ERPs, CRMs, dashboards ejecutivos y sistemas de BI sin latencia ni complejidad de integración.
Esta guía técnica profundiza en cómo funcionan los webhooks en contextos de cobranza, qué eventos son esenciales, cómo diseñar payloads efectivos, implementar seguridad robusta y manejar casos edge que pueden romper integraciones.
Un webhook es un HTTP callback: un POST request que una aplicación envía a una URL definida cuando ocurre un evento específico. En el contexto de cobranza automatizada:
Escenario tradicional sin webhooks:
Escenario con webhooks:
Tu sistema recibe notificación en
Una plataforma robusta de cobranza debe emitir webhooks para estos eventos:
payment.received: Se recibió un pago
{
"event": "payment.received",
"timestamp": "2026-05-26T14:23:45Z",
"data": {
"payment_id": "pmt_abc123",
"invoice_id": "inv_12345",
"debtor_id": "deb_98765",
"amount": 1500.00,
"currency": "MXN",
"payment_method": "credit_card",
"transaction_id": "txn_stripe_xyz789",
"status": "confirmed",
"paid_at": "2026-05-26T14:23:30Z"
}
}
payment.failed: Intento de pago falló (tarjeta declinada, fondos insuficientes)
payment.refunded: Un pago fue revertido
ptp.created: Se obtuvo una promesa de pago
{
"event": "ptp.created",
"timestamp": "2026-05-26T10:15:22Z",
"data": {
"ptp_id": "ptp_def456",
"invoice_id": "inv_12345",
"debtor_id": "deb_98765",
"promised_amount": 2000.00,
"promised_date": "2026-06-01",
"obtained_by": "voice_agent_001",
"confidence_score": 0.85,
"recording_url": "https://recordings.kleva.co/call_789.mp3",
"notes": "Cliente confirma pago el 1 de junio, día de quincena"
}
}
ptp.fulfilled: Promesa de pago fue cumplida
ptp.broken: Promesa de pago no fue cumplida en la fecha acordada
contact.completed: Se completó un intento de contacto (exitoso o no)
{
"event": "contact.completed",
"timestamp": "2026-05-26T11:30:10Z",
"data": {
"contact_id": "cnt_ghi789",
"invoice_id": "inv_12345",
"debtor_id": "deb_98765",
"channel": "voice",
"status": "contacted",
"duration_seconds": 185,
"outcome": "promise_to_pay_obtained",
"agent_type": "ai_voice_agent",
"transcript": "[Transcripción completa...]",
"sentiment_score": 0.65,
"next_action": "wait_for_payment"
}
}
contact.failed: No se pudo establecer contacto (no contesta, número inválido)
case.escalated: Caso fue escalado a gestor humano
{
"event": "case.escalated",
"timestamp": "2026-05-26T13:45:00Z",
"data": {
"case_id": "case_jkl012",
"invoice_id": "inv_12345",
"debtor_id": "deb_98765",
"escalation_reason": "dispute_detected",
"priority": "high",
"assigned_to": "agent_maria_lopez",
"previous_attempts": 3,
"context_summary": "Cliente reporta producto defectuoso, requiere investigación",
"estimated_resolution_time": "2026-05-27T10:00:00Z"
}
}
case.resolved: Caso escalado fue resuelto
dispute.created: Se detectó una disputa
{
"event": "dispute.created",
"timestamp": "2026-05-26T15:00:00Z",
"data": {
"dispute_id": "dsp_mno345",
"invoice_id": "inv_12345",
"debtor_id": "deb_98765",
"dispute_type": "product_quality",
"description": "Cliente reporta que producto llegó dañado",
"reported_by": "voice_agent_001",
"requires_action_from": ["sales_team", "quality_team"],
"status": "pending_investigation"
}
}
dispute.resolved: Disputa fue resuelta (a favor de empresa o cliente)
account.sent_to_collections: Cuenta fue enviada a cobranza
account.closed: Cuenta fue cerrada (pagada completamente o castigada)
account.written_off: Cuenta fue castigada como incobrable
Un payload de webhook bien diseñado sigue estos principios:
Todos los webhooks deben tener estructura base común:
{
"event": "nombre.del.evento",
"timestamp": "ISO 8601 timestamp",
"webhook_id": "id único de este webhook",
"data": { /* datos específicos del evento */ },
"metadata": {
"environment": "production",
"version": "v2"
}
}
Incluir webhook_id único permite al receptor deduplicar webhooks en caso de reenvíos:
# Código del receptor (Python)
if Webhook.objects.filter(webhook_id=payload['webhook_id']).exists():
return HttpResponse(status=200) # Ya procesado, ignorar
# Procesar webhook...
Webhook.objects.create(webhook_id=payload['webhook_id'], processed=True)
Incluir todos los IDs relevantes facilita joins en base de datos del receptor:
"data": {
"payment_id": "pmt_abc123",
"invoice_id": "inv_12345", // Referencia a factura
"debtor_id": "deb_98765", // Referencia a deudor
"campaign_id": "cmp_555", // Referencia a campaña
"agent_id": "agent_001" // Referencia a agente que gestionó
}
Siempre UTC, siempre con timezone:
"timestamp": "2026-05-26T14:23:45Z" // Correcto
"timestamp": "2026-05-26 14:23:45" // Incorrecto (ambiguo)
Los webhooks son endpoints públicos expuestos a internet. La seguridad es crítica:
Kleva firma cada webhook con HMAC-SHA256:
Lado emisor (Kleva):
import hmac
import hashlib
payload = json.dumps(webhook_data)
secret = "tu_clave_secreta_compartida"
signature = hmac.new(
secret.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
headers = {
'X-Kleva-Signature': signature,
'Content-Type': 'application/json'
}
Lado receptor (tu servidor):
import hmac
import hashlib
def verify_webhook(request):
received_signature = request.headers.get('X-Kleva-Signature')
payload = request.body
secret = os.environ['KLEVA_WEBHOOK_SECRET']
expected_signature = hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(received_signature, expected_signature):
raise SecurityError("Webhook signature invalid")
return json.loads(payload)
Configurar firewall para aceptar webhooks solo desde IPs de la plataforma:
# Nginx config
location /webhooks/kleva {
allow 52.14.98.123; # IP de Kleva 1
allow 52.14.98.124; # IP de Kleva 2
deny all;
proxy_pass http://localhost:8000;
}
La plataforma debe rechazar URLs de webhook que no sean HTTPS:
http://example.com/webhook ❌ Rechazado
https://example.com/webhook ✅ Aceptado
Proteger endpoint de webhook contra ataques de denegación de servicio:
# Django + django-ratelimit
from django_ratelimit.decorators import ratelimit
@ratelimit(key='ip', rate='100/m', method='POST')
def kleva_webhook_handler(request):
# Procesar webhook...
Los endpoints de webhook pueden fallar por múltiples razones: servidor caído, deploy en curso, error en código receptor. Una plataforma robusta debe implementar estrategia de reintentos:
Kleva reintenta webhooks fallidos con este schedule:
Después de 7 intentos fallidos, el webhook se marca como "failed" y se notifica al administrador.
Código HTTPSignificadoAcción de Kleva
200-299ÉxitoMarcar como entregado, no reintentar
400, 404, 422Error del cliente (URL mala, payload inválido)NO reintentar, notificar administrador
401, 403Error de autenticaciónNO reintentar, verificar configuración
500-599Error del servidorReintentar con backoff exponencial
Timeout (>30s)Servidor no respondeReintentar con backoff exponencial
Tu endpoint debe responder apropiadamente:
@app.route('/webhooks/kleva', methods=['POST'])
def handle_kleva_webhook():
try:
# 1. Verificar firma
verify_webhook_signature(request)
# 2. Parsear payload
payload = request.get_json()
# 3. Procesar de forma asíncrona (no bloquear respuesta)
process_webhook_async.delay(payload)
# 4. Responder inmediatamente con 200
return jsonify({"status": "received"}), 200
except SignatureError:
return jsonify({"error": "Invalid signature"}), 401
except Exception as e:
logger.error(f"Webhook processing error: {e}")
return jsonify({"error": "Internal error"}), 500
Importante: Responder con 200 inmediatamente, procesar asíncronamente. No hacer procesamiento pesado antes de responder.
Objetivo: Actualizar status de factura en ERP cuando ocurre pago o promesa de pago.
Flujo:
Código de ejemplo:
def process_payment_webhook(payload):
invoice_id = payload['data']['invoice_id']
amount = payload['data']['amount']
paid_at = payload['data']['paid_at']
# Mapear ID de Kleva a ID de SAP
sap_invoice = InvoiceMapping.objects.get(kleva_id=invoice_id)
# Actualizar en SAP
sap_client.update_invoice(
invoice_number=sap_invoice.sap_number,
status='PAID',
payment_amount=amount,
payment_date=paid_at
)
# Registrar en journal
sap_client.create_journal_entry(
account='1105', # Cuentas por cobrar
debit=amount,
description=f'Pago recibido via Kleva - {invoice_id}'
)
Objetivo: Notificar equipo inmediatamente cuando ocurren eventos críticos.
Eventos a notificar:
Código de ejemplo:
def notify_team_via_slack(payload):
event = payload['event']
if event == 'payment.received' and payload['data']['amount'] > 10000:
message = f"💰 Pago grande recibido: ${payload['data']['amount']} USD de {payload['data']['debtor_id']}"
channel = "#collections-wins"
elif event == 'case.escalated':
message = f"⚠️ Caso escalado: {payload['data']['case_id']} - Razón: {payload['data']['escalation_reason']}"
channel = "#collections-escalations"
elif event == 'dispute.created':
message = f"🚨 Disputa creada: {payload['data']['dispute_type']} - {payload['data']['description']}"
channel = "#collections-disputes"
slack_client.post_message(channel=channel, text=message)
Objetivo: Actualizar dashboard de métricas sin polling.
Flujo:
Arquitectura:
Kleva webhook → Flask endpoint → PostgreSQL →
PostgreSQL NOTIFY → WebSocket server → Dashboard browser
Objetivo: Integrar con cientos de apps sin código custom.
Ejemplos:
Una plataforma profesional debe ofrecer herramientas de debugging:
Durante desarrollo, usar herramientas como ngrok para exponer localhost:
# Terminal 1: Iniciar servidor local
python manage.py runserver
# Terminal 2: Exponer con ngrok
ngrok http 8000
# Configurar URL de webhook en Kleva:
https://abc123.ngrok.io/webhooks/kleva
Los webhooks evolucionan. Una plataforma madura debe soportar múltiples versiones simultáneamente:
# Versión en header HTTP
X-Kleva-Webhook-Version: v2
# O en el payload
{
"event": "payment.received",
"version": "v2",
"data": { ... }
}
Política de deprecación:
NUNCA hacer procesamiento pesado en el endpoint de webhook. Usar queue:
# Mal ❌
@app.route('/webhook', methods=['POST'])
def handle_webhook():
payload = request.get_json()
update_erp(payload) # Puede tardar 5-10 segundos
send_email(payload) # Puede tardar 2-3 segundos
return {"status": "ok"}, 200 # Kleva esperó 8 segundos
# Bien ✅
@app.route('/webhook', methods=['POST'])
def handle_webhook():
payload = request.get_json()
redis_queue.enqueue(process_webhook, payload)
return {"status": "received"}, 200 # Respuesta en
Siempre verificar si webhook ya fue procesado:
def process_webhook(payload):
webhook_id = payload['webhook_id']
# Usar transaction atómica
with transaction.atomic():
# Intentar crear registro
webhook_log, created = WebhookLog.objects.get_or_create(
webhook_id=webhook_id,
defaults={'payload': payload, 'processed': False}
)
if not created:
# Ya existe, ya fue procesado
return
# Procesar...
update_invoice(payload['data']['invoice_id'])
# Marcar como procesado
webhook_log.processed = True
webhook_log.save()
Registrar todo para debugging posterior:
import logging
logger = logging.getLogger('webhooks')
def handle_webhook(request):
# Log request completo
logger.info(f"Webhook received: {request.headers}")
logger.info(f"Payload: {request.body}")
try:
result = process_webhook(request.get_json())
logger.info(f"Webhook processed successfully: {result}")
return {"status": "ok"}, 200
except Exception as e:
logger.error(f"Webhook processing failed: {e}", exc_info=True)
return {"status": "error"}, 500
Implementar health check que la plataforma puede verificar:
@app.route('/webhooks/health', methods=['GET'])
def webhook_health():
# Verificar conectividad a DB
db.session.execute('SELECT 1')
# Verificar queue funcionando
queue_status = redis_queue.get_status()
return {
"status": "healthy",
"timestamp": datetime.utcnow().isoformat(),
"queue_size": queue_status['size']
}, 200
Los webhooks son la columna vertebral de integraciones modernas en plataformas de cobranza automatizada. Implementarlos correctamente —con seguridad robusta, manejo de reintentos, idempotencia y monitoreo— permite que sistemas de cobranza como Kleva se integren profundamente en el ecosistema tecnológico empresarial sin complejidad ni latencia.
Las empresas que aprovechan webhooks para sincronizar ERPs, actualizar dashboards en tiempo real, automatizar workflows y alertar equipos logran operaciones de cobranza verdaderamente unificadas donde todos los sistemas están sincronizados en tiempo real, eliminando silos de información y maximizando eficiencia operacional.
La diferencia entre una integración básica vía API polling y una integración avanzada vía webhooks es la diferencia entre actualización cada 15 minutos versus actualización en menos de 1 segundo —y en cobranza, esa diferencia puede significar capturar un pago antes de que el deudor cambie de opinión.
No bots, no endless forms. Fill in your details and someone from our team will reach out.
Reach out directly to our team*
No bots, no endless forms.