BeeIn OA API
官方帳號開發者文件 — 一個 base URL,所有 OA 整合走這條。
#介紹
BeeIn 官方帳號(OA)讓你的服務能跟使用者透過 BeeIn 對話 — 推送通知、接收訊息、整合 AI 客服 / CRM /
排程系統都行。所有 OA 開發者面向的端點都在 https://webhook.beein.app/。
OA 整合分兩個方向:
- 主動:你呼叫 Bot API(
/bot/*)發訊息給追蹤者、推媒體、查資料 - 被動:使用者跟你的 OA 互動時(傳訊、追蹤、HereLink 到點)我們戳你預設的 webhook URL
#驗證
每個官方帳號發行的 Bot API token 形如 oa_<short>_<random>,永遠以 oa_ 開頭。
Token 在 OA 後台「開發者 → Bot Token」頁面建立,建立時可指定權限(如 send_message、
broadcast、read_followers)。
所有請求都帶上:
Authorization: Bearer oa_xxxxxx_xxxxxxxxxxxxxxxxxxxxxxxx
#快速開始 — 30 秒發第一封訊息
curl -X POST https://webhook.beein.app/bot/message \
-H "Authorization: Bearer oa_xxxxxx_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"recipientUserId": "69cecc429fc0e1d5abce2f24",
"type": "text",
"text": "Hello from BeeIn OA!"
}'
成功回:
{
"messageId": "69f558...",
"sentAt": "2026-05-02T14:00:00.000Z",
"delivered": true
}
#發送訊息給追蹤者
對「已追蹤你 OA 的使用者」單發一則訊息。沒追蹤過 OA 的使用者會被拒。
支援的 type
| type | 用途 | 必填欄位 |
|---|---|---|
text | 純文字 | text |
oa.text | 純文字(v2.7 spec 同義) | payload.text 或 text |
image | 圖片 | payload.mediaId |
video | 影片 | payload.mediaId |
audio | 音訊 | payload.mediaId |
file | 檔案 | payload.mediaId |
oa.card | 結構化卡片 | payload JSON |
sticker | 貼圖(v2.7.139) | packId, stickerId |
範例 — 文字
{
"recipientUserId": "69cecc...",
"type": "text",
"text": "訂單已出貨,預計明天送達"
}
範例 — 卡片
{
"recipientUserId": "69cecc...",
"type": "oa.card",
"payload": {
"title": "今日特餐",
"image": "https://your-cdn.com/menu.jpg",
"body": "宮保雞丁套餐 NT$180",
"actions": [
{ "label": "立即訂購", "url": "https://your-shop.com/order/123" }
]
}
}
#推送媒體(圖片 / 影片 / 音訊 / 檔案)
媒體推送是兩步:先拿一個一次性 upload token(5 分鐘有效),把 bytes 直接 PUT 到 worker 寫入 R2,
再用拿到的 mediaId 發訊息。BeeIn 不會把任何 R2 credential 給你 — 你拿到的是經過簽章的 token,
只能寫到我們指定的 key。
Step 1 — 拿 upload token
{
"mimeType": "image/png",
"size": 234567
}
回:
{
"uploadUrl": "https://webhook.beein.app/bot/media/upload",
"token": "eyJ0aWQ...U2YzM4ZA",
"tokenId": "69f5...",
"expiresAt": "2026-05-02T14:05:00.000Z",
"maxSize": 5242880,
"mimeType": "image/png"
}
Step 2 — PUT bytes
curl -X PUT https://webhook.beein.app/bot/media/upload \
-H "Authorization: Bearer eyJ0aWQ...U2YzM4ZA" \
-H "Content-Type: image/png" \
--data-binary @invoice.png
成功回:
{
"ok": true,
"mediaId": "69f5...",
"downloadUrl": "/api/v1/media/69f5...",
"expiresAt": "2026-05-16T14:00:00.000Z"
}
Step 3 — 用 mediaId 發訊息
{
"recipientUserId": "69cecc...",
"type": "image",
"payload": { "mediaId": "69f5..." }
}
限制
| 類型 | mimeType | 上限 |
|---|---|---|
| 圖片 | image/png image/jpeg image/webp image/gif | 5 MB |
| 音訊 | audio/mp4 audio/m4a audio/mpeg | 10 MB |
| 影片 | video/mp4 | 50 MB |
#廣播給所有追蹤者
對「全部追蹤者」一次發一則訊息。後端做 batch 投遞 + 速率控管。
{
"type": "text",
"text": "今晚 10pm 系統維護,預計 30 分鐘完成"
}
broadcast 權限(不要跟 send_message 混用),
鎖在團隊密碼管理器,少用且容易監控 — 配合 lastUsedAt 異常使用警報效果最好。
日常用的 token 只開 send_message,外流也不會被拿來發垃圾廣播。
#追蹤者列表
列出 / 查詢個別追蹤者。回傳僅含 OA 已被授權看到的欄位(displayName / avatarUrl / followedAt 等), 不包含 email / phone 等隱私資料。
#輸入中 / 挑選貼圖中… 指示器
告訴對方「對方正在輸入…」或「對方挑選貼圖中…」。送或不送,由開發者自己決定 — 回應快的機器人可以完全不送;走 LLM / 慢後端的機器人送這個指示器,使用者體感會好很多。
請求欄位
| field | 必填 | 說明 |
|---|---|---|
recipientUserId | ✓ | 追蹤者 userId(同 /bot/message) |
action | ✓ | "start" / "stop" |
intent | 預設 "text";設 "sticker_picker" 顯示「對方挑選貼圖中…」 |
範例 — 開始打字
{
"recipientUserId": "69cecc...",
"action": "start"
}
範例 — 開始挑貼圖
{
"recipientUserId": "69cecc...",
"action": "start",
"intent": "sticker_picker"
}
範例 — 停止
{
"recipientUserId": "69cecc...",
"action": "stop"
}
start 一次。
發訊息(含貼圖)本身就會自然結束指示器,所以「送完 sticker 後不用再送 stop」也可以。
客戶端收到的 WS 事件
{
"event": "oa:typing",
"data": {
"oaId": "...",
"oaName": "...",
"conversationId": "...",
"typing": true,
"intent": "text",
"senderType": "official_account",
"source": "bot",
"expiresAt": "2026-05-30T13:34:01.000Z"
}
}
#取得 OA 可用貼圖
列出 OA 可發送的所有貼圖包 + 包內貼圖。BeeIn 的貼圖屬於使用者(不屬於 OA 本身) — OA 透過 OA 擁有者 (owner) 的貼圖授權發送:
- OA owner 在他自己帳號裡安裝的貼圖包 (
UserSticker) - OA owner 自己創作的貼圖包 (
StickerPack.authorId) - 只列已發佈 (
status: "published") 的包;draft / removed 不列
回應
{
"owner": "<OA owner userId>",
"packs": [
{
"packId": "6a18...",
"shortName": "neko01",
"name": "貓貓貼圖",
"isPublic": true,
"status": "published",
"authorId": "...",
"stickers": [
{
"stickerId": "6a19...",
"imageUrl": "https://r2.beein.app/stickers/.../01.png",
"mimeType": "image/png",
"tags": ["happy"],
"order": 0
}
]
}
]
}
#發送貼圖
沿用 /bot/message 路徑,type 設 "sticker",
帶 packId + stickerId。imageUrl 由 server 自動填,
API 只處理 ID。
範例
{
"recipientUserId": "69cecc...",
"type": "sticker",
"packId": "6a18...",
"stickerId": "6a19..."
}
存取規則
跟 取得 OA 可用貼圖 一致 — 必須是 OA owner 安裝或創作的貼圖包,且包必須 status: "published"。
若 owner 沒這個包,發送會被擋下並回對應 error code。
錯誤碼
| HTTP | code | 情境 |
|---|---|---|
| 400 | STICKER_INVALID_INPUT | 沒帶 packId 或 stickerId |
| 400 | STICKER_INVALID_ID | 不是合法 ObjectId |
| 403 | STICKER_PACK_NOT_INSTALLED | OA owner 沒裝這個包 |
| 403 | STICKER_PACK_UNAVAILABLE | 包不是 published(draft / removed) |
| 404 | STICKER_PACK_NOT_FOUND | 找不到此貼圖包 |
| 404 | STICKER_NOT_FOUND | 該包內沒這個 sticker |
| 404 | OA_NO_OWNER | OA 沒設定 owner(資料異常) |
追蹤者收到的 message:new
{
"event": "message:new",
"data": {
"message": {
"id": "...",
"senderId": "<oaId>",
"type": "sticker",
"text": "[貼圖]",
"stickerImageUrl": "https://r2.../...png",
"stickerId": "...",
"packId": "..."
}
}
}
POST /bot/typing
帶 intent: "sticker_picker"。發出貼圖後該指示器會自然消失。
#接收事件 — Webhook 設定
當使用者跟你的 OA 互動(傳訊、追蹤、HereLink 到點觸發等),BeeIn 會主動 HTTP POST 你預設的 webhook URL。 這部分**從 OA 後台 → 開發者 → Webhook** 設定,不需要 API 呼叫。
必要條件
- HTTPS(不接受 http://、不接受 IP literal、不接受私網 hostname)
- Port 限 443 或 8443
- 儲存時必做 handshake:BeeIn 送
{"event":"webhook.test","challenge":"..."}, 你的 server 必須回{"challenge_response":"<HMAC-SHA256(secret, challenge)>"} - 之後每筆事件 timeout 10s,5xx 會 retry(5s → 30s → 2m → 10m → 30m,共 5 次)
Headers(每筆事件都會帶)
| Header | 說明 |
|---|---|
X-HereLink-Event-Id | 事件唯一 ID,retry 期間不變 — 用來去重 |
X-HereLink-Timestamp | Unix seconds |
X-HereLink-Signature | v1=<HMAC-SHA256(secret, timestamp + "." + body)> |
x-beein-event 等 | 舊版 header 也會帶(向後相容) |
回應
回 2xx 表示已收到。回應 body 可選擇帶上立即回覆:
{
"replies": [
{ "type": "text", "text": "收到,馬上請老師回覆" }
]
}
#事件清單
OA 在後台訂閱要接收的事件。建議只訂你真的會處理的,避免 retry 佔頻寬。
對話 — Conversation
| 事件 | 觸發 | 受人工接手影響 |
|---|---|---|
oa.user.message | 追蹤者傳訊息給 OA | 是 |
message.received | 同上(舊名,仍支援) | 是 |
message.callback | 追蹤者點 inline 按鈕 | 否 |
追蹤 / 成員
| 事件 | 觸發 |
|---|---|
oa.follow.changed | 新追蹤 / 取消追蹤(v2.7 統合事件,data.action = followed | unfollowed) |
follow.added / follow.removed | 各別事件(舊名,仍支援) |
member.added / member.removed | OA staff 進出 |
member.linked / member.unlinked | LIFF linked-site 綁定 / 解綁 |
HereLink(接送通知)
| 事件 | 觸發 | 受人工接手影響 |
|---|---|---|
herelink.arrival.triggered | 追蹤者到點,OA 收到通知 | 是 |
Event envelope
所有事件 body 都長這樣:
{
"eventId": "evt_01HX...",
"event": "oa.user.message",
"version": "2026-05-02",
"oaId": "69f4b404...",
"conversationId": "69f4b9e1...",
"timestamp": "2026-05-02T12:00:00.000Z",
"deliveryAttempt": 1,
"data": { /* 事件特定欄位 */ }
}
#簽章驗證
每筆 webhook 事件你都該驗章,否則任何人知道你 URL 都能偽造事件。
Node.js
const crypto = require('crypto');
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
const ts = req.headers['x-herelink-timestamp'];
const sig = (req.headers['x-herelink-signature'] || '').replace('v1=', '');
// 拒絕 5 分鐘外的時間戳(防 replay)
if (Math.abs(Date.now() / 1000 - ts) > 300) return res.sendStatus(400);
const expected = crypto.createHmac('sha256', MY_WEBHOOK_SECRET)
.update(`${ts}.${req.body.toString('utf8')}`)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) {
return res.sendStatus(401);
}
const body = JSON.parse(req.body);
// ... 用 eventId 去重後處理 ...
res.json({});
});
PHP
$ts = $_SERVER['HTTP_X_HERELINK_TIMESTAMP'] ?? '';
$sig = str_replace('v1=', '', $_SERVER['HTTP_X_HERELINK_SIGNATURE'] ?? '');
$body = file_get_contents('php://input');
if (abs(time() - $ts) > 300) http_response_code(400);
$expected = hash_hmac('sha256', "$ts.$body", $MY_WEBHOOK_SECRET);
if (!hash_equals($expected, $sig)) http_response_code(401);
timingSafeEqual / hash_equals 之類的 constant-time 比較,
不要用 === — 會被 timing attack 推出簽章。
#站台帳號連動(Linked Site)
讓你站台上的使用者可以聯絡上「不想公開 BeeIn 帳號」的站長 / 副站長 / 客服 / 業務, 同時讓那位被聯絡的人能看到「對方在我站台是誰」 — 而對方完全不知道自己連到了哪個 BeeIn 帳號。 隱私雙向保護:
- 對主動方(聯絡人):只看到「副站長 Mark」字樣,不會知道對方 BeeIn 是 @mark
- 對被加方(管理員):看到「@while = 論壇上的大白鯨(會員 5 年, 發文 200 篇)」,連動之後即使忘了也能查
角色分工
| 角色 | 誰 | 看得到什麼 |
|---|---|---|
| 站台 | 你的後端 (持 bsk_xxx) | 建 QR、接 webhook 通知 |
| 主動方(scanner) | 掃 QR 的論壇用戶 / 客戶 | 「我加了副站長 Mark 為好友」(看不到 @mark) |
| 被加方(target) | 站長 / 副站長 / 客服(BeeIn 帳號) | 「@while = 論壇大白鯨」 — 對方在站台的真實身分 |
典型場景
- 論壇 / 社群:站長不想公開 BeeIn @id;用戶看到「聯絡副站長」按鈕點下去就能加上
- 客服 / 商家:客戶想找客服老闆;客服在 BeeIn 端看得到「VIP, 訂單 5 筆」站台資訊
- 學校 / 機構:家長想聯絡老師;老師在 BeeIn 看得到「某某學生家長」
- 供應商系統:供應商想找採購;採購看得到「合作 Tier-2, 交付 12 案」
整體流程(以論壇為例)
場景:XXX 討論區 — 站長 Monkey、副站長 Mark / Andy;論壇用戶大白鯨想找副站長 Mark。
- 大白鯨在論壇上按「聯絡副站長 Mark」
- 論壇後端 POST
/linked-sites/qr:targetBeeInId=@mark(被加方 — Mark 的 BeeIn ID,論壇後端記在 DB,前端絕不顯示)siteUserInfoUrl= 大白鯨在論壇的 profile URL(給 Mark 之後查身份用)siteUserId/siteUserName= 大白鯨在論壇的 ID / 帳號displayName= 「大白鯨(會員 5 年)」siteName= 「XXX 討論區」
- 論壇拿到
qrContent+webUrl,顯示給大白鯨「請用 BeeIn 掃描 / 點此打開 BeeIn」 - 大白鯨用 BeeIn app 開啟連結 → scanner =
@while - BeeIn UI 顯示:「你正在加 XXX 討論區的副站長 Mark 為好友」(沒有 @mark 字樣)→ 大白鯨按確認
- BeeIn server 建立連動:把
@while跟 XXX 討論區的whale123(QR 帶的userName)綁起來,寫進 Mark 端的「連動列表」。不會主動 GET 你的站台 — 所有 member 細節都等被加方點卡片時才用 WebView 載。 - Mark 的 BeeIn app 收到通知卡:「
@while連動到你 — 來自 XXX 討論區(大白鯨)」(卡片上只顯示建 QR 時帶的displayName/inviteName,沒有 member 詳細欄位)。Mark 點卡片 → BeeIn 用 WebView 開siteUserInfoUrl?username=whale123&beeinToken=…→ 你的站台回 HTML 頁面,秀「會員 5 年, 發文 200 篇」等內容。 - BeeIn 推
member.linkedwebhook 給論壇後端:「@mark 與大白鯨已連動」 - 之後雙方在 BeeIn 一般訊息聊天 — Mark 即使忘了
@while是誰,去「連動列表」一查就知道是論壇大白鯨
取得 Site API Key(bsk_xxx)
請在 BeeIn App 內申請 Site API Key — 一張獨立的長期金鑰,跟
OA Bot Token(oa_)完全分開。路徑:開啟 BeeIn App → 設定 →
開發者 / Linked Site → 新增站台金鑰,填入站名 / 用途 /
預估 MAU 等基本資訊,送出後即可取得 bsk_xxx(只顯示一次,請妥善
保存)。
oa_xxx 用於官方帳號 Bot API(/bot/*);bsk_xxx 用於站台連動
API(/linked-sites/*)。混用會收 401 INVALID_API_KEY。
#建立連動 QR
建一張一次性 QR,預設 1 小時有效,被掃過 / 被同意綁定就立即失效。
bsk_xxx 那一刻已經設好,**每次建 QR 不用再帶**。Server 一律從 key 拿,body 帶了也會被忽略。
Body(v2.7.96 最精簡)
| 欄位 | 必填 | 說明 |
|---|---|---|
targetBeeInId | 是 | 被加方(管理員 / 客服)的 BeeIn ID 或 @username |
userName | 建議 | 主動方在你站台的唯一識別字串(帳號 / ID,你決定);BeeIn 用這個 key 反查、改名、未來打 member 頁面都靠它。 |
displayName | 否 | 連動後被加方在 BeeIn 連動卡片上看到的名字(例:「Alice (VIP #001)」)。沒帶就用 bsk_xxx 預設。 |
inviteName | 否 | QR 公開 landing page(https://beein.app/link/<id>)上面顯示的名字 — 「XXX 的 <inviteName> 邀請您使用 BeeIn」。和 displayName 分離:對外可用親切稱呼(「Alice 老闆」),對內卡片用正式 ID(「Alice (VIP #001)」)。沒帶就 fallback 到 displayName。 |
expiresIn | 否 | QR 有效秒數,60-86400,預設 3600 |
舊欄位 siteUserId / siteUserName 仍然接受,等同 userName / displayName。siteUserInfoUrl / siteName / webhookUrl 仍然接受但直接忽略(避免舊 client 收 400)。
範例
curl -X POST https://webhook.beein.app/linked-sites/qr \
-H "Authorization: Bearer bsk_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"targetBeeInId": "@mark",
"userName": "whale123",
"displayName": "大白鯨 (會員 5 年, 發文 200 篇)",
"inviteName": "大白鯨",
"expiresIn": 1800
}'
欄位對照:targetBeeInId = 被加方(管理員)的 BeeIn @username · userName = 主動方在你站台的 unique ID · displayName = 連動後卡片顯示名 · inviteName = QR 公開 landing page 顯示名 · expiresIn = QR 有效秒數
注意:範例為了講解可能會看到 // 註解,但實際 POST 時 body 不能含註解 — JSON 規格不支援,送出去會被 400 BAD_JSON 退回。
回應:
{
"qrCode": {
"_id": "69f5...", // ← linkId 雛形,App 內部用,不顯示給人
"qrContent": "beein://link/69f5...", // App 掃這個 / 自己畫 QR
"webUrl": "https://beein.app/link/69f5...", // 手機點這個(著陸頁)
"qrImageUrl": "https://webhook.beein.app/linked-sites/qr/69f5.../image", // v2.7.99 ← 直接 <img src> 用
"qrImageDataUri": "data:image/png;base64,iVBORw0K...", // v2.7.99 ← inline 預渲染好的 PNG
"expiresAt": "2026-05-02T15:00:00.000Z",
"status": "pending"
}
}
直接拿圖用(v2.7.99)
不想自己跑 QR library?Server 已經幫你畫好 BeeIn 品牌 QR(中央 BeeIn icon、白色圓角底盤、H-level 容錯),兩種拿法:
qrImageDataUri— base64 inline PNG。適合「建好之後直接渲染」場景,response 一拿到就能塞<img src="...">,不會有額外 HTTP request。約 10–15 KB。qrImageUrl— 公開 GET endpoint,回傳一樣的 PNG。適合「QR 出現在第三方頁面、想吃 browser cache」場景。不需要 auth — payload 本來就公開(著陸頁webUrl顯示的也是同一張 QR)。掃過 / 過期後 image 仍可取(只是掃了會被 server 端拒收),所以 cache 5 分鐘是安全的。
<!-- 第三方站台一條 tag 把 BeeIn 品牌 QR 嵌進去 -->
<img src="https://webhook.beein.app/linked-sites/qr/69f5.../image"
alt="Scan to add Moderator Mark on BeeIn"
width="240" height="240" />
qrContent(beein://link/...)永遠回。我們的 image 只是省事用,沒有強制換掉你既有的 QR 渲染流程。
Server 自動串 ?username=(v2.7.96)
bsk_xxx 的 siteUserInfoUrl 設成 base URL 即可,每次 BeeIn 開啟某個 user 的頁面時自動 append:
| 你的 base URL | BeeIn 實際呼叫 |
|---|---|
https://your-site.com/user-info.php | https://your-site.com/user-info.php?username=alice123 |
https://your-site.com/user-info.php?lang=zh | https://your-site.com/user-info.php?lang=zh&username=alice123 |
用 Node URL API 處理,自動分辨 ? / &、自動 URL-encode 特殊字元。
⚠️ siteUserInfoUrl 必須回 HTML,不是 JSON
被加方點開 BeeIn 上的連動站台卡片 → BeeIn App 用 WebView 直接開 siteUserInfoUrl?username=…&viewer=…&beeinToken=… 給人類看。所以你必須在這個 URL 回 HTML(專屬呈現該 user 內容的頁面),不是 JSON。三個 query:
username— 建 QR 時你帶的userName(你站台 user 的 unique ID),用來反查要顯示哪個 user 的內容。viewer— 正在開這個 WebView 的 BeeIn 使用者的@username(例:mark)。給你做 access log / 顯示「您好,@mark」之類用。v2.7.99 新增。beeinToken— 簽好的 short-lived JWT,證明這個 request 來自 BeeIn WebView 而不是有人偽造你的 URL。強烈建議驗。
<!doctype html>
<html lang="zh-TW">
<head>
<meta charset="utf-8">
<title>Alice 的會員資料</title>
<style>body{font:15px/1.5 -apple-system,sans-serif;padding:24px;}</style>
</head>
<body>
<h1>Alice ★</h1>
<p>會員等級:VIP</p>
<p>發文數:1,234 篇</p>
<p>加入時間:2024-03-15</p>
<!-- 你想擺什麼都行,完全你站台的設計 -->
</body>
</html>
驗 beeinToken JWT(v2.7.99)
用什麼當 HMAC secret:如果你的 bsk_xxx 沒設 webhookSecret(大部分人都沒設),
BeeIn 會 fallback 用 sha256(bsk_xxx) 當 secret 來簽。你站台這邊 1 行 code 算出同樣的 secret 就能驗:
// PHP
$secret = hash('sha256', $BSK_XXX); // 或用 $linkSite->webhookSecret 如果你有設
$decoded = \Firebase\JWT\JWT::decode(
$_GET['beeinToken'],
new \Firebase\JWT\Key($secret, 'HS256')
);
// $decoded->viewerUserId = BeeIn user _id
// $decoded->viewerUsername = @username (= $_GET['viewer'])
// $decoded->linkedSiteId = 連動關係 _id
// $decoded->scannerId = 主動方 BeeIn user _id
// $decoded->targetBeeInId = 被加方 BeeIn user _id
// Node.js
const crypto = require('crypto');
const jwt = require('jsonwebtoken');
const secret = crypto.createHash('sha256').update(BSK_XXX).digest('hex');
const payload = jwt.verify(req.query.beeinToken, secret, { algorithms: ['HS256'] });
?viewer= query。
但若 user 在 WebView 內會做任何「以 BeeIn 帳號身份」的動作 (下單、修改設定等),請務必驗
— 不然有人猜到你的 URL 就能假冒任何 BeeIn 使用者。
{success, member: {...}} JSON,只有 bsk_xxx 訂閱 supportedActions: ['member.query'] 才會啟用。目前 fleet 0 用戶在用。**你照新的 HTML 規格做就對了**。
@mark — BeeIn app 顯示對方時只會用站台給的 siteName「XXX 討論區的副站長 Mark」當作初次卡片標題;連動成功後雙方在 BeeIn 訊息對話時,主動方那端看到的就是 對方在 BeeIn 上自己設的身分資訊(displayName / 頭像 / 簽名)。被加方則完整看到對方 BeeIn @username + 站台 member 資訊。
#改連動會員顯示名(v2.7.96)
當站台使用者在你站上改名時,呼叫這支同步更新 BeeIn 端連動卡片顯示名 —— 不用知道 BeeIn 內部的 linkId,用你站上的 userName(就是建 QR 時帶的那個 unique ID)當 key:
curl -X PATCH https://webhook.beein.app/linked-sites/member \
-H "Authorization: Bearer bsk_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"userName": "alice123", // 改名前後不變的 unique ID
"displayName": "Alice ★ (VIP)" // 新顯示名
}'
回應:
{ "success": true, "updated": 1 }
錯誤碼
| HTTP | code | 意義 |
|---|---|---|
| 401 | SITE_AUTH_INVALID | bsk_xxx 無效或被停用 |
| 404 | LINKED_MEMBER_NOT_FOUND | 這把 key 名下沒有 userName 對應的 active link |
| 400 | (zod validation) | body 缺欄位或型別錯 |
成功後 server 自動清掉相關連動的 member-info cache,下次 BeeIn UI refresh 就會看到新名字。
#連動站台 Webhook 簽章(v2.7.96)
BeeIn 主動 POST 你的 webhookUrl(建 bsk_xxx 時設的那個)時,會像 OA webhook 一樣
帶 HMAC-SHA256 簽章 header。驗章方式跟 OA webhook
完全一樣 — 直接看 #簽章驗證 的 Node.js / PHP 範例即可,
差別只在 secret 換成 bsk_xxx 的 webhookSecret。
會帶簽章的事件
| 事件 | 觸發 |
|---|---|
member.linked | 主動方掃 QR 完成 → BeeIn 通知你「連動成立」 |
link.created | 同上,舊事件名(向後相容,payload 一致) |
member.unlinked | 任一方解除連動 → BeeIn 通知你(目前 product scope 暫關) |
member.report / member.action | BeeIn 端 OA admin 觸發站台動作回呼(目前 product scope 暫關) |
Headers
每筆事件都會帶兩組 header(新舊並行,你選一組驗即可):
| Header | 內容 |
|---|---|
X-HereLink-Event-Id | 事件 UUID,retry 期間不變(去重 key) |
X-HereLink-Timestamp | Unix 秒數 |
X-HereLink-Signature | v1=<HMAC-SHA256(webhookSecret, timestamp + "." + body)> |
x-beein-event | 事件名(舊版 header set) |
x-beein-signature | sha256=<HMAC-SHA256(webhookSecret, body)> (舊版 — 只簽 body,不含 timestamp) |
x-beein-timestamp | Unix 秒數 |
x-beein-delivery | delivery UUID(同 eventId) |
member.linked body 範例
{
"event": "member.linked",
"qrCodeId": "69f5...", // 你建的 QR _id
"scannerId": "69ab...", // 主動方 BeeIn userId
"targetBeeInId": "69cd...", // 被加方 BeeIn userId
"linkedSiteId": "69ef...", // BeeIn 端 LinkedSite 紀錄 _id
"externalUserId": "whale123" // 建 QR 時帶的 userName(你站台的 unique ID)
}
webhookSecret → 收到的 webhook 沒簽章。
建 bsk_xxx 時 webhookSecret 是選填,沒設的話 BeeIn 會 fire 出去 UNSIGNED
(沒任何 X-HereLink-Signature / x-beein-signature header) — 你那邊驗章程式就會擋掉。
要啟用簽章,把 webhookSecret(隨機 32 byte hex 之類)填進 bsk_xxx 設定即可,
BeeIn 內部會自動切到簽章路徑,不用改任何別的設定。
MY_WEBHOOK_SECRET 換成你 bsk_xxx 裡填的那把
webhookSecret。務必 timing-safe 比對(crypto.timingSafeEqual /
hash_equals),不要用 ===。
beeinToken 用同一把 secret。
被加方點連動卡片 → BeeIn 用 WebView 開 siteUserInfoUrl?username=…&beeinToken=…;
beeinToken 是 JWT,演算法也是 HS256,secret 一樣是 bsk_xxx 的 webhookSecret —
所以同一份 secret 既能驗 webhook 也能驗 WebView token。
#用 BeeIn 登入 — 簡介
讓追蹤你官號 (OA) 的使用者用 BeeIn App 掃 QR 登入你的網站。共 4 步:OA 設定 → 放登入按鈕 → 處理 callback → 用 authCode 換 user。同意畫面內建,scope (email / avatar / phone) 由使用者勾選後 server 才會送對應欄位。
Base URL: https://webhook.beein.app · 設定 PATCH: https://api.beein.app/api/v1/oa/<OA_ID>/login-config
#Step 1 — OA 設定 (一次)
OA 後台到 Bot API Token 設定相關區域,新增 Token 時必須選取 manage_login_config 權限。
curl -X PATCH https://webhook.beein.app/bot/login-config \
-H "Authorization: Bearer oa_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"enabled": true,
"notifyURL": "https://your-site.example/auth/beein/notify",
"successURL": "https://your-site.example/auth/beein/callback",
"cancelURL": "https://your-site.example/auth/beein/cancel",
"allowedRedirectHosts": ["your-site.example"],
"allowedFrameAncestors": ["your-site.example"],
"requestedScopes": ["email"]
}'
Response 回傳目前設定。沒有 webhookSecret — notify body 是 server 發出的非機密資料(不含信用卡之類的東西),不需要簽章。真正驗證使用者身分的關卡是 Step 4 /exchange,那邊需要 Bot API token 才能換到使用者資料,所以即使有人亂猜 / 攔截到 notify payload 或 code,沒有你 OA 的 Bot API token 也換不出任何使用者身分。
| 欄位 | 說明 |
|---|---|
notifyURL | 請輸入於 Webhook 已經綁定的網址,此為必要欄位,沒有輸入無法取得單次令牌 (authCode) 取得用戶資料。 |
successURL | 瀏覽器轉向 — 同意後使用者帶 ?code=…&state=… 回到這。啟用時必填。 |
cancelURL | 瀏覽器轉向 — 取消或逾時帶 ?reason=cancelled|expired&state=…。啟用時必填。 |
allowedRedirectHosts | 白名單:successURL/cancelURL 的 host 必須在這 (防 open redirect)。子網域繼承。 |
allowedFrameAncestors | iframe 嵌入白名單。 |
requestedScopes | ["email","avatar","phone"] 子集。空陣列 = 只有 BeeIn userId + displayName。 |
讀目前設定:GET https://webhook.beein.app/bot/login-config(同樣的 token 認證)。
#Step 3 — 處理 callback
成功 callback
https://your-site.example/auth/beein/callback?authCode=ABC123XYZ&code=ABC123XYZ&state=RANDOM_NONCE
URL 同時帶 authCode 跟 code 兩個 query param,值一樣 — 建議讀 authCode(語意比較清楚);code 是 legacy 別名為了相容舊整合,任何一個都行。
- 驗證
state跟 Step 2 存下來的一樣 (CSRF)。不一樣 → 拒絕。 - 拿
authCode去 Step 4 換 user 身分。一次性,60 秒過期。
取消 callback
https://your-site.example/auth/beein/cancel?reason=cancelled|expired&state=RANDOM_NONCE
reason:cancelled (使用者按取消) 或 expired (3 分鐘沒掃 / 沒同意)。
#Step 4 — code 換 user 身分
必須帶 Bot API token(就是 Step 1 那把 oa_xxx,同一個 manage_login_config 權限)。server 會檢查 authCode 所屬的 OA 跟你的 token 所屬的 OA 是否一致 — 不一致一律回 404,所以即使有人攔截到 code 也換不出資料(他沒你的 token)。
curl -X POST https://webhook.beein.app/oa-login/exchange \
-H "Authorization: Bearer oa_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"authCode": "ABC123XYZ"}'
Response:
{
"userId": "69cfe4500c5ec85a8402693f",
"displayName": "黑貓電子科技",
"username": "mchotdog",
"avatarUrl": "https://media.beein.app/avatars/user/…webp", ← 只有 scope 'avatar' 才有
"email": "[email protected]", ← 只有 scope 'email' 才有
"phone": null, ← 只有 scope 'phone' 才有
"oaId": "69ee34c62900407b94948fa9",
"grantedScopes": ["email", "avatar"],
"approvedAt": "2026-06-08T07:00:00.000Z"
}
v2.7.94 起,回傳是扁平結構:userId / displayName / username 直接在 top-level,不再 nested 在 user{} 下。
userId 當帳號主鍵,而不是 email。userId 穩定且永不變;email 在使用者沒同意 email scope 時會缺少。authCode 一次性,第二次呼叫 404 OA_LOGIN_CODE_INVALID。
#(選) Notify webhook
除了 redirect callback,server 還會直接 POST 你的 notifyURL 通知你 — server-to-server,即使瀏覽器掉線也收得到。沒有簽章 header、沒有 HMAC、沒有 secret 要管。理由:body 內的資料是 server 發出的非敏感資訊,真正驗證身分的關卡在 /exchange(那裡才會檢查你的 Bot API token)。即使有人攔截到這個 POST 或亂猜你的 notifyURL 灌假資料,他沒你的 Bot API token 也換不出任何使用者身分。
POST <your notifyURL>
Content-Type: application/json
Body:
{
"event": "oa_login.approved",
"oaId": "69ee34c62900407b94948fa9",
"sessionToken": "…",
"state": "<Step 2 帶的 state>",
"user": {
"userId": "69cfe4500c5ec85a8402693f",
"displayName": "黑貓電子科技",
"avatarUrl": "https://…" ← scope 'avatar' 才有
},
"grantedScopes": ["email"],
"email": "[email protected]", ← scope 'email' 才有
"authCode": "ABC123XYZ", ← 拿這個去 /exchange 才是權威身分
"issuedAt": "2026-06-08T07:00:00Z",
"approvedAt": "2026-06-08T07:00:05Z"
}
authCode 立刻去 /exchange 換正式身分(用你的 Bot API token)。不要直接信任 body 裡的 user / email 欄位 — 那些是給你快速 prefetch 之用,但任何敏感操作前都應該透過 /exchange 確認。單次嘗試,失敗不自動 retry — OA 仍可用 redirect callback 的
?code=… 補。
#Scopes 規則
| Scope | 含意 | 未授權時 |
|---|---|---|
email | 分享 BeeIn 註冊 email | payload.email 不存在 |
avatar | 分享頭像 URL (媒體 CDN, 公開) | payload.user.avatarUrl = null |
phone | 分享手機號 | payload.phone 不存在 |
userId 當主鍵 (最穩),或萬一 payload 沒 email 回個清楚錯誤頁 (不要 500)。
#錯誤碼
| HTTP | code | 意義 |
|---|---|---|
| 400 | OA_LOGIN_BAD_OA | oaId 格式錯 |
| 400 | OA_LOGIN_BAD_CODE | authCode 格式錯 |
| 400 | OA_LOGIN_BAD_REDIRECT | URL 不是 https |
| 400 | OA_LOGIN_INCOMPLETE | enabling 但沒填 successURL/cancelURL |
| 403 | OA_LOGIN_NOT_ENABLED | OA 沒啟用 SSO |
| 403 | OA_LOGIN_REDIRECT_NOT_ALLOWED | URL host 不在白名單 |
| 404 | OA_LOGIN_OA_NOT_FOUND | OA 不存在或停用 |
| 404 | OA_LOGIN_SESSION_NOT_FOUND | session token 無效 |
| 404 | OA_LOGIN_CODE_INVALID | authCode 無效 / 過期 / 已用過 |
| 409 | OA_LOGIN_NOT_PENDING | session 已 approved/cancelled,不能再操作 |
| 409 | OA_LOGIN_NOT_CONFIGURED | OA 設定不完整 |
| 410 | OA_LOGIN_CANCELLED | session 被取消 |
| 410 | OA_LOGIN_EXPIRED | session 超過 3 分鐘 |
#MiniApp 簡介
MiniApp 是嵌在嗶應 App 中的 WebView 頁面,由 OA 自家網站 host。當追蹤者在 BeeIn 內打開你 的 MiniApp(例如下單、預約、表單),你的網頁可以直接以 OA 名義回傳訊息 到該追蹤者的對話視窗 — 不需要他先離開頁面、不需要他自己再開 chat。
授權方式 — contactToken
嗶應 App 在打開你的 MiniApp WebView 時,會自動把一個短期憑證
contactToken 透過 URL query 參數 ?ct=<token> 傳給你的網頁。
WebView 開啟期間,App 每 ~25 分鐘會自動 refresh 一次,以 postMessage
通知你的網頁更新本機變數。
你的 MiniApp 拿到這個 token,就能呼叫下方 endpoint 對該追蹤者發送訊息。
// 在 MiniApp 內讀取 token
const ct = new URLSearchParams(location.search).get('ct');
// 監聽 refresh
window.addEventListener('message', (e) => {
if (e.data?.type === 'contactToken') {
currentToken = e.data.token;
}
});
限制
contactToken有效 30 分鐘,僅綁定當前這位追蹤者與 當前這個 OA。萬一外洩影響範圍非常小。- 速率限制:每位追蹤者每小時最多 60 則訊息。
- 訊息固定以 OA 名義發送,落入該追蹤者與 OA 的私訊對話; 不能指定其他發送者。
- 所有 endpoint 都用 HTTPS,base URL 固定
https://webhook.beein.app。
#設計建議
- 必須
https://,不接受純 HTTP。 - 以手機優先,建議內容寬度
360 ~ 430px,自動適應大螢幕。 - 不要自製巨大頂部 nav — BeeIn 已提供 AppBar,頁面內只放業務內容。
- 每個 URL 都要能單獨重新載入(WebView 可能被系統回收或網路中斷重整)。
- 下單、預約、送出表單等關鍵動作必須由你的後端確認,不可只信前端狀態。
- 錯誤頁不要顯示 stack trace、Cloudflare 502 原始頁、內部 hostname 等內容。
- 需要更改 AppBar 標題 / 按鈕,請在
beein:ready事件之後呼叫window.beein.*。
#OA 管理員需要設定的內容
位置:BeeIn App → OA 管理區 → MiniApp 管理。
| 欄位 | 說明 |
|---|---|
| 名稱 | 顯示在 BeeIn AppBar / 入口提示的 MiniApp 名稱。 |
| URL | MiniApp 首頁,例如 https://shop.example.com/beein。 |
| RSA-2048 公鑰 | 合作方後端產生一組 RSA key pair,只把公鑰貼到 BeeIn;私鑰留在合作方 server。 |
| 顯示方式 | icon 顯示入口圖示;auto 進入 OA 對話時自動打開;none 不顯示入口。 |
| Icon 類型 | 商城、票券、客服、會員、一般網站、自訂 icon。 |
| 啟用 MiniApp | 關閉後 follower 看不到入口。 |
BeeIn 會用你貼上的公鑰加密 follower 身份,App 開 WebView 時把加密結果放在
X-BeeIn-Auth header(見下一節)。你的 server 用私鑰解密後即可建立自家網站 session。
#WebView 載入時 BeeIn 會送哪些資料
BeeIn App 在開啟 MiniApp 的 WebView 時,會把以下 header 與 query 一起送到你的網址:
GET https://shop.example.com/beein?ct=<contactToken>
X-BeeIn-Auth: <encrypted-auth-blob>
X-BeeIn-Theme: dark | light
| 欄位 | 說明 |
|---|---|
?ct= | 短效 contactToken(約 30 分鐘)。用來在 MiniApp 內呼叫 /miniapp/messages 等 endpoint。只放在記憶體,不寫 DB / log。 |
X-BeeIn-Auth | 給你後端驗證 BeeIn 使用者身份用的加密 blob,只保證初次載入時送出。後續站內跳轉請用你自己的 session cookie。 |
X-BeeIn-Theme | 使用者目前 BeeIn 主題(dark / light),方便你的網頁配色一致。 |
#X-BeeIn-Auth 解密內容
合作方 server 用私鑰解密後,會拿到類似結構:
{
"iss": "beein.app",
"aud": "<oaId or miniapp id>",
"sub": "<BeeIn userId>",
"personaId": "<active persona id>",
"displayName": "Mark",
"email": "[email protected]",
"scopes": ["user.id", "user.email"],
"iat": 1779000000,
"nbf": 1779000000,
"exp": 1779000300,
"nonce": "..."
}
必要驗證步驟:
- 解密成功(私鑰可解開)。
iss === "beein.app"。exp未過期。aud對應到你的 OA / MiniApp。- 若需要會員綁定,用
sub或personaId對應到自家會員。
建議第一次驗證成功後建立你自己的網站 session cookie,後續網頁內跳轉就依你自己 cookie 管理登入狀態。
X-BeeIn-Auth 確認,不能只信前端 window.beein.getUser()。
#JS Bridge — window.beein
BeeIn 注入 bridge 後會觸發 beein:ready。建議用 helper 等待:
function withBeeIn(fn) {
if (window.beein?.__installed__) return fn(window.beein);
window.addEventListener('beein:ready', function once() {
window.removeEventListener('beein:ready', once);
fn(window.beein);
});
}
常用 API
| API | 用途 |
|---|---|
beein.getUser() | 取得前端顯示用 BeeIn user 資料 |
beein.close({redirect, target}) | 關閉 WebView,可指定回聊天室或票券 |
beein.openOaChat(oaId) | 關閉 WebView 並進入指定 OA 聊天 |
beein.setNavTitle(title) | 設定 BeeIn AppBar 標題 |
beein.setNavRightAction(action) | 設定 AppBar 右側文字按鈕 |
beein.setHeaderActions(actions) | 設定 AppBar icon 列(最多 2 個) |
beein.setNav({title, rightAction}) | 一次設定標題與右側按鈕 |
beein.openExternal(url) | 用系統瀏覽器開外部網址 |
beein.scanQr() | 打開 BeeIn 掃碼器 |
beein.notify({title, body}) | 顯示本機通知 |
beein.passes.save / listMine / show / remove | BeeIn 票券:儲存 / 列出 / 顯示 / 移除 |
beein.sendMessage({text}) 仍可用,但語意是「使用者按確認後以使用者身份傳文字」。若你要讓 MiniApp 代表 OA 自動發訊息給該追蹤者,請改用下方 contactToken + /miniapp/messages。
AppBar 範例
withBeeIn((beein) => {
beein.setNav({
title: '購物車',
rightAction: { label: '結帳', style: 'primary', event: 'checkout' },
});
window.addEventListener('checkout', () => {
document.querySelector('#checkout-form')?.requestSubmit();
});
});
右上 icon 列
withBeeIn((beein) => {
beein.setHeaderActions([
{ id: 'orders', icon: 'receipt_long', tooltip: '歷史訂單' },
{ id: 'support', icon: 'support', tooltip: '客服' },
]);
window.__beein_event__ = (name, data) => {
if (name === 'headerAction' && data.id === 'support') beein.openOaChat('<oaId>');
if (name === 'headerAction' && data.id === 'orders') location.href = '/orders';
};
});
搜尋按鈕(內建建議清單)
withBeeIn((beein) => {
beein.setHeaderActions([{
id: 'shopSearch',
type: 'search',
icon: 'search',
placeholder: '搜尋商品',
submitTo: '/search?q={query}',
suggestionsLabel: '熱門搜尋',
suggestions: ['紅茶', '綠茶', '咖啡'],
}]);
});
#MiniApp 發訊息給追蹤者
支援的 type
| type | 用途 | 必填欄位 |
|---|---|---|
text | 純文字訊息 | text |
order | 結構化訂單卡片 | orderCard(JSON,≤ 4KB) |
media | 圖片 / 影片 / 音訊 / 檔案 | mediaId |
範例 — 文字
POST https://webhook.beein.app/miniapp/messages
Content-Type: application/json
{
"contactToken": "eyJ...",
"type": "text",
"text": "訂單已收到,預計 30 分鐘內出餐"
}
範例 — 訂單卡片(含底部按鈕)
{
"contactToken": "eyJ...",
"type": "order",
"orderCard": {
"orderId": "A20260517-001",
"status": "pending_payment",
"statusLabel": "待付款",
"items": [
{ "name": "宮保雞丁套餐", "qty": 1, "price": 180 }
],
"total": 180, "currency": "TWD"
},
"buttons": [
{
"id": "view_order",
"label": "查看訂單",
"style": "primary",
"action": "open_miniapp",
"path": "/order-complete/?order=A20260517-001"
},
{
"id": "tracking",
"label": "物流追蹤",
"style": "secondary",
"action": "open_miniapp",
"path": "/tracking/?order=A20260517-001",
"textColor": "#FFFFFF",
"bgColor": "#FF9800"
}
]
}
底部按鈕欄位 — buttons
每則訊息最多 4 個按鈕。text / order / media 任一 type 都可帶。
| 欄位 | 說明 |
|---|---|
id | 合作方自訂識別字串,^[a-zA-Z][a-zA-Z0-9_-]{0,49}$。BeeIn 不解析;點擊事件回傳該 id 給你做 analytics |
label | 按鈕文字,1–20 字元 |
style | primary(預設) / secondary / ghost |
action | open_miniapp / open_oa_chat / dismiss |
path | 當 action=open_miniapp 時必填;必須是 相對路徑(以 / 開頭,不可含 scheme / 反斜線,≤200 字元)— BeeIn 會接在 MiniApp 設定的 base URL 後面打開 WebView |
textColor | 選填。覆寫按鈕文字色。必須是 hex:#RGB / #RRGGBB / #RRGGBBAA。不接受 rgb() / 命名色 / CSS 變數 |
bgColor | 選填。覆寫按鈕背景色。同 textColor 的 hex 規則 |
顏色提示:兩個欄位都不填,UI 依 style(primary / secondary / ghost)渲染預設 BeeIn 主題色。
填一個(只填文字色或只填背景色)也可,剩下的仍走預設。**對比與可讀性由你負責**——
server 不檢查白底白字之類的可用性問題。
Response
{
"ok": true,
"messageId": "66e0...",
"conversationId": "66ab...",
"sentAt": "2026-05-17T12:05:00.000Z"
}
PHP 範例 — 後端直接呼叫
MiniApp 後端在處理完訂單/表單後想主動推一則 OA 訊息給該追蹤者,可以從伺服器端直接打 webhook endpoint。
contactToken 由 BeeIn webview 帶到前端,你的前端再轉送給後端使用(token 30 分鐘內有效)。
<?php
// BeeIn MiniApp — 從後端送一則 OA 訊息給目前 follower
// 用法: php beein-send.php "<contactToken from BeeIn WebView>"
$contactToken = $argv[1] ?? '';
if ($contactToken === '') {
fwrite(STDERR, "Usage: php beein-send.php <contactToken>\n");
exit(2);
}
$origin = 'https://your-miniapp.example.com'; // 你的 MiniApp host
$endpoint = 'https://webhook.beein.app/miniapp/messages';
$payload = json_encode([
'contactToken' => $contactToken,
'type' => 'text',
'text' => 'Probe at ' . date('c'),
], JSON_UNESCAPED_UNICODE);
$ch = curl_init($endpoint);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Origin: ' . $origin,
'Referer: ' . $origin . '/checkout/',
'User-Agent: YourBrand-MiniApp/1.0 (php-curl)',
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => true,
CURLOPT_TIMEOUT => 10,
]);
$response = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$headerSize = (int) curl_getinfo($ch, CURLINFO_HEADER_SIZE);
curl_close($ch);
echo "==== RESPONSE (status=$status) ====\n";
echo trim(substr($response, 0, $headerSize)) . "\n\n";
echo "Body:\n" . substr($response, $headerSize) . "\n";
小提醒:Origin / Referer / User-Agent header 對 BeeIn server 而言僅作為 log 識別,**不影響授權**(授權由 body 內 contactToken 決定);
server-to-server 不會被 CORS 影響。
常見 error
| HTTP | code | 原因 |
|---|---|---|
| 401 | WORKER_UNAUTHORIZED | 沒有經過 webhook.beein.app |
| 401 | CONTACT_TOKEN_INVALID / CONTACT_TOKEN_EXPIRED | token 不合法或過期,UI 端需重發 |
| 400 | MESSAGE_TOO_LONG / ORDER_CARD_TOO_LARGE | 內容超過限制 |
| 400 | MINIAPP_BUTTONS_INVALID | buttons 不合規(超過 4 個、欄位錯、action=open_miniapp 缺 path 等) |
| 429 | MINIAPP_RATE_LIMITED | 該 follower 1 小時內已超過 60 則 |
#MiniApp 媒體上傳(3 步驟)
媒體(圖片/影片/音訊/檔案)採與既有 webhook 媒體同模式 —
先取一次性 upload token、PUT bytes 到 worker、confirm 後才能拿
mediaId 附到訊息上。
Step 1 — 取得 upload token
{
"contactToken": "eyJ...",
"mimeType": "image/jpeg",
"size": 123456
}
Response
{
"uploadUrl": "https://webhook.beein.app/bot/media/upload",
"token": "eyJ...",
"expiresAt": "...",
"maxSize": 200000000,
"mimeType": "image/jpeg"
}
Step 2 — PUT bytes 到 uploadUrl
PUT https://webhook.beein.app/bot/media/upload
X-Token: <token from step 1>
Content-Type: image/jpeg
Body: <raw bytes>
Step 3 — confirm 拿 mediaId
{
"contactToken": "eyJ...",
"token": "<token from step 1>",
"mimeType": "image/jpeg",
"actualSize": 123456
}
Response
{
"mediaId": "66f0...",
"downloadUrl": "/api/v1/media/66f0...",
"expiresAt": "..."
}
Step 4 — 用 mediaId 發訊息
POST https://webhook.beein.app/miniapp/messages
{
"contactToken": "eyJ...",
"type": "media",
"mediaId": "66f0...",
"text": "附上您的取貨單"
}
expiresAt,過期後重新拿。MiniApp 不要把 raw bytes 直接打到 origin —
一律走 worker bytes 路徑,避免曝露 origin IP。
#最小可用範例
<!doctype html>
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>BeeIn MiniApp Demo</title>
</head>
<body>
<h1 id="hello">歡迎</h1>
<button id="send">送一則 OA 訊息到 BeeIn</button>
<script>
// 1. 讀取 contactToken 並立刻從網址移除
const params = new URLSearchParams(location.search);
let contactToken = params.get('ct');
if (contactToken) {
params.delete('ct');
history.replaceState({}, document.title, location.pathname);
}
// 2. 監聽 token refresh(兩種事件都接,跨 WebView 行為差異)
window.addEventListener('message', (e) => {
if (e.data?.type === 'contactToken') contactToken = e.data.token;
});
window.addEventListener('beein:contactToken', (e) => {
contactToken = e.detail.token;
});
// 3. window.beein bridge ready helper
function withBeeIn(fn) {
if (window.beein?.__installed__) return fn(window.beein);
window.addEventListener('beein:ready', function once() {
window.removeEventListener('beein:ready', once);
fn(window.beein);
});
}
withBeeIn((beein) => {
const user = beein.getUser();
document.querySelector('#hello').textContent = `Hi, ${user?.displayName || 'BeeIn user'}`;
beein.setNavTitle('Demo MiniApp');
});
// 4. 按鈕:代表 OA 傳訊息給目前 follower
document.querySelector('#send').addEventListener('click', async () => {
if (!contactToken) {
alert('尚未取得 contactToken,請重新開啟 MiniApp');
return;
}
const r = await fetch('https://webhook.beein.app/miniapp/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contactToken,
type: 'text',
text: '您已完成 MiniApp 操作',
}),
}).then(x => x.json());
if (!r.ok) alert(r.code || '發送失敗');
});
</script>
</body>
</html>
#對接檢查清單
| 項目 | 負責方 | 必要性 |
|---|---|---|
| MiniApp 網站部署 HTTPS | 合作方 | 必做 |
| 產生 RSA-2048 key pair,私鑰留在 server | 合作方 | 必做 |
| OA 管理區貼 URL / 公鑰 / icon / 顯示方式 | OA 管理員 | 必做 |
後端解密 X-BeeIn-Auth 並建立 session cookie | 合作方 | 必做 |
前端支援 beein:ready 與 window.beein | 合作方 | 建議 |
讀 ct 並支援 token refresh event | 合作方 | 要從 MiniApp 發訊息則必做 |
MiniApp 訊息走 webhook.beein.app/miniapp/* | 合作方 | 必做 |
| 錯誤頁不曝光內部 URL / stack trace | 合作方 | 必做 |
安全注意事項
contactToken只放記憶體,不進 DB / log / 第三方分析。X-BeeIn-Auth必須在 server 端解密驗證,不要只信前端getUser()。- RSA 私鑰只放合作方 server,不貼 BeeIn,不放前端。
- MiniApp 訊息只能打
https://webhook.beein.app/miniapp/*。 - 所有下單 / 付款 / 核銷必須在合作方後端做權限檢查,不可只信前端。
- 若用
openExternal(url),URL 會進系統瀏覽器歷史;不要把 token 放在外部 URL。
#使用場景
AI 客服自動回覆
使用者傳訊 → webhook 收到 oa.user.message → 丟給你的 LLM → 回 webhook
response body 帶 text reply。要延遲回覆就用主動 Bot API。
訂單通知 + 收據圖片
下單 → 你的後台產收據圖 → upload-url 拿 token → PUT bytes → 用 mediaId 發
image 訊息給用戶。整段純後端對後端,使用者打開 app 就看到。
HereLink 接送通知
家長的 GPS 進地理圍籬 → BeeIn 端觸發 → webhook 送 herelink.arrival.triggered
到你的校園系統 → 系統自動 broadcast 給老師群組。
排程通知 / 提醒
cron job 每天 9am 用 Bot API 發給特定追蹤者:「您今日的待辦」、「藥物提醒」、「會議
30 分鐘前」。一律走 /bot/message。
真人接手 / AI 雙模式
AI webhook 接訊息自動回;管理員按「我來接手」→ webhook 暫停投遞 → 後續訊息只到 admin app;接手結束 → 恢復 webhook 投遞。完全在 BeeIn 後台處理,不用你寫狀態機。
CRM 資料同步
新追蹤者 → webhook oa.follow.changed → 寫入你的 CRM;取消追蹤 → 標記
inactive。Pure event-driven,不用 polling。
#錯誤碼
| HTTP | code | 說明 |
|---|---|---|
| 401 | OA_NO_TOKEN | 沒帶 Authorization header |
| 401 | OA_TOKEN_INVALID | Token 格式錯(不是 oa_ 開頭)或不存在 |
| 403 | OA_PERMISSION_DENIED | Token 沒有此操作的權限 |
| 400 | INVALID_MIME | 上傳 mimeType 不在白名單 |
| 400 | FILE_TOO_LARGE | 超過該 mime 的大小上限 |
| 401 | INVALID_TOKEN | Upload token 簽章錯或過期(5 分鐘) |
| 413 | FILE_TOO_LARGE | 實際 PUT 的 Content-Length 超過 token 申請量 |
| 400 | CONTENT_TYPE_MISMATCH | PUT 的 Content-Type 跟 token 申請時不同 |
| 404 | RECIPIENT_NOT_FOUND | 收件者沒追蹤這個 OA |
| 502 | ORIGIN_UNREACHABLE | worker 連不到 origin(極少發生,自動 retry) |
最後更新:2026-05-02 · v2.7 · [email protected]