Вебхуки
Вебхуки
Gemlead отправляет HTTPS-запрос на ваш сервер в тот момент, когда лид найден, становится горячим, отвечает или реагирует на ваше сообщение, а также когда у Sam AI готов ответ, когда он отправляет ответ или передаёт вам диалог. Каждый запрос подписан, чтобы вы знали, что он от нас.
Настройка эндпоинта
Добавьте эндпоинт в приложении, в разделе API, или запросом Создание эндпоинта вебхука. Укажите публичный HTTPS URL и нужные типы событий и сохраните полученный secret, чтобы проверять подписи.
- Только HTTPS, без учётных данных в URL и без частных, loopback или link-local адресов.
- До 10 эндпоинтов на аккаунт.
filters.persona_idsограничивает события эндпоинта лидами выбранных персон, для событий любого типа.- Отправьте тестовое событие, чтобы проверить сервер до прихода настоящих событий.
Вебхуки входят в тарифы Business и Enterprise. Если аккаунт переходит на тариф Free или Starter, его эндпоинты сохраняются, но ничего не отправляют, пока он не вернётся на Business или 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"]
}
}'Типы событий
У всех событий одинаковая внешняя структура. Содержимое data зависит от type.
| Тип | Когда отправляется |
|---|---|
lead.created | Gemlead находит и оценивает нового лида, какой бы ни была оценка. Для каждого лида событие отправляется один раз, вместе с первой оценкой, и только в течение 3 дней после того, как лид найден, поэтому переоценка более старого лида его не вызывает. Если лид сразу получает 60 или выше, отправляется ещё и lead.hot. |
lead.hot | Оценка лида впервые достигает 60 или выше. Для каждого лида событие отправляется один раз, при переоценке повторно не отправляется. |
reply.received | Лид присылает вам новое сообщение, в кампании или вне её. Для каждого сообщения событие отправляется один раз, если сообщению меньше 6 часов. Если от лида приходит много сообщений сразу, событие отправляется только для 5 самых новых. |
reaction.received | Лид реагирует эмодзи на одно из ваших сообщений. Событие отправляется один раз для каждого эмодзи лида на сообщении. |
sam.suggestion.ready | У Sam AI готов новый ответ, который вам остаётся отправить, в диалоге в режиме draft. Для каждой подсказки событие отправляется один раз, если ей меньше 6 часов. |
sam.reply.sent | Ответ Sam AI уходит с вашего аккаунта X, сам в режиме auto или после того, как вы одобрите его в приложении. Для каждого ответа событие отправляется один раз, если ему меньше 6 часов. Сообщения, которые вы пишете сами, его никогда не вызывают, их возвращает Получение диалога. |
sam.handoff | Sam AI передаёт вам диалог, если не видит хорошего ответа без вашего участия, если лид прислал только фото или голосовое сообщение или если его ответ не удалось отправить. В режиме auto он после этого оставляет следующие сообщения лида вам. Для каждой передачи событие отправляется один раз, если ей меньше 6 часов. |
Объект события
- idstringУникальный идентификатор события,
evt_и 24 символа. Также передаётся в заголовкеGemlead-Event-Id. - objectstringВсегда
event. - typestringТип события.
lead.createdlead.hotreply.receivedreaction.receivedsam.suggestion.readysam.reply.sentsam.handoff - created_atstringВремя события, ISO 8601.
- account_idstringАккаунт Gemlead, в котором оно произошло.
- dataobjectСодержимое события, которое зависит от его типа.
lead.created
Gemlead находит и оценивает нового лида, какой бы ни была оценка. Для каждого лида событие отправляется один раз, вместе с первой оценкой, и только в течение 3 дней после того, как лид найден, поэтому переоценка более старого лида его не вызывает. Если лид сразу получает 60 или выше, отправляется ещё и lead.hot.
Данные
- objectobjectЛид в том виде, в каком его возвращает Получение лида.
{
"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
Оценка лида впервые достигает 60 или выше. Для каждого лида событие отправляется один раз, при переоценке повторно не отправляется.
Данные
- objectobjectЛид в том виде, в каком его возвращает Получение лида.
{
"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
Лид присылает вам новое сообщение, в кампании или вне её. Для каждого сообщения событие отправляется один раз, если сообщению меньше 6 часов. Если от лида приходит много сообщений сразу, событие отправляется только для 5 самых новых.
Данные
- leadobjectЛид, который написал.
- messageobject
- campaignobject или nullКампания, в которой находится лид, с её
id,nameиstatus. - conversation_idstringИспользуйте его в запросе Получение диалога, чтобы получить всю переписку.
{
"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
Лид реагирует эмодзи на одно из ваших сообщений. Событие отправляется один раз для каждого эмодзи лида на сообщении.
{
"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 готов новый ответ, который вам остаётся отправить, в диалоге в режиме draft. Для каждой подсказки событие отправляется один раз, если ей меньше 6 часов.
Данные
- leadobjectЛид, которому отвечает Sam AI.
- conversation_idstringИспользуйте его в запросе Получение Sam AI диалога, чтобы снова прочитать подсказку или сменить режим.
- campaignobject или nullКампания, в которой находится лид, с её
id,nameиstatus. - suggestionobjectГотовый ответ Sam AI.
{
"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
Ответ Sam AI уходит с вашего аккаунта X, сам в режиме auto или после того, как вы одобрите его в приложении. Для каждого ответа событие отправляется один раз, если ему меньше 6 часов. Сообщения, которые вы пишете сами, его никогда не вызывают, их возвращает Получение диалога.
Данные
- leadobjectЛид, которому ответил Sam AI.
- conversation_idstringИспользуйте его в запросе Получение диалога, чтобы получить всю переписку.
- campaignobject или nullКампания, в которой находится лид, с её
id,nameиstatus. - messagesмассив stringsDM, которые отправил Sam AI, по порядку.
- sent_bystring
auto, если Sam AI отправил его сам,approved, если вы одобрили его в приложении.autoapproved - replying_to_message_idstring или nullСообщение лида, на которое он ответил.
- sent_atstringВремя отправки, 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 передаёт вам диалог, если не видит хорошего ответа без вашего участия, если лид прислал только фото или голосовое сообщение или если его ответ не удалось отправить. В режиме auto он после этого оставляет следующие сообщения лида вам. Для каждой передачи событие отправляется один раз, если ей меньше 6 часов.
Данные
- leadobjectЛид, которому нужно ответить.
- conversation_idstringИспользуйте его в запросе Получение диалога, чтобы получить всю переписку.
- campaignobject или nullКампания, в которой находится лид, с её
id,nameиstatus. - reasonstring или nullПочему Sam AI передаёт диалог вам, в его собственной формулировке.
{
"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."
}
}Доставка
Gemlead отправляет каждое событие как JSON-запрос POST и ждёт вашего ответа 10 секунд. Любой ответ 2xx означает успешную доставку. Редирект, любой другой статус, тайм-аут или ошибка соединения считаются неудачей, и событие отправляется повторно.
| Заголовок | Значение |
|---|---|
Gemlead-Signature | Временная метка и подпись, t=1762444800,v1=5f2b... |
Gemlead-Event-Id | id события, чтобы пропускать уже обработанные события. |
Gemlead-Event-Type | type события. |
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=58ca5e964b92d617c979d3f067cdc2035a1a1a9d3f848d7d927c2db09b8e9baaПроверка подписи
Отправить запрос на ваш URL может кто угодно. Прежде чем доверять запросу, проверьте, что он пришёл от Gemlead.
- Прочитайте сырое тело в виде байтов, до любого разбора JSON. Разбор и повторная сериализация меняют байты.
- Разделите заголовок
Gemlead-Signatureпо запятым.tсодержит Unix-время в секундах,v1содержит подпись. - Вычислите HMAC SHA256 от
t, точки и сырого тела с полным секретом эндпоинта в качестве ключа, включаяwhsec_, и запишите результат в hex. - Сравните результат с каждым
v1, используя сравнение за постоянное время. - Отклоняйте запросы, у которых
tрасходится с вашими часами больше чем на 5 минут, чтобы старый запрос нельзя было воспроизвести. Каждая повторная отправка подписывается с новымt.
Если проверка не прошла, отвечайте 400. Храните секрет в переменной окружения, например 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);Повторные отправки
Если доставка не удалась, Gemlead пробует ещё 7 раз, каждый раз выжидая дольше. После 8-й попытки, примерно через 45 часов после первой, доставка помечается как неудачная.
| Попытка | Отправка |
|---|---|
| 1 | Сразу |
| 2 | Через 1 минуту после предыдущей неудачи |
| 3 | Через 5 минут после предыдущей неудачи |
| 4 | Через 30 минут после предыдущей неудачи |
| 5 | Через 2 часа после предыдущей неудачи |
| 6 | Через 6 часов после предыдущей неудачи |
| 7 | Через 12 часов после предыдущей неудачи |
| 8 | Через 24 часа после предыдущей неудачи |
Эндпоинт, который не отвечает успешно 3 дня подряд и набрал не меньше 8 неудачных попыток, отключается. Его status меняется на disabled, disabled_reason на failing, а ожидающие доставки завершаются неудачей. Исправьте сервер, затем снова включите эндпоинт в приложении или запросом Изменение эндпоинта вебхука, и отсчёт начнётся заново. Тестовые события отправляются один раз и не учитываются.
Список доставок показывает код статуса и ошибку каждой попытки, чтобы вы видели, что ответил ваш сервер.
Рекомендации
- Отвечайте 2xx в течение 10 секунд, а саму работу выполняйте в очереди.
- Доставка выполняется как минимум один раз. Используйте
idсобытия, который также есть вGemlead-Event-Id, чтобы пропускать уже обработанные события. - События могут приходить не по порядку. Сравнивайте
created_atили запрашивайте лида заново, чтобы получить его актуальное состояние. - Проверяйте каждую подпись и отклоняйте старые временные метки.
- Новый секрет сразу заменяет старый. Выкатите его сразу после обновления, а доставки, не прошедшие за это время, будут повторены с подписью новым секретом.
- События и доставки хранятся 30 дней.