Webhook
Webhook
Gemlead invia una richiesta HTTPS al tuo server nel momento in cui un lead viene trovato, diventa caldo, risponde o reagisce al tuo messaggio, oppure quando Sam AI ha una risposta pronta, ne invia una o ti passa una conversazione. È firmata, così sai che arriva da noi.
Configura un endpoint
Aggiungi un endpoint nell'app, alla voce API, oppure con Crea un endpoint webhook. Dagli un URL HTTPS pubblico e i tipi di evento che vuoi, poi conserva il secret che restituisce per verificare le firme.
- Solo HTTPS, nessuna credenziale nell'URL e nessun indirizzo privato, di loopback o link local.
- Fino a 10 endpoint per account.
filters.persona_idslimita gli eventi che riceve un endpoint ai lead di alcune buyer persona, qualunque sia il tipo di evento.- Invia un evento di test per verificare il tuo server prima che arrivino gli eventi reali.
I webhook sono inclusi nei piani Business ed Enterprise. Se l'account passa a Free o Starter, i suoi endpoint restano ma non inviano nulla finché non torna 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"]
}
}'Tipi di evento
Ogni evento ha la stessa struttura. Il suo data dipende dal suo type.
| Tipo | Inviato quando |
|---|---|
lead.created | Gemlead trova e valuta un nuovo lead, qualunque sia il punteggio. Arriva una sola volta per ogni lead, con il suo primo punteggio, e solo entro 3 giorni da quando è stato trovato, quindi una nuova valutazione di un lead più vecchio non lo invia mai. Un lead che ottiene subito 60 o più invia anche lead.hot. |
lead.hot | Il punteggio di un lead raggiunge 60 o più per la prima volta. Arriva una sola volta per ogni lead, una nuova valutazione non lo invia più. |
reply.received | Un lead ti invia un nuovo messaggio, in una campagna o no. Arriva una volta per ogni messaggio che ha meno di 6 ore. Quando arrivano molti messaggi del lead insieme, lo inviano solo i 5 più recenti. |
reaction.received | Un lead reagisce a uno dei tuoi messaggi con un'emoji. Arriva una volta per ogni emoji del lead su un messaggio. |
sam.suggestion.ready | Sam AI ha pronta una nuova risposta da inviare, in una conversazione in modalità draft. Arriva una volta per ogni suggerimento che ha meno di 6 ore. |
sam.reply.sent | Una risposta di Sam AI parte dal tuo account X, da sola in modalità auto o dopo che l'hai approvata nell'app. Arriva una volta per ogni risposta che ha meno di 6 ore. I messaggi che scrivi tu non lo inviano mai, li ritrovi con Recupera una conversazione. |
sam.handoff | Sam AI ti passa una conversazione quando non vede una buona risposta senza di te, quando il lead ha inviato solo una foto o un messaggio vocale, o quando la sua risposta non è riuscita a partire. In modalità auto lascia poi a te i messaggi successivi del lead. Arriva una volta per ogni passaggio che ha meno di 6 ore. |
L'oggetto evento
- idstringIdentificatore univoco dell'evento,
evt_e 24 caratteri. Presente anche nell'headerGemlead-Event-Id. - objectstringSempre
event. - typestringIl tipo di evento.
lead.createdlead.hotreply.receivedreaction.receivedsam.suggestion.readysam.reply.sentsam.handoff - created_atstringQuando è avvenuto l'evento, ISO 8601.
- account_idstringL'account Gemlead su cui è avvenuto.
- dataobjectIl contenuto dell'evento, che dipende dal suo tipo.
lead.created
Gemlead trova e valuta un nuovo lead, qualunque sia il punteggio. Arriva una sola volta per ogni lead, con il suo primo punteggio, e solo entro 3 giorni da quando è stato trovato, quindi una nuova valutazione di un lead più vecchio non lo invia mai. Un lead che ottiene subito 60 o più invia anche lead.hot.
Dati
- objectobjectIl lead, come lo restituisce Recupera 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
Il punteggio di un lead raggiunge 60 o più per la prima volta. Arriva una sola volta per ogni lead, una nuova valutazione non lo invia più.
Dati
- objectobjectIl lead, come lo restituisce Recupera 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 ti invia un nuovo messaggio, in una campagna o no. Arriva una volta per ogni messaggio che ha meno di 6 ore. Quando arrivano molti messaggi del lead insieme, lo inviano solo i 5 più recenti.
Dati
- leadobjectIl lead che ha scritto.
- messageobject
- campaignobject o nullLa campagna in cui si trova il lead, con
id,nameestatus. - conversation_idstringUsalo con Recupera una conversazione per avere tutto il thread.
{
"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 reagisce a uno dei tuoi messaggi con un'emoji. Arriva una volta per ogni emoji del lead su un messaggio.
{
"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 ha pronta una nuova risposta da inviare, in una conversazione in modalità draft. Arriva una volta per ogni suggerimento che ha meno di 6 ore.
Dati
- leadobjectIl lead a cui risponde Sam AI.
- conversation_idstringUsalo con Recupera il Sam AI di una conversazione per rileggere il suggerimento o cambiare la modalità.
- campaignobject o nullLa campagna in cui si trova il lead, con
id,nameestatus. - suggestionobjectLa risposta che Sam AI ha 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
Una risposta di Sam AI parte dal tuo account X, da sola in modalità auto o dopo che l'hai approvata nell'app. Arriva una volta per ogni risposta che ha meno di 6 ore. I messaggi che scrivi tu non lo inviano mai, li ritrovi con Recupera una conversazione.
Dati
- leadobjectIl lead a cui ha risposto Sam AI.
- conversation_idstringUsalo con Recupera una conversazione per avere tutto il thread.
- campaignobject o nullLa campagna in cui si trova il lead, con
id,nameestatus. - messagesarray di stringsI DM che Sam AI ha inviato, in ordine.
- sent_bystring
autoquando Sam AI l'ha inviata da solo,approvedquando l'hai approvata tu nell'app.autoapproved - replying_to_message_idstring o nullIl messaggio del lead a cui ha risposto.
- sent_atstringQuando è partita, 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 ti passa una conversazione quando non vede una buona risposta senza di te, quando il lead ha inviato solo una foto o un messaggio vocale, o quando la sua risposta non è riuscita a partire. In modalità auto lascia poi a te i messaggi successivi del lead. Arriva una volta per ogni passaggio che ha meno di 6 ore.
Dati
- leadobjectIl lead a cui rispondere.
- conversation_idstringUsalo con Recupera una conversazione per avere tutto il thread.
- campaignobject o nullLa campagna in cui si trova il lead, con
id,nameestatus. - reasonstring o nullPerché Sam AI te la passa, come l'ha spiegato.
{
"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."
}
}Consegna
Gemlead invia ogni evento come POST JSON e attende la tua risposta per 10 secondi. Qualsiasi 2xx conta come consegnato. Un redirect, qualsiasi altro stato, un timeout o un errore di connessione contano come fallimento, e l'evento viene inviato di nuovo.
| Header | Valore |
|---|---|
Gemlead-Signature | Il timestamp e la firma, t=1762444800,v1=5f2b... |
Gemlead-Event-Id | L'id dell'evento, per saltare gli eventi che hai già gestito. |
Gemlead-Event-Type | Il type dell'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=58ca5e964b92d617c979d3f067cdc2035a1a1a9d3f848d7d927c2db09b8e9baaVerifica le firme
Chiunque può inviare una richiesta al tuo URL. Verifica che ognuna arrivi da Gemlead prima di fidarti.
- Leggi il body grezzo come byte, prima di qualsiasi parsing JSON. Fare il parsing e riscriverlo cambia i byte.
- Dividi l'header
Gemlead-Signaturesulle virgole.tè un timestamp Unix in secondi,v1è la firma. - Calcola l'HMAC SHA256 di
t, un punto e il body grezzo, usando come chiave l'intero segreto dell'endpoint,whsec_compreso, e scrivilo in esadecimale. - Confrontalo con ogni
v1in tempo costante. - Rifiuta un
tche si discosta di più di 5 minuti dal tuo orologio, così nessuno può riprodurre una vecchia richiesta. Ogni nuovo tentativo è firmato con untnuovo.
Rispondi 400 quando la verifica fallisce. Conserva il segreto in una variabile d'ambiente come 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);Nuovi tentativi
Quando una consegna fallisce, Gemlead riprova 7 volte, ogni volta dopo un'attesa più lunga. Dopo l'8º tentativo, circa 45 ore dopo il primo, la consegna viene segnata come fallita.
| Tentativo | Inviato |
|---|---|
| 1 | Subito |
| 2 | 1 minuto dopo il fallimento precedente |
| 3 | 5 minuti dopo il fallimento precedente |
| 4 | 30 minuti dopo il fallimento precedente |
| 5 | 2 ore dopo il fallimento precedente |
| 6 | 6 ore dopo il fallimento precedente |
| 7 | 12 ore dopo il fallimento precedente |
| 8 | 24 ore dopo il fallimento precedente |
Un endpoint che fallisce per 3 giorni di fila, con almeno 8 tentativi falliti, viene disattivato. Il suo status diventa disabled, il suo disabled_reason diventa failing e le consegne ancora in attesa finiscono come fallite. Sistema il tuo server, poi riattivalo nell'app o con Aggiorna un endpoint webhook, che fa ripartire il conteggio. Gli eventi di test vengono inviati una sola volta e non vengono mai conteggiati.
Elenca le consegne mostra il codice di stato e l'errore di ogni tentativo, così vedi cosa ha risposto il tuo server.
Buone pratiche
- Rispondi 2xx entro 10 secondi, poi esegui il lavoro in una coda.
- Ogni evento viene consegnato almeno una volta. Usa l'
iddell'evento, presente anche inGemlead-Event-Id, per saltare un evento che hai già gestito. - Gli eventi possono arrivare in ordine sparso. Confronta
created_at, oppure recupera di nuovo il lead per avere il suo stato più recente. - Verifica ogni firma e rifiuta i timestamp vecchi.
- Quando rinnovi il segreto, viene sostituito all'istante. Fai subito il deploy di quello nuovo. Le consegne che falliscono nel frattempo vengono ritentate e firmate con il nuovo.
- Eventi e consegne vengono conservati 30 giorni.