HubSpot Webhook + AWS Bedrock AgentCore で実現する自動チャットサポートシステム
HubSpot の会話メッセージを Webhook で受信し、AWS Bedrock AgentCore で回答を生成して HubSpot にコメント投稿する自動サポートシステムを実験的に構築しました。

このシステムでは、HubSpot Webhook からのメッセージ受信、Bedrock AgentCore による回答生成、そして HubSpot への自動コメント投稿という一連の流れを実現しています。これによって顧客からの問い合わせに対して事前の調査、特に過去の対応履歴などをベースにしたものを自動化することを目指しています。
アーキテクチャ
技術スタックには HubSpot Webhook API、AWS SQS、AWS Bedrock AgentCore、HubSpot Conversations API、AWS Lambda を使用しました。
HubSpot Chat → Webhook → Lambda→ SQS → Lambda → Bedrock AgentCore & HubSpot API
Lambda を2つ用意して SQS を間に挟んでいるのは、AgentCore Runtimeのレスポンスが Webhook APIで求められるレスポンス時間を満たせないと判断したためです。 Webhook APIがタイムアウトしてしまうと処理は完了できませんが、対応履歴の検索とそれに基づいた回答生成という複雑なタスクを実現させるには Webhook API の求めるレスポンス時間内に処理が完了する可能性が非常に低くなります。よって SQS を間に挟み、Webhook API はあくまで HubSpot からのイベントを受け取るだけのコマンド的振る舞いに専念させました。
Webhook 処理
HubSpot からの Webhook を受信する Lambda では、まず署名検証を行います。HubSpot Webhook v1 方式の署名検証を実装しました。
function verifyHubSpotSignature(body: string, signature: string, clientSecret: string): boolean {
// 署名の前処理(sha256=プレフィックスを除去)
let cleanSignature = signature;
if (signature.startsWith('sha256=')) {
cleanSignature = signature.substring(7);
}
// HubSpot v1方式: client_secret + request_body の文字列に対してSHA256ハッシュを計算
const sourceString = clientSecret + body;
const expectedSignature = crypto.createHash('sha256').update(sourceString).digest('hex');
// 署名を比較(タイミング攻撃を防ぐため、crypto.timingSafeEqualを使用)
const signatureBuffer = Buffer.from(cleanSignature, 'hex');
const expectedSignatureBuffer = Buffer.from(expectedSignature, 'hex');
return crypto.timingSafeEqual(signatureBuffer, expectedSignatureBuffer);
}
次に、特定のチャンネルのみを処理対象とするフィルタリングを行います。これは HubSpot には営業がやり取りしているメールや別サービスからの問い合わせなども届く可能性があるために追加したものです。channelAccountIdを事前に調べておき、対象外かどうかを判定しましょう。
const message = messagesData.results[0];
if (!message.channelAccountId || !['9999'].includes(message.channelAccountId)) {
logger.info('このチャンネルはbotのサポート対象外です');
return null;
}
AgentCore に処理させるメッセージは SQS へ送信します。メッセージには会話 ID、プロンプト、メッセージ ID、タイムスタンプを含めました。
const messageBody = {
conversationId: msg.event.objectId,
prompt: msg.prompt,
messageId: msg.event.messageId,
timestamp: new Date().toISOString(),
};
const command = new SendMessageCommand({
QueueUrl: process.env.SQS_QUEUE_URL,
MessageBody: JSON.stringify(messageBody),
});
SQS 処理と AI 回答生成
実際に処理を行う側の Lambda では、SQS メッセージからプロンプトを取得して Bedrock AgentCore を呼び出します。
const messageBody = JSON.parse(record.body);
const conversationId = messageBody.conversationId;
const prompt = messageBody.prompt;
const command = new InvokeAgentRuntimeCommand({
agentRuntimeArn: process.env.BEDROCK_AGENT_RUNTIME_ARN!,
payload: Buffer.from(JSON.stringify({ prompt: prompt })),
});
const response = await client.send(command);
const bedrockResponse = await response.response.transformToString();
生成された回答は HubSpot にコメントとして投稿されます。コメントには「🤖 AI Assistant」という識別子を付けて、AI による回答であることを明示してみました。
async function postHubspotComment(conversationId: string, text: string): Promise<void> {
const hubspotApiKey = await getHubspotApiKey();
// テキストを整形(AI Assistantの識別子を追加)
const formattedText = `🤖 AI Assistant\n\n${text}`;
// リッチテキスト版(改行を<br>に変換)
const richText = formattedText.replace(/\n/g, '<br>');
const hubspotPayload = {
type: 'COMMENT',
text: formattedText,
richText: richText
};
const response = await fetch(
`https://api.hubapi.com/conversations/v3/conversations/threads/${conversationId}/messages`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${hubspotApiKey}`
},
body: JSON.stringify(hubspotPayload)
}
);
}
実験運用しての学び
結論から書くと、このエージェントシステムは採用することなくスタックを削除しました。これは実験するにつれ、HubSpot Webhookを利用したbotシステムという設計がベストプラクティスではないように感じたことが理由です。
HubSpot Webhookでメッセージを受け取る場合、チャットフローを利用したbot回答やシステム・社内からのメッセージも Webhook イベントとして送信されます。そのためイベントデータの前処理が複雑化しやすく、短いチャットのやり取りにおいては必要以上にエージェントがトリガーされたり、ユーザーが複数回の送信に分割してメッセージを送信してきた場合のハンドリングなどが困難でした。
また、利用した API の1部がベータ版提供状態だったこともあり、安定的な稼働への不安が強いこともありました。
まとめ
このようなWebhookを利用したチャットを実装する場合は、「チャットフロー内にあるWebhook連携機能」を使った設計にすることをお勧めします。なお、こちらはHubSpot のプランをアップグレードする必要がありますので、その点もご注意ください。
Discussion