Webhooks
Los webhooks te permiten recibir notificaciones automáticas cuando un job se completa, evitando la necesidad de hacer polling.
Configuración
1. Configura tu URL de Webhook
PUT /api/v1/settings/webhook
{
"webhook_url": "https://tu-servidor.com/masleads-webhook",
"enabled": true
}
Response:
{
"webhook_url": "https://tu-servidor.com/masleads-webhook",
"webhook_secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxx",
"enabled": true,
"updated_at": "2025-01-08T10:00:00Z"
}
Importante: Guarda el
webhook_secretde forma segura. Lo necesitarás para verificar las firmas.
Requisitos de la URL
| Requisito | Detalle |
|---|---|
| Protocolo | HTTPS obligatorio (no HTTP) |
| Puerto | 443 (estándar HTTPS) |
| Timeout | Debe responder en < 10 segundos |
| Response | Código 2xx para confirmar recepción |
Formato del Webhook
Cuando un job se completa, MasLeads enviará un POST a tu URL:
Headers
Content-Type: application/json
X-Webhook-Signature: sha256=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
X-Webhook-Timestamp: 1704710400
X-Webhook-Event: job.completed
Body
{
"event": "job.completed",
"timestamp": "2025-01-08T10:30:00Z",
"data": {
"job_id": "job_abc123xyz",
"status": "completed",
"total_leads": 50,
"leads_found": 42,
"leads_not_found": 8,
"credits_used": 84,
"credits_refunded": 16,
"created_at": "2025-01-08T10:25:00Z",
"completed_at": "2025-01-08T10:30:00Z"
}
}
Tipos de Eventos
| Evento | Descripción |
|---|---|
job.completed |
Job terminó exitosamente |
job.failed |
Job falló |
job.partial |
Job completó parcialmente (algunos leads con error) |
Verificar Firma (HMAC-SHA256)
Siempre verifica la firma para asegurarte de que el webhook proviene de MasLeads.
Algoritmo
firma = HMAC-SHA256(webhook_secret, timestamp + "." + body)
Python
import hmac
import hashlib
def verify_webhook_signature(payload: bytes, signature: str,
timestamp: str, secret: str) -> bool:
"""
Verifica la firma HMAC-SHA256 del webhook.
Args:
payload: Body del request (bytes)
signature: Header X-Webhook-Signature
timestamp: Header X-Webhook-Timestamp
secret: Tu webhook_secret
Returns:
True si la firma es válida
"""
# Construir el mensaje firmado
signed_payload = f"{timestamp}.{payload.decode('utf-8')}"
# Calcular HMAC esperado
expected_signature = hmac.new(
secret.encode('utf-8'),
signed_payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
# Extraer firma del header (quitar prefijo "sha256=")
received_signature = signature.replace("sha256=", "")
# Comparación segura contra timing attacks
return hmac.compare_digest(expected_signature, received_signature)
JavaScript (Node.js)
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, timestamp, secret) {
const signedPayload = `${timestamp}.${payload}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
const receivedSignature = signature.replace('sha256=', '');
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(receivedSignature)
);
}
Ejemplo Completo: Flask
from flask import Flask, request, jsonify
import hmac
import hashlib
app = Flask(__name__)
WEBHOOK_SECRET = "whsec_xxxxxxxxxxxxxxxxxxxxxxxx"
@app.route('/masleads-webhook', methods=['POST'])
def handle_webhook():
# 1. Obtener headers
signature = request.headers.get('X-Webhook-Signature')
timestamp = request.headers.get('X-Webhook-Timestamp')
if not signature or not timestamp:
return jsonify({"error": "Missing signature headers"}), 400
# 2. Verificar firma
if not verify_webhook_signature(
request.data, signature, timestamp, WEBHOOK_SECRET
):
return jsonify({"error": "Invalid signature"}), 401
# 3. Verificar timestamp (prevenir replay attacks)
import time
if abs(time.time() - int(timestamp)) > 300: # 5 minutos
return jsonify({"error": "Timestamp too old"}), 401
# 4. Procesar evento
event = request.json
if event['event'] == 'job.completed':
job_id = event['data']['job_id']
# Descargar resultados
process_completed_job(job_id)
return jsonify({"received": True}), 200
Ejemplo Completo: Express.js
const express = require('express');
const crypto = require('crypto');
const app = express();
const WEBHOOK_SECRET = 'whsec_xxxxxxxxxxxxxxxxxxxxxxxx';
app.post('/masleads-webhook',
express.raw({type: 'application/json'}),
(req, res) => {
const signature = req.headers['x-webhook-signature'];
const timestamp = req.headers['x-webhook-timestamp'];
if (!signature || !timestamp) {
return res.status(400).json({error: 'Missing headers'});
}
// Verificar firma
const payload = req.body.toString();
if (!verifyWebhookSignature(payload, signature, timestamp, WEBHOOK_SECRET)) {
return res.status(401).json({error: 'Invalid signature'});
}
// Verificar timestamp
if (Math.abs(Date.now()/1000 - parseInt(timestamp)) > 300) {
return res.status(401).json({error: 'Timestamp too old'});
}
// Procesar
const event = JSON.parse(payload);
console.log('Job completed:', event.data.job_id);
res.json({received: true});
}
);
Política de Reintentos
Si tu servidor no responde con 2xx, MasLeads reintentará:
| Intento | Espera | Tiempo total |
|---|---|---|
| 1 | Inmediato | 0s |
| 2 | 30 segundos | 30s |
| 3 | 2 minutos | 2m 30s |
| 4 | 10 minutos | 12m 30s |
| 5 | 1 hora | 1h 12m 30s |
Después del 5º intento fallido, el webhook se marca como failed y no se reintenta más.
Ver webhooks fallidos
Puedes ver el historial de webhooks en Perfil → API → Webhooks: - Timestamp, evento, status code, intentos
Desactivar Webhooks
PUT /api/v1/settings/webhook
{
"enabled": false
}
Rotación del Secret
Para rotar el webhook_secret:
POST /api/v1/settings/webhook/rotate-secret
Response:
{
"webhook_secret": "whsec_nuevo_secret_aqui",
"previous_secret_valid_until": "2025-01-08T11:00:00Z"
}
Durante 1 hora, ambos secrets serán válidos para permitir actualizar tu servidor sin perder webhooks.
Mejores Prácticas
✅ Hacer
- Responder rápido (< 3s) y procesar en background
- Verificar siempre la firma
- Verificar el timestamp (prevenir replay attacks)
- Implementar idempotencia (el mismo webhook puede llegar más de una vez)
- Usar HTTPS con certificado válido
❌ No hacer
- Procesar lógica pesada síncrona en el handler
- Confiar en webhooks sin verificar firma
- Usar HTTP (no HTTPS)
- Asumir que los webhooks llegan en orden
Idempotencia
Usa job_id como clave para evitar procesar duplicados:
def handle_webhook(event):
job_id = event['data']['job_id']
# Verificar si ya procesamos este job
if redis.sismember('processed_webhooks', job_id):
return # Ya procesado, ignorar
# Procesar
process_job(job_id)
# Marcar como procesado
redis.sadd('processed_webhooks', job_id)
redis.expire('processed_webhooks', 86400) # TTL 24h
Anterior: ← Cómo Funciona | Siguiente: Rate Limiting →