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_secret de 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

  1. Responder rápido (< 3s) y procesar en background
  2. Verificar siempre la firma
  3. Verificar el timestamp (prevenir replay attacks)
  4. Implementar idempotencia (el mismo webhook puede llegar más de una vez)
  5. Usar HTTPS con certificado válido

❌ No hacer

  1. Procesar lógica pesada síncrona en el handler
  2. Confiar en webhooks sin verificar firma
  3. Usar HTTP (no HTTPS)
  4. 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 →