Webhookボットサーバーを作成
Tabpleが送るリクエストの構造と署名の検証、8秒以内の同期応答とコールバックによる非同期応答を実装します。
準備するもの
- HTTPSでアクセスできるパブリックなアドレスのサーバー — localhost・プライベートIPはブロックされます。
- ボットの登録時に作成したSecret — 署名の検証に同じ値を使います。
- 8秒以内に応答するか、それ以上かかりそうな場合は自ら非同期コールバックに切り替えるハンドラー — 8秒を超えると、切り替えではなくタイムアウトによる失敗として処理されます。
リクエストの構造を理解する
受信するリクエスト
メンションが発生すると、Tabpleは登録されたWebhook URLにJSON本文をPOSTします。text はメンションを除いたユーザーのメッセージで、channel.kind はプロジェクトチャンネルなら project、DMなら dm(このとき project_id はnull)です。
{
"event": "message.created",
"invocation_id": 482,
"bot": { "id": 3, "name": "assistant" },
"channel": { "id": 17, "kind": "project", "project_id": 12 },
"invoker": { "id": 7, "name": "Geun" },
"parent_message_id": 90412,
"text": "이번 주 회의록 요약해줘",
"callback_url": "https://tabple.com/api/bots/3/invocations/482/callback",
"callback_token": "f3a9c1…(48자 hex)",
"timestamp": "2026-06-10T02:14:33.120Z"
}リクエストヘッダー
署名の検証に必要な値が、ヘッダーで一緒に渡されます。
Content-Type: application/json
X-Tabple-Bot-ID: 3
X-Tabple-Timestamp: 1781057673
X-Tabple-Signature: v1=2f8a41…(hex 64자)
User-Agent: Tabple-Bot/1.0X-Tabple-Timestamp はUnix秒単位です。サーバーの時刻と5分以上ずれているリクエストはリプレイ攻撃の可能性があるため、拒否してください。署名の検証(HMAC-SHA256)
署名の方式
署名の対象は <X-Tabple-Timestampの値>.<元のリクエスト本文> という文字列で、登録したSecretをキーとしてHMAC-SHA256を計算したhex値に v1= プレフィックスを付け、X-Tabple-Signature ヘッダーで送ります。
Node.jsでの検証例
Expressを前提とした全体の流れです。元の本文を保持するために、rawパーサーを使います。
import crypto from "node:crypto";
import express from "express";
const SECRET = process.env.TABPLE_BOT_SECRET;
const app = express();
function isValidSignature(rawBody, ts, signature) {
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected = "v1=" + crypto
.createHmac("sha256", SECRET)
.update(`${ts}.${rawBody}`)
.digest("hex");
const a = Buffer.from(signature ?? "");
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const rawBody = req.body.toString("utf8");
const ts = req.header("X-Tabple-Timestamp");
const signature = req.header("X-Tabple-Signature");
if (!isValidSignature(rawBody, ts, signature)) {
return res.status(401).json({ error: "invalid signature" });
}
const event = JSON.parse(rawBody);
res.json({ reply: `${event.invoker.name}님, "${event.text}" 잘 받았어요!` });
});
app.listen(3000);同期応答 — 8秒以内に返す
200 + reply
8秒以内に応答できる場合は、HTTP 200で reply 文字列を返してください。そのままボットのメッセージとして投稿されます。
{
"reply": "이번 주 회의록 요약입니다. …",
"attachments": [
{ "name": "summary.png", "url": "https://cdn.example.com/summary.png",
"mime": "image/png", "size": 48211, "kind": "image" }
]
}非同期応答 — コールバックPUT
まず202で受け付ける
LLMの呼び出しのように時間のかかる処理では、HTTP 202(または200 + { "pending": true })をすぐに返し、処理が終わったら、リクエストに含まれていた callback_url に結果を送ります。
コールバックPUTを送る
callback_url にPUTリクエストを送ってください。認証には、リクエストに含まれていた callback_token を X-Tabple-Callback-Token ヘッダーにそのまま入れます。本文は同期応答と同じ形式です。
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const rawBody = req.body.toString("utf8");
if (!isValidSignature(rawBody, req.header("X-Tabple-Timestamp"),
req.header("X-Tabple-Signature"))) {
return res.status(401).end();
}
const event = JSON.parse(rawBody);
res.status(202).end();
runLongTask(event.text).then(async (answer) => {
if (!event.callback_url) return;
await fetch(event.callback_url, {
method: "PUT",
headers: {
"Content-Type": "application/json",
"X-Tabple-Callback-Token": event.callback_token,
},
body: JSON.stringify({ reply: answer }),
});
});
});コールバックの有効期間
コールバックトークンは、呼び出し時点から5分間、1回だけ有効です。期限切れのトークンや、すでに使用されたトークンで送ると404が返されます。
callback_url と callback_token は、サーバーの設定によってはnullになる場合があります。nullの場合は、同期応答のみを使ってください。応答の制限とトラブルシューティング
replyにも通常のメッセージと同じ4,000文字の制限があり、超過分は切り捨てられます。attachmentsは最大8件で、urlはHTTPSのみ許可されます。kindはimageまたはfileで、それ以外の値はfileとして処理されます。- ボットサーバーが4xx/5xxを返すか8秒を超えると、チャットに「ボット応答失敗」のメッセージが投稿されます — エラー本文の冒頭も一緒に表示されるので、デバッグに活用してください。
replyのない200応答はエラーとして処理されます。返す内容がない場合も、短い案内文を返してください。- 登録したWebhook URLがリダイレクト(3xx)を返した場合は、追従せずにエラーとして扱います — 最終的なアドレスをWebhook URLとして直接登録してください。
- 呼び出しの上限(ボット・呼び出し元・プロジェクト別)については、「チャットボットの接続」ガイドを参照してください。