BoardMark
เอกสาร

Integrations: webhook ไปแชตของทีมหรือระบบของคุณ

ส่งความเคลื่อนไหวของ project ไป Slack, Microsoft Teams, Google Chat, Discord หรือระบบของคุณ: วิธีตั้งค่า เหตุการณ์ การส่งซ้ำ ประวัติการส่ง และ JSON ที่มีลายเซ็น

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.releasedRelease ออกไม่เลือก

การแก้ครั้งเดียวอาจเป็นหลายเหตุการณ์ เช่น ย้ายสถานะพร้อมเปลี่ยนผู้รับผิดชอบ ส่งทั้งเปลี่ยนสถานะและเปลี่ยนผู้รับผิดชอบ การเปลี่ยนแปลงที่ทำผ่าน 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-Typeapplication/json
User-AgentBoardMark-Webhooks/1
BoardMark-Eventเหตุการณ์ เช่น issue.status_changed
BoardMark-Deliverywhd_… — เหมือนเดิมทุกครั้งที่ลองใหม่หรือส่งอีกครั้ง ใช้กันการประมวลผลซ้ำ
BoardMark-Signaturet=<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 ที่เคยประมวลผลแล้ว

Node.js (ไม่ต้องติดตั้งอะไรเพิ่ม)
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);
Python (standard library)
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 "", 204
Go (standard library)
package 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 ไม่ได้