BeeIn OA API

官方帳號開發者文件 — 一個 base URL,所有 OA 整合走這條。

Base URL: https://webhook.beein.app 驗證:Bearer oa_xxx 版本:v2.7

#介紹

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_messagebroadcastread_followers)。

所有請求都帶上:

Authorization: Bearer oa_xxxxxx_xxxxxxxxxxxxxxxxxxxxxxxx
Token 只在建立時顯示一次。 沒記下來就只能廢掉重發。一張 OA 可發多個 token、給不同 permission,建議每個串接系統一張獨立 token,方便個別撤銷。

#快速開始 — 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
}

#發送訊息給追蹤者

POST /bot/message 需 send_message 權限

對「已追蹤你 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

範例 — 文字

{
  "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

POST /bot/media/upload-url 需 send_message 權限
{
  "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

PUT /bot/media/upload Bearer <token from step 1>
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/gif5 MB
音訊audio/mp4 audio/m4a audio/mpeg10 MB
影片video/mp450 MB
14 天保留期。上傳成功後檔案會跟一般訊息媒體一樣,14 天後自動清理。Token 本身的 audit 紀錄 保留 30 天供稽核。

#廣播給所有追蹤者

POST /bot/broadcast 需 broadcast 權限

對「全部追蹤者」一次發一則訊息。後端做 batch 投遞 + 速率控管。

{
  "type": "text",
  "text": "今晚 10pm 系統維護,預計 30 分鐘完成"
}
廣播是高風險動作 — 一旦發出無法收回。 建議拆獨立一張 token 只給 broadcast 權限(不要跟 send_message 混用), 鎖在團隊密碼管理器,少用且容易監控 — 配合 lastUsedAt 異常使用警報效果最好。 日常用的 token 只開 send_message,外流也不會被拿來發垃圾廣播。

#追蹤者列表

GET /bot/followers?page=1&limit=100 需 read_followers 權限
GET /bot/followers/:userId 需 read_followers 權限

列出 / 查詢個別追蹤者。回傳僅含 OA 已被授權看到的欄位(displayName / avatarUrl / followedAt 等), 不包含 email / phone 等隱私資料。

#接收事件 — 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-TimestampUnix seconds
X-HereLink-Signaturev1=<HMAC-SHA256(secret, timestamp + "." + body)>
x-beein-event 舊版 header 也會帶(向後相容)

回應

2xx 表示已收到。回應 body 可選擇帶上立即回覆:

{
  "replies": [
    { "type": "text", "text": "收到,馬上請老師回覆" }
  ]
}
只支援 text reply。要回媒體請走主動 Bot API(推送媒體),不要塞在 webhook response body。

#事件清單

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.removedOA staff 進出
member.linked / member.unlinkedLIFF 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。

  1. 大白鯨在論壇上按「聯絡副站長 Mark」
  2. 論壇後端 POST /linked-sites/qr
    • targetBeeInId = @mark(被加方 — Mark 的 BeeIn ID,論壇後端記在 DB,前端絕不顯示)
    • siteUserInfoUrl = 大白鯨在論壇的 profile URL(給 Mark 之後查身份用)
    • siteUserId / siteUserName = 大白鯨在論壇的 ID / 帳號
    • displayName = 「大白鯨(會員 5 年)」
    • siteName = 「XXX 討論區」
  3. 論壇拿到 qrContent + webUrl,顯示給大白鯨「請用 BeeIn 掃描 / 點此打開 BeeIn」
  4. 大白鯨用 BeeIn app 開啟連結 → scanner = @while
  5. BeeIn UI 顯示:「你正在加 XXX 討論區的副站長 Mark 為好友」(沒有 @mark 字樣)→ 大白鯨按確認
  6. BeeIn server 透過 worker 去 GET siteUserInfoUrl,拉大白鯨在論壇的 member 資料
  7. Mark 的 BeeIn app 收到通知卡:「@while 連動到你 — 他在 XXX 論壇是『大白鯨』(會員 5 年, 發文 200 篇)」
  8. BeeIn 推 member.linked webhook 給論壇後端:「@mark 與大白鯨已連動」
  9. 之後雙方在 BeeIn 一般訊息聊天 — Mark 即使忘了 @while 是誰,去「連動列表」一查就知道是論壇大白鯨

取得 Site API Key(bsk_xxx

要先聯絡 BeeIn 申請 Site API Key — 一張獨立的長期金鑰,跟 OA Bot Token(oa_) 完全分開。請寫信至 [email protected] 提供站台基本資訊 (站名 / 用途 / 預估 MAU)申請。

Site API Key 跟 OA Bot Token 不可混用。 oa_xxx 用於官方帳號 Bot API(/bot/*);bsk_xxx 用於站台連動 API(/linked-sites/*)。混用會收 401 INVALID_API_KEY

#建立連動 QR

POST /linked-sites/qr Bearer bsk_xxx

建一張一次性 QR,預設 1 小時有效,被掃過 / 被同意綁定就立即失效。

Body

欄位必填說明
targetBeeInId被加方(管理員 / 客服)的 BeeIn ID 或 @username
siteUserInfoUrl主動方(掃 QR 那位 / 客戶)在你站台的 profile URL — 給被加方驗證身分用
siteName顯示給被加方看的「站台名」,1-100 字
siteUserId主動方在你站台的 user ID(callback 會帶回)
siteUserName主動方在你站台的 username
displayName主動方顯示名 / 標籤(最多 50 字),例:「Mark Wang (VIP)」
expiresInQR 有效秒數,60-86400,預設 3600
webhookUrl連動 / 解綁 / 動作 callback 走這個 URL(建議加,否則拿不到通知)
siteUserId / siteUserName / siteUserInfoUrl / displayName 描述的都是「主動方」(要去掃 QR 的那個人)— 不是被加方。被加方只給 targetBeeInId 即可。

範例(XXX 討論區)

curl -X POST https://webhook.beein.app/linked-sites/qr \
  -H "Authorization: Bearer bsk_xxxxxx_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "targetBeeInId":   "@mark",                                      // 被加方:副站長 Mark(論壇後端 DB 持有,前端不顯示)
    "siteUserInfoUrl": "https://forum.example.com/api/users/whale123/profile",
    "siteName":        "XXX 討論區",
    "siteUserId":      "whale123",                                   // 主動方:大白鯨
    "siteUserName":    "大白鯨",
    "displayName":     "大白鯨(會員 5 年, 發文 200 篇)",
    "expiresIn":       1800,
    "webhookUrl":      "https://forum.example.com/api/beein/webhook"
  }'

回應:

{
  "qrCode": {
    "_id":       "69f5...",
    "qrContent": "beein://link/69f5...",    // 給 app catch 用
    "webUrl":    "https://beein.app/link/69f5...",  // 給用戶手機 click 用
    "expiresAt": "2026-05-02T15:00:00.000Z",
    "status":    "pending"
  }
}

siteUserInfoUrl 你要回什麼(被加方會看到的內容)

被加方(副站長 Mark)BeeIn app 在連動成功後會展示這份資料 — 並且**未來他想查 「@while 是誰」時可以隨時翻出來**。BeeIn server 透過 worker GET 此 URL(不洩漏 BeeIn IP),期待回:

{
  "success": true,
  "member": {
    "siteUserId":   "whale123",
    "siteUserName": "大白鯨",
    "displayName":  "大白鯨",
    "avatarUrl":    "https://forum.example.com/avatars/whale123.jpg",
    "role":         "member",
    "joinedAt":     "2021-03-15",
    "postCount":    200,
    "profileUrl":   "https://forum.example.com/u/whale123",
    "extra": { /* 自訂欄位,BeeIn UI 整段展開給被加方看 */ }
  }
}

失敗時回 {"success": false, "error": {"code": "...", "message": "..."}}

隱私重點: 整個流程主動方(大白鯨)完全看不到 @mark — BeeIn app 顯示對方時只會用站台給的 siteName「XXX 討論區的副站長 Mark」當作初次卡片標題;連動成功後雙方在 BeeIn 訊息對話時, 主動方那端看到的就是 對方在 BeeIn 上自己設的身分資訊(displayName / 頭像 / 簽名) — 可能是「Mini」之類完全不同的代稱,BeeIn 全程不會把 @mark 透露給主動方。 被加方則完整看到對方 BeeIn @username + 站台 member 資訊。

#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 名稱。
URLMiniApp 首頁,例如 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": "..."
}

