LealUp Docs
API y Webhooks

Webhooks

Suscribirse a eventos de LealUp — entrega, reintentos, firmas HMAC y protección anti-replay.

Los webhooks permiten que tu sistema reciba notificaciones en tiempo real cuando pasan cosas en LealUp — sin tener que hacer polling.

Casos de uso típicos

  • Sincronizar salud a tu CRM — cuando un cliente cae a rojo, marca el deal en Salesforce/HubSpot.
  • Alertas custom — mandar un mensaje a tu canal de incidentes cuando hay churn signal.
  • Integrar con billing — cuando un cliente cambia de tier, actualizar tu sistema de facturación.
  • Data warehouse — replicar eventos de LealUp a Snowflake/BigQuery.

Eventos disponibles

Una subset de los eventos que puedes suscribir:

EventoDescripción
customer.creatednuevo cliente creado
customer.updatedcambio en ARR, owner, status, campos
customer.deletedcliente eliminado (soft-delete)
customer.status_changedcambio a churned, paused, etc.
health.changedscore cruzó umbral (saludable→neutro, neutro→riesgo, etc.)
health.drop_sharpcaída >15 pts en 7 días
renewal.approachingrenewal en 60/30/15/7 días
renewal.completedcliente renovó
renewal.churnedcliente no renovó
task.createdtarea creada (por playbook o manual)
task.completedtarea completada
task.overduetarea vencida
playbook.triggeredplaybook se disparó
ai.detected_issueLa IA detectó un issue potencial en WhatsApp/email
integration.disconnecteduna integración dejó de funcionar

Lista completa: GET /v1/webhook-events.

Crear una suscripción

POST /v1/webhook-subscriptions
Authorization: Bearer ...
Content-Type: application/json
{
  "url": "https://tu-sistema.com/lealup-webhook",
  "events": [
    "health.drop_sharp",
    "renewal.approaching"
  ],
  "secret": "wh_secret_abc123...",
  "description": "Alertas a Slack via Zapier",
  "filters": {
    "segment_id": "01HSEG..."
  }
}

Respuesta:

{
  "id": "01HWEB...",
  "url": "...",
  "events": [...],
  "created_at": "2026-04-15T...",
  "status": "active"
}

Campos

  • url — HTTPS obligatorio. Debe responder 2xx en <5 seg.
  • events — lista de tipos (o ["*"] para todos).
  • secret — para firmar requests. Mínimo 32 chars. Lo genera tu sistema; LealUp lo almacena para firmar.
  • filters — opcional, para suscripciones más granulares (solo clientes de X segmento, con ARR > Y, etc.).

Formato del payload

POST https://tu-sistema.com/lealup-webhook
Content-Type: application/json
X-LealUp-Signature: sha256=abc123def456...
X-LealUp-Timestamp: 1713193200
X-LealUp-Event: health.drop_sharp
X-LealUp-Delivery-Id: 01HDEL...

{
  "event": "health.drop_sharp",
  "delivery_id": "01HDEL...",
  "created_at": "2026-04-15T14:30:00Z",
  "tenant_id": "01HTEN...",
  "data": {
    "customer_id": "01HCUS...",
    "customer_name": "Acme Corp",
    "previous_score": 72,
    "current_score": 54,
    "delta": -18,
    "days": 5,
    "owner_id": "01HUSR...",
    "owner_email": "[email protected]"
  }
}

Verificar la firma (obligatorio)

Siempre valida el header X-LealUp-Signature para confirmar que el request viene de LealUp y no fue tampered.

Algoritmo

signature = HMAC-SHA256(
  secret,
  timestamp + "." + raw_body
)

Ejemplo (Node.js)

import crypto from "crypto";

function verifyWebhook(
  rawBody: string,
  signature: string,
  timestamp: string,
  secret: string,
): boolean {
  // 1. Verifica que el timestamp sea reciente (anti-replay).
  const age = Date.now() / 1000 - parseInt(timestamp);
  if (age > 300) return false; // >5 min → rechaza

  // 2. Calcula la firma esperada.
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  // 3. Compara con time-safe equals.
  const received = signature.replace("sha256=", "");
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(received),
  );
}

Ejemplo (Python)

import hmac
import hashlib
import time

def verify_webhook(raw_body: bytes, signature: str, timestamp: str, secret: str) -> bool:
    if abs(time.time() - int(timestamp)) > 300:
        return False
    expected = hmac.new(
        secret.encode(),
        f"{timestamp}.".encode() + raw_body,
        hashlib.sha256,
    ).hexdigest()
    received = signature.replace("sha256=", "")
    return hmac.compare_digest(expected, received)

