Webhooks
Webhooks
O Gemlead envia uma requisição HTTPS assinada ao seu servidor no momento em que um lead é encontrado, fica quente, responde ou reage à sua mensagem, ou quando o Sam AI tem uma resposta pronta, envia uma resposta ou passa uma conversa para você. A assinatura prova que ela vem da gente.
Configurar um endpoint
Adicione um endpoint no app, em API, ou com Criar um endpoint de webhook. Informe uma URL HTTPS pública e os tipos de evento que você quer, e guarde o secret retornado para verificar as assinaturas.
- Só HTTPS, sem credenciais na URL e sem endereço privado, de loopback ou link local.
- Até 10 endpoints por conta.
filters.persona_idslimita os eventos que um endpoint recebe aos leads de certas personas, seja qual for o tipo.- Envie um evento de teste para conferir seu servidor antes que os eventos reais cheguem.
Os webhooks vêm com os planos Business e Enterprise. Se a conta passar para o Free ou o Starter, os endpoints continuam lá, mas não enviam nada até ela voltar para Business ou 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
Todo evento tem o mesmo envelope. O data dele depende do type.
| Tipo | Enviado quando |
|---|---|
lead.created | O Gemlead encontra e pontua um novo lead, seja qual for o score. Cada lead dispara o evento uma vez só, com o primeiro score, e apenas até 3 dias depois de ser encontrado, então repontuar um lead mais antigo nunca o dispara. Um lead que já recebe 60 ou mais de cara dispara também lead.hot. |
lead.hot | O score de um lead chega a 60 ou mais pela primeira vez. Cada lead dispara o evento uma vez só, e repontuar o lead nunca o dispara de novo. |
reply.received | Um lead envia uma nova mensagem para você, numa campanha ou não. Cada mensagem dispara o evento uma vez, se tiver menos de 6 horas. Quando muitas mensagens do lead chegam ao mesmo tempo, só as 5 mais recentes disparam o evento. |
reaction.received | Um lead reage a uma das suas mensagens com um emoji. Cada emoji do lead numa mensagem dispara o evento uma vez. |
sam.suggestion.ready | O Sam AI tem uma nova resposta pronta para você enviar, numa conversa no modo draft. Cada sugestão dispara o evento uma vez, se tiver menos de 6 horas. |
sam.reply.sent | Uma resposta do Sam AI sai pela sua conta do X, sozinha no modo auto ou depois que você a aprova no app. Cada resposta dispara o evento uma vez, se tiver menos de 6 horas. As mensagens que você mesmo escreve nunca disparam o evento, elas aparecem em Obter uma conversa. |
sam.handoff | O Sam AI passa uma conversa para você quando não vê uma boa resposta sem a sua ajuda, quando o lead mandou só uma foto ou uma mensagem de voz, ou quando a resposta dele não conseguiu sair. No modo auto, ele então deixa as próximas mensagens do lead com você. Cada repasse dispara o evento uma vez, se tiver menos de 6 horas. |
O objeto event
- idstringIdentificador único do evento,
evt_seguido de 24 caracteres. Também vem no cabeçalhoGemlead-Event-Id. - objectstringSempre
event. - typestringO tipo do evento.
lead.createdlead.hotreply.receivedreaction.receivedsam.suggestion.readysam.reply.sentsam.handoff - created_atstringQuando o evento aconteceu, em ISO 8601.
- account_idstringA conta do Gemlead em que ele aconteceu.
- dataobjectO conteúdo do evento, que depende do tipo.
lead.created
O Gemlead encontra e pontua um novo lead, seja qual for o score. Cada lead dispara o evento uma vez só, com o primeiro score, e apenas até 3 dias depois de ser encontrado, então repontuar um lead mais antigo nunca o dispara. Um lead que já recebe 60 ou mais de cara dispara também lead.hot.
Dados
- objectobjectO lead, como Obter um lead o retorna.
{
"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
O score de um lead chega a 60 ou mais pela primeira vez. Cada lead dispara o evento uma vez só, e repontuar o lead nunca o dispara de novo.
Dados
- objectobjectO lead, como Obter um lead o retorna.
{
"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
Um lead envia uma nova mensagem para você, numa campanha ou não. Cada mensagem dispara o evento uma vez, se tiver menos de 6 horas. Quando muitas mensagens do lead chegam ao mesmo tempo, só as 5 mais recentes disparam o evento.
Dados
- leadobjectO lead que escreveu.
- messageobject
- campaignobject ou nullA campanha em que o lead está, com
id,nameestatus. - conversation_idstringUse com Obter uma conversa para ver o histórico 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
Um lead reage a uma das suas mensagens com um emoji. Cada emoji do lead numa mensagem dispara o evento uma vez.
{
"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
O Sam AI tem uma nova resposta pronta para você enviar, numa conversa no modo draft. Cada sugestão dispara o evento uma vez, se tiver menos de 6 horas.
Dados
- leadobjectO lead a quem o Sam AI responde.
- conversation_idstringUse com Obter o Sam AI de uma conversa para ler a sugestão de novo ou mudar o modo.
- campaignobject ou nullA campanha em que o lead está, com
id,nameestatus. - suggestionobjectA resposta que o Sam AI tem pronta.
{
"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
Uma resposta do Sam AI sai pela sua conta do X, sozinha no modo auto ou depois que você a aprova no app. Cada resposta dispara o evento uma vez, se tiver menos de 6 horas. As mensagens que você mesmo escreve nunca disparam o evento, elas aparecem em Obter uma conversa.
Dados
- leadobjectO lead a quem o Sam AI respondeu.
- conversation_idstringUse com Obter uma conversa para ver o histórico completo.
- campaignobject ou nullA campanha em que o lead está, com
id,nameestatus. - messagesarray de stringsAs DMs que o Sam AI enviou, em ordem.
- sent_bystring
autoquando o Sam AI enviou sozinho,approvedquando você aprovou no app.autoapproved - replying_to_message_idstring ou nullA mensagem do lead que ela respondeu.
- sent_atstringQuando ela foi enviada, em 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
O Sam AI passa uma conversa para você quando não vê uma boa resposta sem a sua ajuda, quando o lead mandou só uma foto ou uma mensagem de voz, ou quando a resposta dele não conseguiu sair. No modo auto, ele então deixa as próximas mensagens do lead com você. Cada repasse dispara o evento uma vez, se tiver menos de 6 horas.
Dados
- leadobjectO lead a responder.
- conversation_idstringUse com Obter uma conversa para ver o histórico completo.
- campaignobject ou nullA campanha em que o lead está, com
id,nameestatus. - reasonstring ou nullPor que o Sam AI passa a conversa para você, nas palavras dele.
{
"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."
}
}Envio
O Gemlead envia cada evento como um POST em JSON e espera 10 segundos pela sua resposta. Qualquer 2xx conta como entregue. Um redirecionamento, qualquer outro status, um timeout ou um erro de conexão conta como falha, e o evento é enviado de novo.
| Cabeçalho | Valor |
|---|---|
Gemlead-Signature | O timestamp e a assinatura, t=1762444800,v1=5f2b... |
Gemlead-Event-Id | O id do evento, para pular eventos que você já tratou. |
Gemlead-Event-Type | O type do 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 assinaturas
Qualquer pessoa pode enviar uma requisição para a sua URL. Confira se cada uma vem do Gemlead antes de confiar nela.
- Leia o corpo bruto como bytes, antes de qualquer parsing de JSON. Fazer o parsing e reescrever o corpo muda os bytes.
- Divida o cabeçalho
Gemlead-Signaturenas vírgulas.té um timestamp Unix em segundos,v1é a assinatura. - Calcule o HMAC SHA256 de
t, um ponto e o corpo bruto, usando como chave o segredo inteiro do endpoint,whsec_incluído, e codifique o resultado em hexadecimal. - Compare o resultado com cada
v1em tempo constante. - Recuse um
tcom mais de 5 minutos de diferença do seu relógio, para que uma requisição antiga não possa ser reenviada. Cada nova tentativa é assinada com umtnovo.
Responda 400 quando a verificação falhar. Guarde o segredo numa variável de ambiente 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);Novas tentativas
Quando um envio falha, o Gemlead tenta de novo 7 vezes, com uma espera maior a cada vez. Depois da 8ª tentativa, cerca de 45 horas após a primeira, o envio é marcado como falha.
| Tentativa | Enviada |
|---|---|
| 1 | Na hora |
| 2 | 1 minuto após a falha anterior |
| 3 | 5 minutos após a falha anterior |
| 4 | 30 minutos após a falha anterior |
| 5 | 2 horas após a falha anterior |
| 6 | 6 horas após a falha anterior |
| 7 | 12 horas após a falha anterior |
| 8 | 24 horas após a falha anterior |
Um endpoint que falha por 3 dias seguidos, com pelo menos 8 tentativas com falha, é desativado. O status dele passa a disabled, o disabled_reason a failing, e os envios ainda pendentes terminam como falha. Corrija seu servidor e depois reative o endpoint no app ou com Atualizar um endpoint de webhook, o que zera a contagem. Os eventos de teste são enviados uma vez só e nunca entram nessa contagem.
Listar envios mostra o código de status e o erro de cada tentativa, para você ver o que seu servidor respondeu.
Boas práticas
- Responda 2xx em até 10 segundos e depois faça o trabalho numa fila.
- Cada evento é entregue pelo menos uma vez. Use o
iddo evento, também emGemlead-Event-Id, para pular um evento que você já tratou. - Os eventos podem chegar fora de ordem. Compare
created_at, ou busque o lead de novo para ter o estado mais recente. - Verifique toda assinatura e recuse timestamps antigos.
- Renovar o segredo substitui o atual na hora. Faça o deploy do novo logo em seguida. Os envios que falharem nesse meio-tempo são repetidos e assinados com ele.
- Eventos e envios ficam guardados por 30 dias.