본문으로 건너뛰기

주요 메뉴

무료로 시작하기로그인
외부 도구 연결봇개발자Webhook

Webhook 봇 서버 만들기

Tabple가 보내는 요청의 구조와 서명 검증, 8초 동기 응답과 콜백 기반 비동기 응답을 구현합니다.

약 8분 2026년 10월 13일 업데이트

준비물

  • HTTPS로 접근 가능한 공인 주소의 서버 — localhost·사설 IP는 차단됩니다.
  • 봇 등록 시 만든 Secret — 서명 검증에 같은 값을 사용합니다.
  • 8초 안에 응답하거나 더 걸릴 것 같으면 스스로 비동기 콜백으로 전환하는 핸들러 — 8초를 넘기면 전환이 아니라 타임아웃 실패로 처리됩니다.

요청 구조 이해하기

1

수신하는 요청

멘션이 발생하면 Tabple가 등록된 Webhook URL로 JSON 본문을 POST합니다. text가 멘션을 제외한 사용자 메시지이고, channel.kind는 프로젝트 채널이면 project, DM이면 dm(이때 project_id는 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

요청 헤더

서명 검증에 필요한 값이 헤더로 함께 전달됩니다.

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
참고X-Tabple-Timestamp는 Unix 초 단위입니다. 서버 시각과 5분 넘게 차이 나는 요청은 재전송 공격일 수 있으니 거부하세요.

서명 검증 (HMAC-SHA256)

1

서명 방식

서명 대상은 <X-Tabple-Timestamp 값>.<원본 요청 본문> 문자열이고, 등록한 Secret을 키로 HMAC-SHA256을 계산한 hex 값에 v1= 접두어를 붙여 X-Tabple-Signature 헤더로 보냅니다.

주의JSON을 파싱한 뒤 다시 직렬화한 문자열이 아니라 수신한 원본 본문 그대로 서명을 계산해야 합니다. 키 순서나 공백이 달라지면 검증이 실패합니다.
2

Node.js 검증 예시

Express 기준 전체 흐름입니다. 원본 본문을 보존하기 위해 raw 파서를 사용합니다.

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);

동기 응답 — 8초 안에 답하기

1

200 + reply

8초 안에 답할 수 있으면 HTTP 200으로 reply 문자열을 돌려주세요. 그대로 봇 메시지로 게시됩니다.

HTTP 200 응답 본문json
{
  "reply": "이번 주 회의록 요약입니다. …",
  "attachments": [
    { "name": "summary.png", "url": "https://cdn.example.com/summary.png",
      "mime": "image/png", "size": 48211, "kind": "image" }
  ]
}
주의8초 안에 응답이 도착하지 않으면 호출이 타임아웃 처리되고 채팅에 '봇 응답 실패' 메시지가 게시됩니다.

비동기 응답 — 콜백 PUT

1

202로 먼저 접수

LLM 호출처럼 오래 걸리는 작업은 HTTP 202(또는 200 + { "pending": true })를 즉시 돌려주고, 작업이 끝난 뒤 요청에 담겨 온 callback_url로 결과를 보냅니다.

2

콜백 PUT 보내기

callback_url에 PUT 요청을 보내세요. 인증은 요청에 담겨 온 callback_token을 X-Tabple-Callback-Token 헤더에 그대로 넣습니다. 본문은 동기 응답과 같은 형식입니다.

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

콜백 유효 기간

콜백 토큰은 호출 시점부터 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로 직접 등록하세요.
  • 호출 한도(봇·호출자·프로젝트별)는 '채팅 봇 연결하기' 가이드를 참고하세요.