必要驗證步驟:

  1. 解密成功(私鑰可解開)。
  2. iss === "beein.app"
  3. exp 未過期。
  4. aud 對應到你的 OA / MiniApp。
  5. 若需要會員綁定,用 subpersonaId 對應到自家會員。

建議第一次驗證成功後建立你自己的網站 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 / removeBeeIn 票券:儲存 / 列出 / 顯示 / 移除
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 發訊息給追蹤者

POST webhook.beein.app/miniapp/messages 需 contactToken

支援的 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 字元
styleprimary(預設) / secondary / ghost
actionopen_miniapp / open_oa_chat / dismiss
pathaction=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

HTTPcode原因
401WORKER_UNAUTHORIZED沒有經過 webhook.beein.app
401CONTACT_TOKEN_INVALID / CONTACT_TOKEN_EXPIREDtoken 不合法或過期,UI 端需重發
400MESSAGE_TOO_LONG / ORDER_CARD_TOO_LARGE內容超過限制
400MINIAPP_BUTTONS_INVALIDbuttons 不合規(超過 4 個、欄位錯、action=open_miniapppath 等)
429MINIAPP_RATE_LIMITED該 follower 1 小時內已超過 60 則

#MiniApp 媒體上傳(3 步驟)

媒體(圖片/影片/音訊/檔案)採與既有 webhook 媒體同模式 — 先取一次性 upload token、PUT bytes 到 worker、confirm 後才能拿 mediaId 附到訊息上。

Step 1 — 取得 upload token

POST webhook.beein.app/miniapp/media/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

POST webhook.beein.app/miniapp/media/confirm
{
  "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":         "附上您的取貨單"
}
安全提醒: 媒體 URL 帶有 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:readywindow.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。

#錯誤碼

HTTPcode說明
401OA_NO_TOKEN沒帶 Authorization header
401OA_TOKEN_INVALIDToken 格式錯(不是 oa_ 開頭)或不存在
403OA_PERMISSION_DENIEDToken 沒有此操作的權限
400INVALID_MIME上傳 mimeType 不在白名單
400FILE_TOO_LARGE超過該 mime 的大小上限
401INVALID_TOKENUpload token 簽章錯或過期(5 分鐘)
413FILE_TOO_LARGE實際 PUT 的 Content-Length 超過 token 申請量
400CONTENT_TYPE_MISMATCHPUT 的 Content-Type 跟 token 申請時不同
404RECIPIENT_NOT_FOUND收件者沒追蹤這個 OA
502ORIGIN_UNREACHABLEworker 連不到 origin(極少發生,自動 retry)

最後更新:2026-05-02 · v2.7 · [email protected]