Webhook
Webhook
リードが見つかる、ホットになる、返信する、メッセージにリアクションする。Sam AIが返信を用意する、返信を送る、会話をあなたに引き継ぐ。その瞬間に、GemleadはあなたのサーバーへHTTPSリクエストを送ります。署名付きなので、Gemleadからの送信だと確認できます。
エンドポイントの設定
エンドポイントは、アプリのAPIから、またはWebhookエンドポイントを作成で追加します。公開HTTPSのURLと受け取りたいイベントタイプを指定し、署名の検証のために、返されるsecretを保存してください。
- HTTPSのみ対応です。URLに認証情報を含めることはできず、プライベートアドレス、ループバックアドレス、リンクローカルアドレスも使えません。
- 1アカウントあたり最大10個のエンドポイントを登録できます。
filters.persona_idsを使うと、エンドポイントが受け取るイベントを、イベントタイプに関係なく特定のペルソナのリードに絞り込めます。- 本番のイベントが届く前に、テストイベントを送信してサーバーの動作を確認しましょう。
Webhookは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が新しいリードを見つけてスコアリングしたとき。スコアに関係なく送信されます。リードごとに1回だけ、最初のスコアがついたときに送信され、見つけてから3日以内のリードに限られるため、古いリードを再スコアリングしても送信されません。最初のスコアが60以上なら、lead.hotも送信されます。 |
lead.hot | リードのスコアが初めて60以上になったとき。リードごとに1回だけ送信され、再スコアリングで再び送信されることはありません。 |
reply.received | キャンペーン中かどうかにかかわらず、リードから新しいメッセージが届いたとき。送信から6時間以内のメッセージについて、1件ごとに1回送信されます。リードのメッセージが一度に多数届いた場合は、最新の5件だけが送信されます。 |
reaction.received | リードがあなたのメッセージに絵文字でリアクションしたとき。1つのメッセージに付いたリードの絵文字ごとに1回送信されます。 |
sam.suggestion.ready | draftモードの会話で、Sam AIが送信用の新しい返信を用意したとき。作成から6時間以内の提案について、1件ごとに1回送信されます。 |
sam.reply.sent | Sam AIの返信があなたのXアカウントから送信されたとき。autoモードでは自動で、それ以外はアプリであなたが承認したときに送信されます。送信から6時間以内の返信について、1件ごとに1回送信されます。あなたが自分で書いたメッセージでは送信されず、会話を取得で確認できます。 |
sam.handoff | Sam AIが会話をあなたに引き継いだとき。あなたなしでは適切な返信ができないと判断した場合、リードが写真かボイスメッセージだけを送ってきた場合、返信を送信できなかった場合に起こります。autoモードでは、その後のリードのメッセージへの対応はあなたに任されます。発生から6時間以内の引き継ぎについて、1件ごとに1回送信されます。 |
イベントオブジェクト
- 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が新しいリードを見つけてスコアリングしたとき。スコアに関係なく送信されます。リードごとに1回だけ、最初のスコアがついたときに送信され、見つけてから3日以内のリードに限られるため、古いリードを再スコアリングしても送信されません。最初のスコアが60以上なら、lead.hotも送信されます。
{
"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以上になったとき。リードごとに1回だけ送信され、再スコアリングで再び送信されることはありません。
{
"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時間以内のメッセージについて、1件ごとに1回送信されます。リードのメッセージが一度に多数届いた場合は、最新の5件だけが送信されます。
{
"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
リードがあなたのメッセージに絵文字でリアクションしたとき。1つのメッセージに付いたリードの絵文字ごとに1回送信されます。
{
"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
draftモードの会話で、Sam AIが送信用の新しい返信を用意したとき。作成から6時間以内の提案について、1件ごとに1回送信されます。
データ
- leadobjectSam AIが返信する相手のリード。
- conversation_idstring会話のSam AIを取得で使うと、提案を読み直したり、モードを変更したりできます。
- campaignobjectまたはnullリードが参加しているキャンペーン。
id、name、statusを含みます。 - suggestionobjectSam 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時間以内の返信について、1件ごとに1回送信されます。あなたが自分で書いたメッセージでは送信されず、会話を取得で確認できます。
データ
- leadobjectSam AIが返信した相手のリード。
- conversation_idstringスレッド全体を取得するには、会話を取得で使います。
- campaignobjectまたはnullリードが参加しているキャンペーン。
id、name、statusを含みます。 - messagesstringsの配列Sam AIが送信したDM。送信順です。
- sent_bystringSam AIが自動で送信した場合は
auto、アプリであなたが承認した場合は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時間以内の引き継ぎについて、1件ごとに1回送信されます。
{
"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は署名です。t、ドット、生のボディをつなげた値のHMAC SHA256を、whsec_を含むエンドポイントのシークレット全体をキーにして計算し、16進数で表します。- その値を各
v1と、定数時間の比較で照合します。 - 古いリクエストを使い回されないよう、サーバーの時計と5分以上ずれた
tは拒否します。再試行のたびに、新しい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回再試行します。最初の試行から約45時間後の8回目でも失敗すると、その配信は失敗として記録されます。
| 試行 | 送信のタイミング |
|---|---|
| 1 | すぐに |
| 2 | 前回の失敗から1分後 |
| 3 | 前回の失敗から5分後 |
| 4 | 前回の失敗から30分後 |
| 5 | 前回の失敗から2時間後 |
| 6 | 前回の失敗から6時間後 |
| 7 | 前回の失敗から12時間後 |
| 8 | 前回の失敗から24時間後 |
3日連続で失敗し、失敗した試行が8回以上になったエンドポイントは無効になります。statusはdisabledに、disabled_reasonはfailingに変わり、待機中の配信はすべて失敗として終了します。サーバーを修正したら、アプリまたはWebhookエンドポイントを更新で再び有効にしてください。カウントはそこからやり直しになります。テストイベントは一度だけ送信され、このカウントには含まれません。
配信を一覧表示では、各試行のステータスコードとエラーを確認でき、サーバーが何を返したかがわかります。
ベストプラクティス
- 10秒以内に2xxを返し、実際の処理はキューで行ってください。
- 配信は少なくとも1回行われます。処理済みのイベントは、
Gemlead-Event-Idにも入っているイベントのidを使ってスキップしてください。 - イベントは順不同で届くことがあります。
created_atを比較するか、リードを取得し直して最新の状態を確認してください。 - すべての署名を検証し、古いタイムスタンプは拒否してください。
- シークレットを再発行すると、すぐに置き換わります。新しいシークレットはすぐにデプロイしてください。その間に失敗した配信は、新しいシークレットで署名されて再試行されます。
- イベントと配信は30日間保持されます。