Skip to main content

Main navigation

Start for freeLog in
Connect your toolsBotsDevelopersWebhook

Build a webhook bot server

Implement the request structure Tabple sends, signature verification, the 8-second synchronous reply, and callback-based asynchronous replies.

about 8 min Updated October 13, 2026

What you need

  • A server on a public address reachable over HTTPS — localhost and private IPs are blocked.
  • The secret you created when registering the bot — use the same value for signature verification.
  • A handler that answers within 8 seconds, or switches to the asynchronous callback flow when it needs longer — going past 8 seconds is a timeout failure, not an automatic switch.

Understanding the request

1

The request you receive

When a mention happens, Tabple POSTs a JSON body to the registered webhook URL. text is the user message without the mention, and channel.kind is project for a project channel or dm for a DM (in which case project_id is null).

POST <your-webhook-url>json
{
  "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"
}
2

Request headers

The values you need for signature verification come along as headers.

http
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.0
NOTEX-Tabple-Timestamp is in Unix seconds. Reject any request more than 5 minutes off your server clock — it may be a replay attack.

Signature verification (HMAC-SHA256)

1

How the signature works

The signed string is <X-Tabple-Timestamp value>.<raw request body>. Tabple computes HMAC-SHA256 over it with the secret you registered, prefixes the hex digest with v1=, and sends it in the X-Tabple-Signature header.

CAUTIONCompute the signature over the raw body exactly as received, not over a string you re-serialized after parsing the JSON. Different key order or whitespace makes verification fail.
2

Node.js verification example

The whole flow with Express. Use a raw parser to keep the original body intact.

server.mjsjavascript
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);

Synchronous reply — answer within 8 seconds

1

200 + reply

If you can answer within 8 seconds, return HTTP 200 with a reply string. It is posted as the bot's message as is.

HTTP 200 응답 본문json
{
  "reply": "이번 주 회의록 요약입니다. …",
  "attachments": [
    { "name": "summary.png", "url": "https://cdn.example.com/summary.png",
      "mime": "image/png", "size": 48211, "kind": "image" }
  ]
}
CAUTIONIf no reply arrives within 8 seconds, the call times out and a bot-response-failed message is posted to the chat.

Asynchronous reply — callback PUT

1

Acknowledge with 202 first

For slow work such as an LLM call, return HTTP 202 (or 200 with { "pending": true }) right away, then send the result to the callback_url that came with the request once the work finishes.

2

Send the callback PUT

Send a PUT request to callback_url. For authentication, put the callback_token that came with the request into the X-Tabple-Callback-Token header as is; the body uses the same format as a synchronous reply.

async-reply.mjsjavascript
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 }),
    });
  });
});
3

Callback lifetime

A callback token is valid for 5 minutes from the call, and for one use only. Sending after that, or with a token already used, returns 404.

NOTEcallback_url and callback_token can be null depending on server configuration. If they are null, use synchronous replies only.

Reply limits and troubleshooting

  • reply has the same 4,000-character limit as a regular message, and anything over that is truncated.
  • attachments holds up to 8 entries and url must be HTTPS. kind is image or file; any other value is treated as file.
  • If the bot server returns 4xx/5xx or takes longer than 8 seconds, a bot-response-failed message is posted to the chat — the beginning of the error body is shown with it, so use it for debugging.
  • A 200 response without reply is treated as an error. Return a short note even when there is nothing to answer.
  • If the registered webhook URL returns a redirect (3xx), Tabple doesn't follow it and treats it as an error — register the final address as the webhook URL.
  • For call limits per bot, caller, and project, see the Connect a chat bot guide.