Webhooks
Un endpoint de webhook es una URL https tuya a la que letsrealty llama cada vez que pasa algo con tus Leads: un Lead nuevo, una edición, un cambio de estado, una nueva consulta, una actividad completada. Las llamadas suelen llegar en menos de un minuto después del cambio.
Eventos
lead.created— se creó un Lead, por cualquier vía.lead.updated— cambiaron campos de contacto o del pipeline;data.changed_fieldsindica cuáles.lead.status_changed— un estado nuevo;data.previous_statustiene el anterior.lead.assigned— un nuevo responsable del Lead;data.previous_assigned_agent_idtiene el anterior.lead.scored— la IA calificó el Lead.lead.archived— se archivó el Lead.inquiry.created— un Lead se puso en contacto;data.leadtrae el Lead.activity.completed— se marcó una actividad como hecha.ping— una prueba enviada desde la consola o la API; nunca se suscribe.
Todos los eventos tienen el mismo sobre; data es el recurso completo, nunca un diff, así que nunca necesitas una llamada adicional:
{
"id": "2f6d1c4e-…",
"type": "lead.status_changed",
"version": 1,
"created_at": "2026-09-30T14:05:00.123456+00:00",
"tenant": { "slug": "my-agency" },
"data": { "id": "6f1c…", "name": "Ana Rojas", "status": "contacted", "previous_status": "new", "…": "…" }
}
La referencia de la API documenta cada campo de los payloads en Webhooks.
Registrar un endpoint
Un administrador de la cuenta puede agregar uno en Integraciones → Webhooks en la consola, eligiendo sus eventos. La consola muestra el secreto de firma (whsec_…) una sola vez.
Una integración puede hacer lo mismo con una clave con webhooks:manage, el patrón REST-hook que usa Zapier:
curl https://api.letsrealty.io/v1/webhooks/subscriptions \
-H "Authorization: Bearer $LETSREALTY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/hooks/letsrealty", "events": ["lead.created", "inquiry.created"] }'
La respuesta 201 trae el id del endpoint y su secret; guarda ambos. DELETE /v1/webhooks/subscriptions/{endpoint_id} cancela la suscripción, y solo funciona con endpoints que suscribió la misma clave. Revocar una clave desactiva los endpoints que suscribió. Las URL deben ser https y accesibles desde internet.
Verificar una entrega
Cada llamada se firma según Standard Webhooks. Lleva tres headers:
webhook-id— eliddel evento; es el mismo en cada reintento y reenvío, así que úsalo para ignorar duplicados.webhook-timestamp— cuándo se envió este intento, en segundos Unix.webhook-signature—v1,seguido del HMAC-SHA256 en base64 de{webhook-id}.{webhook-timestamp}.{raw body}, con la parte de tu secreto después dewhsec_, decodificada de base64, como clave.
Verifica siempre contra el cuerpo sin procesar de la solicitud, antes de parsearlo, y rechaza timestamps que se alejen más de cinco minutos de tu reloj.
Python
Con el paquete standardwebhooks (pip install standardwebhooks):
from standardwebhooks.webhooks import Webhook
webhook = Webhook(LETSREALTY_WEBHOOK_SECRET) # "whsec_…"
def handle(raw_body: bytes, headers: dict[str, str]) -> None:
event = webhook.verify(raw_body, headers) # lanza una excepción si la firma es incorrecta
...
O solo con la biblioteca estándar:
import base64
import hashlib
import hmac
import time
def verify(secret: str, headers: dict[str, str], raw_body: bytes) -> None:
msg_id = headers["webhook-id"]
timestamp = headers["webhook-timestamp"]
if abs(time.time() - int(timestamp)) > 300:
raise ValueError("timestamp too far from now")
key = base64.b64decode(secret.removeprefix("whsec_"))
signed = f"{msg_id}.{timestamp}.".encode() + raw_body
expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
for candidate in headers["webhook-signature"].split():
version, _, signature = candidate.partition(",")
if version == "v1" and hmac.compare_digest(signature, expected):
return
raise ValueError("bad signature")
Node.js
Con el paquete standardwebhooks (npm install standardwebhooks), en Express:
import express from "express";
import { Webhook } from "standardwebhooks";
const webhook = new Webhook(process.env.LETSREALTY_WEBHOOK_SECRET); // "whsec_…"
const app = express();
app.post("/hooks/letsrealty", express.raw({ type: "application/json" }), (req, res) => {
let event;
try {
event = webhook.verify(req.body.toString("utf8"), req.headers);
} catch {
return res.sendStatus(400);
}
res.sendStatus(204);
// procesa `event` después de responder
});
O solo con node:crypto:
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(secret, headers, rawBody) {
const id = headers["webhook-id"];
const timestamp = headers["webhook-timestamp"];
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) throw new Error("timestamp too far from now");
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${rawBody}`).digest();
const ok = headers["webhook-signature"].split(" ").some((candidate) => {
const [version, signature] = candidate.split(",");
const given = Buffer.from(signature ?? "", "base64");
return version === "v1" && given.length === expected.length && timingSafeEqual(given, expected);
});
if (!ok) throw new Error("bad signature");
}
Respuestas, reintentos y reenvíos
Responde con cualquier 2xx en menos de 10 segundos; deja el trabajo lento para después. Cualquier otra cosa —un error, un timeout, una redirección— se reintenta después de 1 minuto, 5 minutos, 30 minutos, 2 horas, 6 horas, 12 horas, 24 horas y 24 horas: unos tres días en total. Los eventos pueden llegar desordenados; compara created_at o los propios campos del recurso cuando importe.
Después de 20 intentos fallidos seguidos, el endpoint se desactiva y se envía un email a los administradores de tu cuenta. Responder 410 Gone lo desactiva de inmediato. Volver a activarlo en la consola (o con PATCH /v1/webhooks/{endpoint_id} y "enabled": true) reinicia el conteo.
Cada intento queda en el registro de entregas del endpoint, en la consola y en GET /v1/webhooks/{endpoint_id}/deliveries. Para enviar uno de nuevo —por ejemplo, después de corregir un bug— usa Reenviar en la consola o POST /v1/webhooks/{endpoint_id}/deliveries/{delivery_id}:redeliver. Un reenvío mantiene el mismo webhook-id. POST /v1/webhooks/{endpoint_id}/test envía un ping.
Ponerse al día
Si tu endpoint estuvo caído más tiempo que los reintentos, GET /v1/events lista los eventos de los últimos 30 días, del más antiguo al más reciente, con el mismo sobre. Envía since —una hora con offset UTC, o el id del último evento que procesaste— y, opcionalmente, type:
curl "https://api.letsrealty.io/v1/events?since=2f6d1c4e-…" \
-H "Authorization: Bearer $LETSREALTY_API_KEY"
Una clave solo ve los eventos que cubren sus permisos de lectura: leads:read para lead.* e inquiry.*, activities:read para activity.*.
Rotar el secreto
POST /v1/webhooks/{endpoint_id}/rotate-secret (o Rotar secreto en la consola) emite un secreto nuevo. El anterior deja de verificar de inmediato, así que actualiza tu receptor enseguida.