Entrega y reintentos

  • Timeout: tu endpoint debe responder en 5 segundos.
  • Success: cualquier status 2xx cuenta como delivered.
  • Retry: si falla (timeout, 5xx, conexión rota), reintentamos con backoff exponencial:
    • Intento 1: inmediato.
    • Intento 2: +1 min.
    • Intento 3: +5 min.
    • Intento 4: +30 min.
    • Intento 5: +2 h.
    • Intento 6: +12 h.
    • Intento 7 (último): +24 h.
  • Dead letter: después de 7 intentos fallidos, el delivery se marca como failed y se loguea. Tu suscripción no se desactiva; seguirá recibiendo nuevos eventos.

Idempotencia y orden

Idempotencia

Cada delivery tiene X-LealUp-Delivery-Id único. Guarda los que ya procesaste — en caso de reintento, LealUp reenvía con el mismo delivery_id.

if (await redis.exists(`processed:${deliveryId}`)) {
  return { status: 200 };  // ya lo procesamos, OK
}

await processWebhook(payload);
await redis.set(`processed:${deliveryId}`, "1", { ex: 86400 });

Orden

LealUp no garantiza orden estricto (entrega at-least-once). Si recibes customer.updated y luego customer.created fuera de orden, usa el created_at del payload para resolver.

Filtros

Puedes limitar cuándo una suscripción dispara:

{
  "url": "...",
  "events": ["health.changed"],
  "filters": {
    "segment_id": "01HSEG...",
    "min_arr": 50000,
    "from_status": ["healthy", "neutral"],
    "to_status": "at_risk"
  }
}

Filtros soportados dependen del evento. Ver GET /v1/webhook-events/{event}/filters.

Gestionar suscripciones

GET /v1/webhook-subscriptions           # lista
GET /v1/webhook-subscriptions/{id}      # detalle + stats (deliveries último 7d)
PATCH /v1/webhook-subscriptions/{id}    # update URL, events, secret
DELETE /v1/webhook-subscriptions/{id}   # eliminar

Ver deliveries

GET /v1/webhook-subscriptions/{id}/deliveries

Query: status (success | failed | pending), from, to.

Respuesta incluye request headers, body, response status, latency — útil para debug.

Replay

Si tu endpoint estuvo caído y quieres reprocesar:

POST /v1/webhook-subscriptions/{id}/deliveries/{delivery_id}/replay

UI

También puedes gestionar suscripciones desde Admin → API → Webhooks:

  • Crear / editar / eliminar.
  • Ver deliveries recientes con response.
  • Test endpoint (LealUp manda un evento de prueba).

Seguridad

  • HTTPS obligatorio — endpoints HTTP son rechazados.
  • Firma HMAC — siempre verifica (previene spoofing).
  • Timestamp check — rechaza requests >5 min (previene replay).
  • Rotación de secretPATCH .../secret — LealUp firma con el nuevo secret inmediatamente; tú tienes que actualizar tu endpoint a aceptar el nuevo.
  • IP allowlist — opcional, pide rango de IPs de LealUp a soporte ([email protected]).

Límites

  • Max 50 suscripciones por workspace.
  • Max 100 requests/seg entrantes a tu endpoint (burst).
  • Max retention de deliveries logs: 30 días (90 en Scale+).

Problemas comunes

"Estoy recibiendo eventos duplicados"

  • Normal — at-least-once delivery. Usa delivery_id para idempotencia.

"No recibo nada"

  • Verifica URL en GET /v1/webhook-subscriptions/{id}.
  • Chequea deliveries: GET .../deliveries?status=failed para ver si están fallando.
  • Test manual: Admin → API → Webhooks → [sub] → Test.
  • Firewalls/VPN — LealUp conecta desde IPs fijas (pide allowlist a soporte).

"Firma no valida"

  • Timestamp skew — tu server y LealUp deben estar sincronizados (<5 min).
  • No estás usando el raw body — muchos frameworks parsean JSON automáticamente. Usa el body original como bytes.
  • Secret incorrecto — verifica que tu secret almacenado coincide con el que LealUp tiene.

"Llegan muy tarde"

  • Si tu endpoint toma >2 seg, el throughput baja (el worker espera). Responde 202 Accepted rápido y procesa async.

Ejemplo completo (Python + FastAPI)

from fastapi import FastAPI, Request, HTTPException
import hmac, hashlib, time

app = FastAPI()
SECRET = os.environ["LEALUP_WEBHOOK_SECRET"]

@app.post("/lealup-webhook")
async def webhook(request: Request):
    body = await request.body()
    sig = request.headers.get("x-lealup-signature", "")
    ts = request.headers.get("x-lealup-timestamp", "0")

    if abs(time.time() - int(ts)) > 300:
        raise HTTPException(401, "Stale timestamp")

    expected = hmac.new(
        SECRET.encode(),
        f"{ts}.".encode() + body,
        hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest(expected, sig.replace("sha256=", "")):
        raise HTTPException(401, "Invalid signature")

    payload = await request.json()
    delivery_id = payload["delivery_id"]

    if await already_processed(delivery_id):
        return {"status": "already_processed"}

    await process_event(payload["event"], payload["data"])
    await mark_processed(delivery_id)
    return {"status": "ok"}

On this page