Integrations: webhook ไปแชตของทีมหรือระบบของคุณ
ส่งความเคลื่อนไหวของ project ไป Slack, Microsoft Teams, Google Chat, Discord หรือระบบของคุณ: วิธีตั้งค่า เหตุการณ์ การส่งซ้ำ ประวัติการส่ง และ JSON ที่มีลายเซ็น
- Webhook ทำอะไรได้?
- เพิ่ม webhook อย่างไร?
- เอา Webhook URL ของ Slack อย่างไร?
- เอา Webhook URL ของ Microsoft Teams อย่างไร?
- เอา Webhook URL ของ Google Chat อย่างไร?
- เอา Webhook URL ของ Discord อย่างไร?
- ส่งเหตุการณ์อะไรได้บ้าง?
- ถ้าส่งไม่สำเร็จจะเป็นอย่างไร?
- payload แบบ JSON หน้าตาเป็นอย่างไร?
- ตรวจลายเซ็นอย่างไร?
- webhook ปลอดภัยอย่างไร?
Webhook ทำอะไรได้?
Webhook ส่งความเคลื่อนไหวของ project — งานใหม่ เปลี่ยนสถานะ เปลี่ยนผู้รับผิดชอบ ความคิดเห็นใหม่ และอื่น ๆ — ไปที่ช่องแชตของทีมหรือระบบของคุณเองทันทีที่เกิดขึ้น ใช้ได้ฟรีทุกแพ็กเกจ
- ตั้งที่ตั้งค่า project → Integrations เฉพาะผู้ดูแล project และผู้ดูแลบริษัท (สมาชิกคนอื่นไม่เห็นแท็บนี้)
- สูงสุด 5 webhook ต่อ project แต่ละตัวเลือกเหตุการณ์และภาษาของข้อความ (ไทยหรืออังกฤษ) เองได้
- Slack, Microsoft Teams, Google Chat และ Discord ได้ข้อความแชตสำเร็จรูป: ใครทำอะไร งานพร้อมสถานะ ผู้รับผิดชอบ และ priority และลิงก์เปิดงาน
- JSON (ระบบของคุณ) ได้ JSON ที่มีลายเซ็น ให้ระบบของคุณนำไปใช้ต่อ — ดูด้านล่าง
- ทำได้ในเว็บแอปเท่านั้น AI agent ดูหรือแก้ webhook ผ่าน MCP ไม่ได้
เพิ่ม webhook อย่างไร?
- เปิดบอร์ด → ฟันเฟือง (ตั้งค่า project) → Integrations → เพิ่ม webhook
- เลือกว่าจะส่งไปที่ไหน: Slack, Microsoft Teams, Google Chat, Discord หรือ JSON (ระบบของคุณ)
- ตั้งชื่อและวาง Webhook URL (วิธีเอา URL ของแต่ละบริการอยู่ด้านล่าง)
- ที่ “ส่งเมื่อ” เลือกเหตุการณ์ — สร้างงานใหม่ เปลี่ยนสถานะ เปลี่ยนผู้รับผิดชอบ และความคิดเห็นใหม่ ถูกเลือกไว้ตั้งแต่ต้น
- เลือกภาษาของข้อความ แล้วกด “บันทึกและส่งทดสอบ” — เห็นผลการทดสอบทันที
สำหรับ JSON ระบบแสดง signing secret (whsec_…) ครั้งเดียวหลังบันทึก — คัดลอกไปไว้ในระบบของคุณ “สร้าง signing secret ใหม่” ในเมนู ⋯ ออกรหัสใหม่และรหัสเดิมใช้ไม่ได้ทันที แต่ละ webhook มีสวิตช์เปิด/ปิด ปุ่มส่งทดสอบ ประวัติ แก้ไข และลบ
เอา Webhook URL ของ Slack อย่างไร?
- เปิด api.slack.com/apps → Create New App → From scratch แล้วเลือก workspace (หรือใช้แอป Slack ที่มีอยู่)
- Incoming Webhooks → เปิด Activate Incoming Webhooks → Add New Webhook → เลือกช่องที่จะส่งไป → Allow
- คัดลอก Webhook URL (ขึ้นต้นด้วย https://hooks.slack.com/services/) แล้ววางใน BoardMark
เอา Webhook URL ของ Microsoft Teams อย่างไร?
Teams รับ webhook ผ่านแอป Workflows (Power Automate) ส่วน Office 365 connector แบบเดิมกำลังถูกยกเลิก BoardMark ส่งเป็น Adaptive Card
- ใน Teams เปิดช่อง → ⋯ → Workflows
- เลือกเทมเพลต “Post to a channel when a webhook request is received” ตั้งชื่อ และเข้าสู่ระบบถ้าถูกถาม
- ตรวจทีมและช่อง แล้วกด Add workflow
- คัดลอก URL ที่ Teams แสดง แล้ววางใน BoardMark
ถ้าองค์กรของคุณจำกัดการใช้ Workflows ให้ติดต่อผู้ดูแล Microsoft 365
เอา Webhook URL ของ Google Chat อย่างไร?
- ใน Google Chat เปิด space → ชื่อ space ▾ → Apps & integrations
- Webhooks → Add webhook ตั้งชื่อ แล้วกด Save
- คัดลอก URL (ขึ้นต้นด้วย https://chat.googleapis.com/v1/spaces/) แล้ววางใน BoardMark
Webhook ของ Google Chat ใช้ได้กับบัญชี Google Workspace
เอา Webhook URL ของ Discord อย่างไร?
- ใน Discord เปิดตั้งค่าช่อง (Edit Channel) → Integrations → Webhooks → New Webhook
- ตั้งชื่อ ตรวจช่อง แล้วกด Copy Webhook URL (ขึ้นต้นด้วย https://discord.com/api/webhooks/)
- วาง URL ใน BoardMark — ต้องมีสิทธิ์ Manage Webhooks ใน server นั้น
ส่งเหตุการณ์อะไรได้บ้าง?
| เหตุการณ์ | ส่งเมื่อ | ตั้งแต่ต้น |
|---|---|---|
issue.created | สร้างงานใหม่ | เลือก |
issue.status_changed | เปลี่ยนสถานะ — งานย้ายไปสถานะอื่น รวมถึงการลากบนบอร์ด | เลือก |
issue.assigned | เปลี่ยนผู้รับผิดชอบ | เลือก |
comment.added | ความคิดเห็นใหม่ | เลือก |
issue.updated | แก้ไขอื่น ๆ — ชื่อ priority label วันครบกำหนด และ field อื่น (การเรียงลำดับไม่ส่ง) | ไม่เลือก |
issue.deleted | ลบงาน | ไม่เลือก |
sprint.started | เริ่ม sprint | ไม่เลือก |
sprint.closed | ปิด sprint | ไม่เลือก |
release.released | Release ออก | ไม่เลือก |
การแก้ครั้งเดียวอาจเป็นหลายเหตุการณ์ เช่น ย้ายสถานะพร้อมเปลี่ยนผู้รับผิดชอบ ส่งทั้งเปลี่ยนสถานะและเปลี่ยนผู้รับผิดชอบ การเปลี่ยนแปลงที่ทำผ่าน AI agent ก็ส่งเหมือนกัน
ถ้าส่งไม่สำเร็จจะเป็นอย่างไร?
- ถือว่าส่งสำเร็จเมื่อ URL ตอบ 2xx ภายใน 10 วินาที ไม่ตาม redirect (redirect ถือว่าไม่สำเร็จ)
- ส่งไม่สำเร็จลองใหม่สูงสุด 8 ครั้งในราว 1.5 ชั่วโมง
- webhook ที่ส่งไม่สำเร็จแสดงว่าล้มเหลวตั้งแต่เมื่อไหร่ ถ้ายังล้มเหลวครบ 3 วันจะปิดเองอัตโนมัติ และส่งอีเมลแจ้งผู้ดูแล project และผู้ดูแลบริษัท ตรวจ URL แล้วเปิดสวิตช์ใหม่ได้
- “ประวัติ” แสดงการส่งทุกครั้งใน 30 วันล่าสุด — เวลา เหตุการณ์ ผล (status code หรือข้อผิดพลาด) และจำนวนครั้งที่ลอง กด “ส่งอีกครั้ง” เพื่อส่งรายการนั้นซ้ำ (webhook ต้องเปิดอยู่)
- “ส่งทดสอบ” ส่งข้อความตัวอย่างแบบเปลี่ยนสถานะได้ทุกเมื่อ ไม่มีอะไรเปลี่ยนบนบอร์ด
payload แบบ JSON หน้าตาเป็นอย่างไร?
webhook แบบ JSON ได้ POST พร้อม header ด้านล่างและ body แบบตัวอย่าง (ข้อความตามภาษาของ webhook) ไม่มีอีเมลของใครใน payload — คนมีแค่ชื่อและ id
| Header | ค่า |
|---|---|
Content-Type | application/json |
User-Agent | BoardMark-Webhooks/1 |
BoardMark-Event | เหตุการณ์ เช่น issue.status_changed |
BoardMark-Delivery | whd_… — เหมือนเดิมทุกครั้งที่ลองใหม่หรือส่งอีกครั้ง ใช้กันการประมวลผลซ้ำ |
BoardMark-Signature | t=<unix วินาที>,v1=<hex ของ HMAC-SHA256 ของ "<t>.<body>" ด้วย secret ของคุณ> |
POST /boardmark HTTP/1.1
Content-Type: application/json
User-Agent: BoardMark-Webhooks/1
BoardMark-Event: issue.status_changed
BoardMark-Delivery: whd_01K6Z3P8Q4X7B2M5N6R0S1T2V4
BoardMark-Signature: t=1791183900,v1=5f2c9e…
{
"id": "evt_01K6Z3P8Q4X7B2M5N6R0S1T2V3",
"type": "issue.status_changed",
"occurred_at": "2026-10-05T14:05:00+07:00",
"project": { "key": "APP", "name": "Mobile App" },
"actor": { "id": "usr_01J8Z3K9Q4X7B2M5N6P8R0S1T2", "name": "Fern Suda", "via_agent": false },
"issue": {
"key": "APP-42",
"title": "Checkout shows the wrong price after a coupon",
"type": "bug",
"status": { "from": "in_progress", "to": "in_review" },
"assignee": { "id": "usr_01J8Z3K9Q4X7B2M5N6P8R0S1T2", "name": "Fern Suda" },
"priority": "high",
"url": "https://app.boardmark.ai/p/APP/i/APP-42"
},
"url": "https://app.boardmark.ai/p/APP/i/APP-42"
}- id — เหตุการณ์ใน BoardMark (evt_…) type — ชื่อเหตุการณ์ และ test: true เฉพาะตอนส่งทดสอบ
- project {key, name} และ actor {id, name, via_agent} — via_agent เป็น true เมื่อการเปลี่ยนแปลงมาจาก AI agent
- issue {key, title, type, status {from, to}, assignee, priority, url} สำหรับเหตุการณ์ของงานและความคิดเห็น — status เป็น status id ของ project (from มีเฉพาะ issue.status_changed)
- comment {id, excerpt} (ตัดที่ 300 ตัวอักษร) sprint {id, name, state} release {id, name} และ changed_fields สำหรับ issue.updated (เช่น priority, labels)
- url — หน้าที่ควรเปิดดูในเว็บแอป
- อาจมี key ใหม่เพิ่มในอนาคต ให้ข้าม key ที่ไม่รู้จัก
ตรวจลายเซ็นอย่างไร?
คำนวณ HMAC-SHA256 ของ t จุด (.) และ body ดิบ (ไบต์ที่ได้รับจริง ก่อน parse JSON) โดยใช้ secret ทั้งค่ารวม whsec_ เป็นคีย์ แล้วเทียบกับ v1 แบบ constant time ปฏิเสธ t ที่เก่ากว่า 5 นาที ตอบ 2xx ให้เร็ว แล้วค่อยทำงานที่ใช้เวลานาน และข้าม BoardMark-Delivery ที่เคยประมวลผลแล้ว
import { createHmac, timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";
const SECRET = process.env.BOARDMARK_WEBHOOK_SECRET; // the whole whsec_… value
// header = BoardMark-Signature, body = the raw request body (Buffer)
export function verify(header, body) {
const f = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
const age = Math.abs(Date.now() / 1000 - Number(f.t));
if (!/^\d+$/.test(f.t ?? "") || age > 300) return false;
const want = createHmac("sha256", SECRET).update(`${f.t}.`).update(body).digest();
const got = Buffer.from(f.v1 ?? "", "hex");
return got.length === want.length && timingSafeEqual(got, want);
}
createServer((req, res) => {
const chunks = [];
req.on("data", (c) => chunks.push(c));
req.on("end", () => {
const body = Buffer.concat(chunks);
if (!verify(req.headers["boardmark-signature"] ?? "", body)) return res.writeHead(401).end();
res.writeHead(204).end(); // answer first, then work
const event = JSON.parse(body.toString("utf8"));
// skip a BoardMark-Delivery id you have already handled (retries reuse it)
console.log(req.headers["boardmark-delivery"], event.type, event.issue?.key);
});
}).listen(8080);import hashlib, hmac, os, time
SECRET = os.environ["BOARDMARK_WEBHOOK_SECRET"].encode() # the whole whsec_… value
def verify(header: str, body: bytes) -> bool:
"""header = BoardMark-Signature, body = the raw request body."""
f = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
t = f.get("t", "")
if not t.isdigit() or abs(time.time() - int(t)) > 300:
return False
want = hmac.new(SECRET, t.encode() + b"." + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(want, f.get("v1", ""))
# Flask:
# @app.post("/boardmark")
# def boardmark():
# if not verify(request.headers.get("BoardMark-Signature", ""), request.get_data()):
# abort(401)
# event = request.get_json()
# # skip a BoardMark-Delivery id you have already handled (retries reuse it)
# return "", 204package boardmark
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"strconv"
"strings"
"time"
)
// Verify checks the BoardMark-Signature header against the raw body.
// secret is the whole whsec_… value.
func Verify(secret, header string, body []byte) bool {
var t, v1 string
for _, p := range strings.Split(header, ",") {
k, v, _ := strings.Cut(p, "=")
switch k {
case "t":
t = v
case "v1":
v1 = v
}
}
ts, err := strconv.ParseInt(t, 10, 64)
if err != nil {
return false
}
if age := time.Now().Unix() - ts; age > 300 || age < -300 {
return false
}
got, err := hex.DecodeString(v1)
if err != nil {
return false
}
m := hmac.New(sha256.New, []byte(secret))
m.Write([]byte(t + "."))
m.Write(body)
return hmac.Equal(got, m.Sum(nil))
}
func Handler(secret string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
if err != nil || !Verify(secret, r.Header.Get("BoardMark-Signature"), body) {
http.Error(w, "bad signature", http.StatusUnauthorized)
return
}
// skip a BoardMark-Delivery id you have already handled (retries reuse it)
w.WriteHeader(http.StatusNoContent)
}
}webhook ปลอดภัยอย่างไร?
- URL ต้องเป็น https:// พอร์ตมาตรฐาน และชี้ไปที่ที่อยู่สาธารณะ — IP ภายใน localhost และเครือข่ายส่วนตัวใช้ไม่ได้ ตรวจทั้งตอนบันทึกและทุกครั้งที่ส่ง
- ไม่ตาม redirect และรอแต่ละครั้งไม่เกิน 10 วินาที
- URL และ secret เก็บแบบเข้ารหัส หลังบันทึกหน้าจอแสดงแค่ชื่อโฮสต์และตัวท้ายของ URL
- URL ของ webhook แชตเปรียบเหมือนรหัสผ่าน ใครได้ไปก็โพสต์เข้าช่องของคุณได้ ถ้าหลุด ให้สร้าง URL ใหม่ในแอปแชต แล้วแก้ไข webhook ใน BoardMark
- payload มีแค่ชื่อและ id ไม่มีอีเมล และข้อความความคิดเห็นถูกตัดที่ 300 ตัวอักษร
- เฉพาะผู้ดูแล project และผู้ดูแลบริษัทดูหรือแก้ webhook ได้ AI agent ทำผ่าน MCP ไม่ได้