Webhooks
Webhooks
Gemlead envía una solicitud HTTPS a tu servidor en cuanto encuentra un lead, o un lead se vuelve caliente, responde o reacciona a tu mensaje, y también cuando Sam AI tiene una respuesta lista, envía una o te pasa una conversación. Va firmada, para que sepas que viene de nosotros.
Configura un endpoint
Añade un endpoint en la app, en API, o con Crear un endpoint de webhook. Dale una URL HTTPS pública y los tipos de evento que quieras, y guarda el secret que devuelve para verificar las firmas.
- Solo HTTPS, sin credenciales en la URL y sin direcciones privadas, loopback ni link local.
- Hasta 10 endpoints por cuenta.
filters.persona_idslimita los eventos que recibe un endpoint a los leads de ciertos buyer personas, sea cual sea su tipo.- Envía un evento de prueba para comprobar tu servidor antes de que lleguen los eventos reales.
Los webhooks vienen con los planes Business y Enterprise. Si la cuenta pasa a Free o Starter, sus endpoints se conservan pero no envían nada hasta que vuelva a Business o Enterprise.
curl -X POST https://gemlead.ai/api/v1/webhook_endpoints \
-H "Authorization: Bearer $GEMLEAD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/gemlead",
"events": ["lead.hot", "reply.received"],
"description": "CRM sync",
"filters": {
"persona_ids": ["lvWBPnytpS"]
}
}'Tipos de evento
Todos los eventos comparten la misma estructura. Su data depende de su type.
| Tipo | Se envía cuando |
|---|---|
lead.created | Gemlead encuentra y puntúa un lead nuevo, sea cual sea su puntuación. Se envía una sola vez por lead, con su primera puntuación, y solo en los 3 días siguientes a encontrarlo, así que volver a puntuar un lead más antiguo nunca lo envía. Si el lead obtiene 60 o más desde el primer momento, también se envía lead.hot. |
lead.hot | La puntuación de un lead llega a 60 o más por primera vez. Se envía una sola vez por lead, y volver a puntuarlo nunca lo reenvía. |
reply.received | Un lead te envía un mensaje nuevo, en una campaña o no. Se envía una sola vez por mensaje, si tiene menos de 6 horas. Cuando llegan muchos mensajes del lead a la vez, solo se envía para los 5 más recientes. |
reaction.received | Un lead reacciona a uno de tus mensajes con un emoji. Se envía una sola vez por cada emoji del lead en un mensaje. |
sam.suggestion.ready | Sam AI tiene lista una respuesta nueva para que la envíes, en una conversación en modo draft. Se envía una sola vez por sugerencia, si tiene menos de 6 horas. |
sam.reply.sent | Una respuesta de Sam AI sale desde tu cuenta de X, sola en modo auto o cuando la apruebas en la app. Se envía una sola vez por respuesta, si tiene menos de 6 horas. Los mensajes que escribes tú nunca lo envían, aparecen en Obtener una conversación. |
sam.handoff | Sam AI te pasa una conversación cuando no ve una buena respuesta sin ti, cuando el lead solo envió una foto o una nota de voz, o cuando su respuesta no se pudo enviar. En modo auto, a partir de ahí te deja a ti los siguientes mensajes del lead. Se envía una sola vez por traspaso, si tiene menos de 6 horas. |
El objeto event
- idstringIdentificador único del evento,
evt_seguido de 24 caracteres. También en el encabezadoGemlead-Event-Id. - objectstringSiempre
event. - typestringEl tipo de evento.
lead.createdlead.hotreply.receivedreaction.receivedsam.suggestion.readysam.reply.sentsam.handoff - created_atstringCuándo ocurrió el evento, en ISO 8601.
- account_idstringLa cuenta de Gemlead en la que ocurrió.
- dataobjectEl contenido del evento, que depende de su tipo.
lead.created
Gemlead encuentra y puntúa un lead nuevo, sea cual sea su puntuación. Se envía una sola vez por lead, con su primera puntuación, y solo en los 3 días siguientes a encontrarlo, así que volver a puntuar un lead más antiguo nunca lo envía. Si el lead obtiene 60 o más desde el primer momento, también se envía lead.hot.
Datos
- objectobjectEl lead, tal como lo devuelve Obtener un lead.
{
"id": "evt_8kQ2vXr4LmT9wPz1Hc6YbN3d",
"object": "event",
"type": "lead.created",
"created_at": "2026-11-05T14:03:40.117Z",
"account_id": "a9PzK2vRtM",
"data": {
"object": {
"id": "k3VnQ8wZpR",
"object": "lead",
"profile": {
"x_user_id": "1592047713",
"username": "mark_picks",
"name": "Mark Ellis",
"bio": "Free NBA picks every day. Sports fan first.",
"followers_count": 2150,
"following_count": 940,
"location": "Denver, CO",
"website": "https://markpicks.net",
"profile_image_url": "https://pbs.twimg.com/profile_images/1592047713/mark_400x400.jpg",
"verified": false,
"url": "https://x.com/mark_picks"
},
"score": 38,
"score_reason": "Shares free picks for fun and sells nothing yet, no sign of a paying community.",
"hot": false,
"status": "scored",
"persona": {
"id": "lvWBPnytpS",
"name": "Tipsters"
},
"campaign": null,
"conversation_id": null,
"locked_to": null,
"found_at": "2026-11-05T14:02:11.482Z",
"scored_at": "2026-11-05T14:03:40.117Z",
"updated_at": "2026-11-05T14:03:40.117Z",
"metadata": {}
}
}
}lead.hot
La puntuación de un lead llega a 60 o más por primera vez. Se envía una sola vez por lead, y volver a puntuarlo nunca lo reenvía.
Datos
- objectobjectEl lead, tal como lo devuelve Obtener un lead.
{
"id": "evt_8kQ2vXr4LmT9wPz1Hc6YbN3d",
"object": "event",
"type": "lead.hot",
"created_at": "2026-11-05T14:03:40.117Z",
"account_id": "a9PzK2vRtM",
"data": {
"object": {
"id": "msgOtJ8dvT",
"object": "lead",
"profile": {
"x_user_id": "1460283925",
"username": "jane_builds",
"name": "Jane Doe",
"bio": "Runs a paid trading community. Daily setups, no hype.",
"followers_count": 12400,
"following_count": 380,
"location": "Austin, TX",
"website": "https://janedoe.com",
"profile_image_url": "https://pbs.twimg.com/profile_images/1460283925/jane_400x400.jpg",
"verified": true,
"url": "https://x.com/jane_builds"
},
"score": 86,
"score_reason": "Runs a paid trading community and asks how to collect payments for it.",
"hot": true,
"status": "scored",
"persona": {
"id": "lvWBPnytpS",
"name": "Tipsters"
},
"campaign": null,
"conversation_id": null,
"locked_to": null,
"found_at": "2026-11-05T14:02:11.482Z",
"scored_at": "2026-11-05T14:03:40.117Z",
"updated_at": "2026-11-05T14:03:40.117Z",
"metadata": {}
}
}
}reply.received
Un lead te envía un mensaje nuevo, en una campaña o no. Se envía una sola vez por mensaje, si tiene menos de 6 horas. Cuando llegan muchos mensajes del lead a la vez, solo se envía para los 5 más recientes.
Datos
- leadobjectEl lead que escribió.
- messageobject
- campaignobject o nullLa campaña en la que está el lead, con su
id,nameystatus. - conversation_idstringÚsalo con Obtener una conversación para tener el hilo completo.
{
"id": "evt_8kQ2vXr4LmT9wPz1Hc6YbN3d",
"object": "event",
"type": "reply.received",
"created_at": "2026-11-05T14:03:40.117Z",
"account_id": "a9PzK2vRtM",
"data": {
"lead": {
"id": "msgOtJ8dvT",
"object": "lead",
"profile": {
"x_user_id": "1460283925",
"username": "jane_builds",
"name": "Jane Doe",
"bio": "Runs a paid trading community. Daily setups, no hype.",
"followers_count": 12400,
"following_count": 380,
"location": "Austin, TX",
"website": "https://janedoe.com",
"profile_image_url": "https://pbs.twimg.com/profile_images/1460283925/jane_400x400.jpg",
"verified": true,
"url": "https://x.com/jane_builds"
},
"score": 86,
"score_reason": "Runs a paid trading community and asks how to collect payments for it.",
"hot": true,
"status": "in_conversation",
"persona": {
"id": "lvWBPnytpS",
"name": "Tipsters"
},
"campaign": {
"id": "c7RkQ2wLxe",
"name": "Tipsters warm intro",
"status": "in_progress"
},
"conversation_id": "msgOtJ8dvT",
"locked_to": null,
"found_at": "2026-11-05T14:02:11.482Z",
"scored_at": "2026-11-05T14:03:40.117Z",
"updated_at": "2026-11-05T14:03:40.117Z",
"metadata": {}
},
"message": {
"id": "1985321409876543210",
"object": "message",
"direction": "inbound",
"text": "Yes, I want to charge for my Discord. How does it work?",
"created_at": "2026-11-06T08:12:44.000Z",
"media": [],
"reactions": [],
"encrypted": false,
"reply_to_message_id": null
},
"campaign": {
"id": "c7RkQ2wLxe",
"name": "Tipsters warm intro",
"status": "in_progress"
},
"conversation_id": "msgOtJ8dvT"
}
}reaction.received
Un lead reacciona a uno de tus mensajes con un emoji. Se envía una sola vez por cada emoji del lead en un mensaje.
{
"id": "evt_8kQ2vXr4LmT9wPz1Hc6YbN3d",
"object": "event",
"type": "reaction.received",
"created_at": "2026-11-05T14:03:40.117Z",
"account_id": "a9PzK2vRtM",
"data": {
"lead": {
"id": "msgOtJ8dvT",
"object": "lead",
"profile": {
"x_user_id": "1460283925",
"username": "jane_builds",
"name": "Jane Doe",
"bio": "Runs a paid trading community. Daily setups, no hype.",
"followers_count": 12400,
"following_count": 380,
"location": "Austin, TX",
"website": "https://janedoe.com",
"profile_image_url": "https://pbs.twimg.com/profile_images/1460283925/jane_400x400.jpg",
"verified": true,
"url": "https://x.com/jane_builds"
},
"score": 86,
"score_reason": "Runs a paid trading community and asks how to collect payments for it.",
"hot": true,
"status": "in_conversation",
"persona": {
"id": "lvWBPnytpS",
"name": "Tipsters"
},
"campaign": {
"id": "c7RkQ2wLxe",
"name": "Tipsters warm intro",
"status": "in_progress"
},
"conversation_id": "msgOtJ8dvT",
"locked_to": null,
"found_at": "2026-11-05T14:02:11.482Z",
"scored_at": "2026-11-05T14:03:40.117Z",
"updated_at": "2026-11-05T14:03:40.117Z",
"metadata": {}
},
"message": {
"id": "1985329981234567890",
"object": "message",
"direction": "outbound",
"text": "Happy to show you. Do you sell picks monthly or per event?",
"created_at": "2026-11-06T08:20:03.000Z",
"media": [],
"reactions": [
{
"emoji": "👍",
"direction": "inbound",
"created_at": "2026-11-06T08:24:51.000Z"
}
],
"encrypted": false,
"reply_to_message_id": "1985321409876543210"
},
"reaction": {
"emoji": "👍",
"direction": "inbound",
"created_at": "2026-11-06T08:24:51.000Z"
}
}
}sam.suggestion.ready
Sam AI tiene lista una respuesta nueva para que la envíes, en una conversación en modo draft. Se envía una sola vez por sugerencia, si tiene menos de 6 horas.
Datos
- leadobjectEl lead al que responde Sam AI.
- conversation_idstringÚsalo con Obtener el Sam AI de una conversación para volver a leer la sugerencia o cambiar el modo.
- campaignobject o nullLa campaña en la que está el lead, con su
id,nameystatus. - suggestionobjectLa respuesta que Sam AI tiene lista.
{
"id": "evt_8kQ2vXr4LmT9wPz1Hc6YbN3d",
"object": "event",
"type": "sam.suggestion.ready",
"created_at": "2026-11-05T14:03:40.117Z",
"account_id": "a9PzK2vRtM",
"data": {
"lead": {
"id": "msgOtJ8dvT",
"object": "lead",
"profile": {
"x_user_id": "1460283925",
"username": "jane_builds",
"name": "Jane Doe",
"bio": "Runs a paid trading community. Daily setups, no hype.",
"followers_count": 12400,
"following_count": 380,
"location": "Austin, TX",
"website": "https://janedoe.com",
"profile_image_url": "https://pbs.twimg.com/profile_images/1460283925/jane_400x400.jpg",
"verified": true,
"url": "https://x.com/jane_builds"
},
"score": 86,
"score_reason": "Runs a paid trading community and asks how to collect payments for it.",
"hot": true,
"status": "in_conversation",
"persona": {
"id": "lvWBPnytpS",
"name": "Tipsters"
},
"campaign": {
"id": "c7RkQ2wLxe",
"name": "Tipsters warm intro",
"status": "in_progress"
},
"conversation_id": "msgOtJ8dvT",
"locked_to": null,
"found_at": "2026-11-05T14:02:11.482Z",
"scored_at": "2026-11-05T14:03:40.117Z",
"updated_at": "2026-11-05T14:03:40.117Z",
"metadata": {}
},
"conversation_id": "msgOtJ8dvT",
"campaign": {
"id": "c7RkQ2wLxe",
"name": "Tipsters warm intro",
"status": "in_progress"
},
"suggestion": {
"messages": [
"It takes two minutes. You set a price, members pay, and they get their Discord role right away.",
"Want the link to set it up?"
],
"stage": "answer_question",
"why": "She asked how it works, so the reply explains it in one line and offers the link.",
"replying_to_message_id": "1985321409876543210",
"created_at": "2026-11-06T08:13:20.000Z"
}
}
}sam.reply.sent
Una respuesta de Sam AI sale desde tu cuenta de X, sola en modo auto o cuando la apruebas en la app. Se envía una sola vez por respuesta, si tiene menos de 6 horas. Los mensajes que escribes tú nunca lo envían, aparecen en Obtener una conversación.
Datos
- leadobjectEl lead al que respondió Sam AI.
- conversation_idstringÚsalo con Obtener una conversación para tener el hilo completo.
- campaignobject o nullLa campaña en la que está el lead, con su
id,nameystatus. - messagesarray de stringsLos DM que envió Sam AI, en orden.
- sent_bystring
autocuando Sam AI la envió solo,approvedcuando la aprobaste en la app.autoapproved - replying_to_message_idstring o nullEl mensaje del lead al que respondió.
- sent_atstringCuándo se envió, en ISO 8601.
{
"id": "evt_8kQ2vXr4LmT9wPz1Hc6YbN3d",
"object": "event",
"type": "sam.reply.sent",
"created_at": "2026-11-05T14:03:40.117Z",
"account_id": "a9PzK2vRtM",
"data": {
"lead": {
"id": "msgOtJ8dvT",
"object": "lead",
"profile": {
"x_user_id": "1460283925",
"username": "jane_builds",
"name": "Jane Doe",
"bio": "Runs a paid trading community. Daily setups, no hype.",
"followers_count": 12400,
"following_count": 380,
"location": "Austin, TX",
"website": "https://janedoe.com",
"profile_image_url": "https://pbs.twimg.com/profile_images/1460283925/jane_400x400.jpg",
"verified": true,
"url": "https://x.com/jane_builds"
},
"score": 86,
"score_reason": "Runs a paid trading community and asks how to collect payments for it.",
"hot": true,
"status": "in_conversation",
"persona": {
"id": "lvWBPnytpS",
"name": "Tipsters"
},
"campaign": {
"id": "c7RkQ2wLxe",
"name": "Tipsters warm intro",
"status": "in_progress"
},
"conversation_id": "msgOtJ8dvT",
"locked_to": null,
"found_at": "2026-11-05T14:02:11.482Z",
"scored_at": "2026-11-05T14:03:40.117Z",
"updated_at": "2026-11-05T14:03:40.117Z",
"metadata": {}
},
"conversation_id": "msgOtJ8dvT",
"campaign": {
"id": "c7RkQ2wLxe",
"name": "Tipsters warm intro",
"status": "in_progress"
},
"messages": [
"It takes two minutes. You set a price, members pay, and they get their Discord role right away.",
"Want the link to set it up?"
],
"sent_by": "auto",
"replying_to_message_id": "1985321409876543210",
"sent_at": "2026-11-06T08:14:05.000Z"
}
}sam.handoff
Sam AI te pasa una conversación cuando no ve una buena respuesta sin ti, cuando el lead solo envió una foto o una nota de voz, o cuando su respuesta no se pudo enviar. En modo auto, a partir de ahí te deja a ti los siguientes mensajes del lead. Se envía una sola vez por traspaso, si tiene menos de 6 horas.
Datos
- leadobjectEl lead al que hay que responder.
- conversation_idstringÚsalo con Obtener una conversación para tener el hilo completo.
- campaignobject o nullLa campaña en la que está el lead, con su
id,nameystatus. - reasonstring o nullEl motivo por el que Sam AI te la pasa, tal como lo escribió.
{
"id": "evt_8kQ2vXr4LmT9wPz1Hc6YbN3d",
"object": "event",
"type": "sam.handoff",
"created_at": "2026-11-05T14:03:40.117Z",
"account_id": "a9PzK2vRtM",
"data": {
"lead": {
"id": "msgOtJ8dvT",
"object": "lead",
"profile": {
"x_user_id": "1460283925",
"username": "jane_builds",
"name": "Jane Doe",
"bio": "Runs a paid trading community. Daily setups, no hype.",
"followers_count": 12400,
"following_count": 380,
"location": "Austin, TX",
"website": "https://janedoe.com",
"profile_image_url": "https://pbs.twimg.com/profile_images/1460283925/jane_400x400.jpg",
"verified": true,
"url": "https://x.com/jane_builds"
},
"score": 86,
"score_reason": "Runs a paid trading community and asks how to collect payments for it.",
"hot": true,
"status": "in_conversation",
"persona": {
"id": "lvWBPnytpS",
"name": "Tipsters"
},
"campaign": {
"id": "c7RkQ2wLxe",
"name": "Tipsters warm intro",
"status": "in_progress"
},
"conversation_id": "msgOtJ8dvT",
"locked_to": null,
"found_at": "2026-11-05T14:02:11.482Z",
"scored_at": "2026-11-05T14:03:40.117Z",
"updated_at": "2026-11-05T14:03:40.117Z",
"metadata": {}
},
"conversation_id": "msgOtJ8dvT",
"campaign": {
"id": "c7RkQ2wLxe",
"name": "Tipsters warm intro",
"status": "in_progress"
},
"reason": "She wants a call before she signs up."
}
}Envío
Gemlead envía cada evento como un POST en JSON y espera tu respuesta durante 10 segundos. Cualquier 2xx cuenta como entregado. Una redirección, cualquier otro estado, un timeout o un error de conexión cuentan como fallo, y el evento se reintenta.
| Encabezado | Valor |
|---|---|
Gemlead-Signature | El timestamp y la firma, t=1762444800,v1=5f2b... |
Gemlead-Event-Id | El id del evento, para ignorar los eventos que ya procesaste. |
Gemlead-Event-Type | El type del evento. |
User-Agent | Gemlead-Webhooks/1.0 |
POST /webhooks/gemlead HTTP/1.1
Content-Type: application/json; charset=utf-8
User-Agent: Gemlead-Webhooks/1.0
Gemlead-Event-Id: evt_8kQ2vXr4LmT9wPz1Hc6YbN3d
Gemlead-Event-Type: lead.hot
Gemlead-Signature: t=1762444800,v1=58ca5e964b92d617c979d3f067cdc2035a1a1a9d3f848d7d927c2db09b8e9baaVerificar firmas
Cualquiera puede enviar una solicitud a tu URL. Comprueba que cada una viene de Gemlead antes de fiarte de ella.
- Lee el cuerpo en bruto como bytes, antes de parsear el JSON. Parsearlo y volver a serializarlo cambia los bytes.
- Separa el encabezado
Gemlead-Signaturepor las comas.tes un timestamp Unix en segundos, yv1, la firma. - Calcula el HMAC SHA256 de
t, un punto y el cuerpo en bruto, con el secreto completo del endpoint como clave,whsec_incluido, y exprésalo en hexadecimal. - Compáralo con cada
v1en tiempo constante. - Rechaza un
tque se aleje más de 5 minutos de tu reloj, para que nadie pueda reenviar una solicitud antigua. Cada reintento se firma con untnuevo.
Responde 400 si la verificación falla. Guarda el secreto en una variable de entorno como GEMLEAD_WEBHOOK_SECRET.
const crypto = require("crypto");
const express = require("express");
// rawBody: the request body exactly as received, before any JSON parsing.
function verifyGemleadSignature(rawBody, header, secret, toleranceSeconds = 300) {
if (typeof header !== "string") return false;
let timestamp = null;
const signatures = [];
for (const part of header.split(",")) {
const [key, value] = part.split("=", 2).map((s) => (s || "").trim());
if (key === "t") timestamp = value;
if (key === "v1" && value) signatures.push(value);
}
if (!/^\d+$/.test(timestamp || "") || signatures.length === 0) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSeconds) return false;
const expected = crypto.createHmac("sha256", secret).update(`${timestamp}.`).update(rawBody).digest();
return signatures.some((signature) => {
const given = Buffer.from(signature, "hex");
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
});
}
const app = express();
// The body must stay raw on this route.
app.post("/webhooks/gemlead", express.raw({ type: "application/json" }), (req, res) => {
const ok = verifyGemleadSignature(
req.body,
req.get("Gemlead-Signature"),
process.env.GEMLEAD_WEBHOOK_SECRET,
);
if (!ok) return res.status(400).send("Invalid signature");
const event = JSON.parse(req.body.toString("utf8"));
// Already handled? Skip it, retries send the same event.id again.
if (event.type === "lead.created") {
// event.data.object is the new lead, hot or not
} else if (event.type === "lead.hot") {
// event.data.object is the lead
} else if (event.type === "reply.received") {
// event.data.lead, event.data.message, event.data.campaign, event.data.conversation_id
}
res.sendStatus(200);
});
app.listen(3000);Reintentos
Cuando un envío falla, Gemlead lo reintenta 7 veces, cada vez tras una espera más larga. Después del 8.º intento, unas 45 horas después del primero, el envío se marca como fallido.
| Intento | Se envía |
|---|---|
| 1 | Al instante |
| 2 | 1 minuto después del fallo anterior |
| 3 | 5 minutos después del fallo anterior |
| 4 | 30 minutos después del fallo anterior |
| 5 | 2 horas después del fallo anterior |
| 6 | 6 horas después del fallo anterior |
| 7 | 12 horas después del fallo anterior |
| 8 | 24 horas después del fallo anterior |
Un endpoint que falla 3 días seguidos, con al menos 8 intentos fallidos, se desactiva. Su status pasa a disabled, su disabled_reason a failing, y los envíos pendientes terminan como fallidos. Arregla tu servidor y vuelve a activarlo en la app o con Actualizar un endpoint de webhook, lo que reinicia el recuento. Los eventos de prueba se envían una sola vez y nunca entran en el recuento.
Listar envíos muestra el código de estado y el error de cada intento, para ver qué respondió tu servidor.
Buenas prácticas
- Responde 2xx en menos de 10 segundos y haz el trabajo después, en una cola.
- Cada evento se entrega al menos una vez. Usa el
iddel evento, también enGemlead-Event-Id, para ignorar un evento que ya procesaste. - Los eventos pueden llegar desordenados. Compara
created_at, o vuelve a pedir el lead para tener su último estado. - Verifica todas las firmas y rechaza los timestamps antiguos.
- Renovar el secreto lo reemplaza al instante. Despliega el nuevo justo después, los envíos que fallen mientras tanto se reintentan firmados con él.
- Los eventos y los envíos se guardan 30 días.