On this page

BeeIn OA API

官號開發者文件:已核對的端點、憑證與回傳格式。

Base URL: https://webhook.beein.app 驗證:Bearer oa_xxx 核對日期:2026-09-18

#介紹

官號 Bot API、連動站台與 MiniApp 使用 https://webhook.beein.app;一般群組機器人另用 account-bot API。下方路徑接在各章節的 Base URL 後,官號入口不要另加 /api/v1。

OA 整合分兩個方向:

  • 主動:呼叫 Bot API(/bot/*)發送已支援的訊息、查詢追蹤者與上傳媒體。
  • 被動:使用者跟你的 OA 互動時(傳訊、追蹤、HereLink 到點)我們戳你預設的 webhook URL

#驗證

官號 Token 以 oa_ 開頭,在官號開發者設定建立。查追蹤者需 read_followers、發訊息需 send_message、查官號資料需 read_profile、登入串接需 manage_login_config。Token 綁定單一官號,不是使用者登入 JWT。

從你的後端送出以下標頭,不要把官號 Token 放在前端 JavaScript、網址或日誌:

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 權限

recipientUserId 使用 followers[].userId._id(相容舊欄位 userId,請勿同時帶入兩者)。收件者必須仍追蹤此官號且未被封鎖。delivered: true 代表伺服器已接受/保存,不代表裝置送達或已讀。目前各節點限流儲存區按官號計算每分鐘 60 次、每日 5,000 次請求;429 會附 error.resetSec。重試也計入請求額度。

支援的 type

type用途必填欄位
text純文字text
oa.text純文字(v2.7 spec 同義)payload.text 或 text
oa.card結構化卡片payload JSON
sticker貼圖(v2.7.139)packId, stickerId

一般文字/卡片/貼圖可帶 idempotencyKey(1–160 字元)避免重送。每則訊息保存一個 key,只有重試同一請求才重用;不適用獨立的 oa.order/oa.order_status 流程。notification.body(1–240 字元)可指定通知預覽,請勿放入機密內容。

範例 — 文字

{
  "recipientUserId": "69cecc...",
  "type": "text",
  "text": "訂單已出貨,預計明天送達"
}

範例 — 卡片

{
  "recipientUserId": "000000000000000000000002",
  "type": "oa.card",
  "payload": {
    "title": "今日特餐",
    "subtitle": "午餐套餐 NT$180",
    "imageUrl": "https://your-cdn.example/menu.jpg",
    "fields": [
      {
        "label": "供應時間",
        "value": "11:00–14:00"
      }
    ],
    "actions": [
      {
        "label": "立即訂購",
        "url": "https://your-shop.example/order/123"
      }
    ]
  }
}

#媒體上傳

POST/bot/media/upload-urlsend_message

此端點只負責建立上傳憑證。官號 Bot Token 與 MiniApp contactToken 不可混用。MiniApp 完整附件流程請看MiniApp 媒體。

{"mimeType":"image/png","size":234567}

回傳 uploadUrl、token、tokenId、expiresAt、maxSize、mimeType。請使用回傳的上傳網址,不要自行拼接儲存路徑。

PUT <uploadUrl>
Authorization: Bearer <upload token, not oa_ token>
Content-Type: image/png
Content-Length: 234567

<raw bytes>

成功回傳 {ok: true, mediaId, downloadUrl, expiresAt}。這只代表媒體已建立,不代表訊息已發送。

mimeType上限
image/png, image/jpeg, image/webp, image/gif5 MiB (5242880 bytes)
audio/mp4, audio/m4a, audio/mpeg10 MiB (10485760 bytes)
video/mp450 MiB (52428800 bytes)

只接受表列 MIME,不接受 PDF、ZIP 等任意檔案。上傳 Token 有效 5 分鐘;媒體初始保留 14 天,以 expiresAt 為準;Token 稽核資料保留 30 天。maxSize 為 MIME 上限,該次上傳仍受申請時 size 限制。

#追蹤者列表

GET/bot/followers?page=1&limit=50需 read_followers 權限

追蹤者是接收官號訊息的使用者,不是管理員或工作人員。Token 只可查詢所屬官號。列表與 total 均排除被官號封鎖的追蹤者,不回傳 email 或 phone。

curl "https://webhook.beein.app/bot/followers?page=1&limit=50" \
  -H "Authorization: Bearer $BEEIN_OA_TOKEN"

回傳範例(節錄欄位):

{
  "followers": [{
    "_id": "000000000000000000000003",
    "accountId": "000000000000000000000001",
    "userId": {
      "_id": "000000000000000000000002",
      "username": "example_user",
      "displayName": "Example User",
      "avatarUrl": null,
      "isVerified": false
    },
    "isBlocked": false,
    "followedAt": "2026-09-18T00:00:00.000Z"
  }],
  "total": 1,
  "page": 1
}
Field用途
total符合篩選的追蹤者總數,不是本頁筆數。
followers[].userId._id發訊息的 recipientUserId;不要使用外層 followers[]._id(追蹤關係紀錄 ID)。
page / limit從第 1 頁開始,預設每頁 50 筆。使用正整數,建議每頁 50–100 筆,逐頁取得;排序為 followedAt 新到舊。分頁不是固定快照,資料變動時請依使用者 ID 去重,必要時重新同步。

若 userId 為 null(關聯使用者已不存在),請略過,不可把追蹤關係 ID 當成收件者。保存 ID 不代表永久可發訊息;發送時仍會檢查追蹤與封鎖狀態。

GET/bot/followers/:userId需 read_followers 權限

個別查詢回傳 {follower: {...}},同樣使用 follower.userId._id,另可能包含 lastSeen。此端點也可能回傳 isBlocked=true 的關係,請檢查狀態;不存在則回 404 OA_NOT_FOLLOWER。

GET/bot/accountread_profile

官號基本資料回傳 {account: {...}},含 name、username、followerCount 等。followerCount 是官號維護的計數;需要與上述名單一致的人數時,請使用列表回傳的 total。

取得 ID 後,使用具有 send_message 權限的同官號 Token 發送:

POST https://webhook.beein.app/bot/message
Authorization: Bearer YOUR_OA_TOKEN
Content-Type: application/json

{
  "recipientUserId": "000000000000000000000002",
  "type": "text",
  "text": "Hello from BeeIn"
}

#輸入中 / 挑選貼圖中… 指示器

POST /bot/typing 需 send_message 權限

告訴對方「對方正在輸入…」或「對方挑選貼圖中…」。送或不送,由開發者自己決定 — 回應快的機器人可以完全不送;走 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"
}
8 秒自動消失。若你的機器人需要持續顯示,請在 8 秒過期前再 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 可用貼圖

GET /bot/stickers 需 send_message 權限

列出 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
        }
      ]
    }
  ]
}
直接用回傳的 packId + stickerId 呼叫 POST /bot/message type=sticker。 imageUrl 是給 UI 預覽用的,發送時不用傳。

#發送貼圖

POST /bot/message 需 send_message 權限

沿用 /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。

錯誤碼

HTTPcode情境
400STICKER_INVALID_INPUT沒帶 packId 或 stickerId
400STICKER_INVALID_ID不是合法 ObjectId
403STICKER_PACK_NOT_INSTALLEDOA owner 沒裝這個包
403STICKER_PACK_UNAVAILABLE包不是 published(draft / removed)
404STICKER_PACK_NOT_FOUND找不到此貼圖包
404STICKER_NOT_FOUND該包內沒這個 sticker
404OA_NO_OWNEROA 沒設定 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 設定

BeeIn 會將已訂閱事件 POST 到你的 HTTPS Webhook。請在官號開發者設定配置;每個官號最多五個 Webhook,包含停用項目。

必要條件

  • HTTPS(不接受 http://、不接受 IP literal、不接受私網 hostname)
  • Port 限 443 或 8443
  • 儲存時需通過握手。測試請求只有舊版 x-beein-* 簽章標頭;先驗證 sha256=HMAC-SHA256(secret, rawBody),再回 HTTP 200 JSON {"challenge_response":"HEX(HMAC-SHA256(secret, challenge))"}。下方驗章範例包含握手處理。
  • 每次接收端逾時 10 秒。失敗(包含非 2xx 與網路錯誤)依 maxRetries 重試:預設重試 3 次,上限 5 次,間隔為 5 秒、30 秒、2 分鐘、10 分鐘、30 分鐘;含首次最多 6 次投遞。連續 10 筆耗盡重試會停用 Webhook。重試在程序內執行,不是持久化必達保證。

一般事件投遞標頭(握手不同)

Header說明
X-HereLink-Event-Id事件唯一 ID,retry 期間不變 — 用來去重
X-HereLink-TimestampUnix seconds
X-HereLink-Signaturev1=<HMAC-SHA256(secret, timestamp + "." + body)>
x-beein-event 等舊版 header 也會帶(向後相容)

回應

驗章並可靠接收事件後回 2xx。官號訊息自動回覆可使用下列第一個文字項目,或 {reply: "..."};並不會發出 replies 裡的所有項目。一般群組機器人需透過其 messages API 回覆。

{
  "replies": [
    { "type": "text", "text": "收到,馬上請老師回覆" }
  ]
}
自動回覆只處理文字。MiniApp 附件請用MiniApp 媒體流程,不要塞在 Webhook 回應內。

#事件清單

OA 在後台訂閱要接收的事件。建議只訂你真的會處理的,避免 retry 佔頻寬。

對話 — Conversation

事件觸發受人工接手影響
oa.user.message追蹤者傳訊息給 OA是
message.received同上(舊名,仍支援)是
message.callback追蹤者點 inline 按鈕否

追蹤 / 成員

事件觸發
oa.follow.changed追蹤/取消追蹤的訂閱別名。body 保留實際觸發的 event(通常為 follower.added 或 follower.removed),不要假設必有 data.action。
follower.added / follower.removed實際送出的名稱為 follower.added / follower.removed;follow.added / follow.removed 仍可作為訂閱別名。
member.added / member.removedOA staff 進出

HereLink(接送通知)

事件觸發受人工接手影響
herelink.arrival.triggered追蹤者到點,OA 收到通知是

Event envelope

官號事件封裝(不等於一般群組 Bot 或登入通知格式)。conversationId 可能為 null;event 是實際觸發名稱,不一定等於訂閱別名。

{
  "eventId": "c47aef1d-756d-4a30-83a6-7a19bb1bf974",
  "event": "message.received",
  "version": "2026-08-31",
  "oaId": "000000000000000000000001",
  "accountId": "000000000000000000000001",
  "conversationId": null,
  "timestamp": "2026-09-18T00:00:00.000Z",
  "deliveryAttempt": 1,
  "data": {}
}

#簽章驗證

請先驗證原始 body,再解析 JSON,拒絕格式錯誤/逾期標頭,並依事件 ID 去重。此路由需放在 express.json() 前,密鑰留在後端。下方範例同時處理舊版簽章握手。

Node.js

const crypto = require('node:crypto');
const express = require('express');
const app = express();
const secret = process.env.BEEIN_WEBHOOK_SECRET;
if (!secret) throw new Error('Set BEEIN_WEBHOOK_SECRET');

// Register BEFORE any app.use(express.json()).
app.post('/webhook', express.raw({ type: 'application/json', limit: '1mb' }), (req, res) => {
  if (!Buffer.isBuffer(req.body)) return res.sendStatus(400);
  const legacy = req.get('x-herelink-signature') === undefined;
  const ts = req.get(legacy ? 'x-beein-timestamp' : 'x-herelink-timestamp') || '';
  const signature = req.get(legacy ? 'x-beein-signature' : 'x-herelink-signature') || '';
  const match = (legacy ? /^sha256=([a-f0-9]{64})$/i : /^v1=([a-f0-9]{64})$/i).exec(signature);
  if (!/^\d{10}$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
    return res.sendStatus(400);
  }
  if (!match) return res.sendStatus(401);
  const hmac = crypto.createHmac('sha256', secret);
  if (!legacy) hmac.update(ts + '.');
  const expected = hmac.update(req.body).digest();
  if (!crypto.timingSafeEqual(expected, Buffer.from(match[1], 'hex'))) {
    return res.sendStatus(401);
  }
  let body;
  try { body = JSON.parse(req.body.toString('utf8')); }
  catch { return res.sendStatus(400); }
  if (!body || typeof body !== 'object' || Array.isArray(body)) return res.sendStatus(400);
  if (legacy) {
    // Legacy fallback is ONLY for the signed pre-save handshake.
    if (body.event !== 'webhook.test' || typeof body.challenge !== 'string' ||
        !body.challenge || body.timestamp !== Number(ts)) return res.sendStatus(401);
    return res.json({
      challenge_response: crypto.createHmac('sha256', secret).update(body.challenge).digest('hex')
    });
  }
  const eventId = req.get('x-herelink-event-id');
  if (!eventId) return res.sendStatus(400);
  // Production: durably enqueue once by eventId before returning 2xx.
  // Return 5xx if durable acceptance fails; do business work asynchronously.
  return res.json({});
});

PHP

<?php
$secret = getenv('BEEIN_WEBHOOK_SECRET');
if (!$secret) { http_response_code(500); exit; }
$legacy = !isset($_SERVER['HTTP_X_HERELINK_SIGNATURE']);
$ts = $_SERVER[$legacy ? 'HTTP_X_BEEIN_TIMESTAMP' : 'HTTP_X_HERELINK_TIMESTAMP'] ?? '';
$signature = $_SERVER[$legacy ? 'HTTP_X_BEEIN_SIGNATURE' : 'HTTP_X_HERELINK_SIGNATURE'] ?? '';
$raw = file_get_contents('php://input');
if (!preg_match('/^\\d{10}$/D', $ts) || abs(time() - (int)$ts) > 300) {
    http_response_code(400); exit;
}
$pattern = $legacy ? '/^sha256=([a-f0-9]{64})$/iD' : '/^v1=([a-f0-9]{64})$/iD';
if (!preg_match($pattern, $signature, $match)) { http_response_code(401); exit; }
$expected = hash_hmac('sha256', $legacy ? $raw : $ts . '.' . $raw, $secret);
if (!hash_equals($expected, strtolower($match[1]))) { http_response_code(401); exit; }
$body = json_decode($raw, true);
if (json_last_error() !== JSON_ERROR_NONE || !is_array($body)) {
    http_response_code(400); exit;
}
header('Content-Type: application/json');
if ($legacy) {
    if (($body['event'] ?? '') !== 'webhook.test' ||
        !is_string($body['challenge'] ?? null) || $body['challenge'] === '' ||
        ($body['timestamp'] ?? null) !== (int)$ts) { http_response_code(401); exit; }
    echo json_encode(['challenge_response' => hash_hmac('sha256', $body['challenge'], $secret)]);
    exit;
}
$eventId = $_SERVER['HTTP_X_HERELINK_EVENT_ID'] ?? '';
if ($eventId === '') { http_response_code(400); exit; }
// Production: durably enqueue once by eventId before returning 2xx.
echo '{}';
使用固定時間比較,且先驗證簽章長度。只檢查時間戳無法避免重複處理;請保存事件 ID,讓業務操作可安全重試。

#站台帳號連動(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 建立連動:把 @while 跟 XXX 討論區的 whale123(QR 帶的 userName)綁起來,寫進 Mark 端的「連動列表」。不會主動 GET 你的站台 — 所有 member 細節都等被加方點卡片時才用 WebView 載。
  7. Mark 的 BeeIn app 收到通知卡:「@while 連動到你 — 來自 XXX 討論區(大白鯨)」(卡片上只顯示建 QR 時帶的 displayName / inviteName,沒有 member 詳細欄位)。Mark 點卡片 → BeeIn 用 WebView 開 siteUserInfoUrl?username=whale123&beeinToken=… → 你的站台回 HTML 頁面,秀「會員 5 年, 發文 200 篇」等內容。
  8. BeeIn 推 member.linked webhook 給論壇後端:「@mark 與大白鯨已連動」
  9. 之後雙方在 BeeIn 一般訊息聊天 — Mark 即使忘了 @while 是誰,去「連動列表」一查就知道是論壇大白鯨

取得 Site API Key(bsk_xxx)

請在 BeeIn App 內申請 Site API Key — 一張獨立的長期金鑰,跟 OA Bot Token(oa_)完全分開。路徑:開啟 BeeIn App → 設定 → 開發者 / Linked Site → 新增站台金鑰,填入站名 / 用途 / 預估 MAU 等基本資訊,送出後即可取得 bsk_xxx(只顯示一次,請妥善 保存)。

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 小時有效,被掃過 / 被同意綁定就立即失效。

siteName、siteUserInfoUrl 與 webhookUrl 取自站台 key,QR 請求中的同名欄位會被忽略。displayName 與 inviteName 仍可逐次提供。

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...",
    "qrContent": "beein://link/69f5...",
    "webUrl": "https://beein.app/link/69f5...",
    "qrImageUrl": "https://webhook.beein.app/linked-sites/qr/69f5.../image",
    "qrImageDataUri": "data:image/png;base64,iVBORw0K...",
    "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" />
還想自己畫 QR?OK ─ qrContent(beein://link/...)永遠回。我們的 image 只是省事用,沒有強制換掉你既有的 QR 渲染流程。

Server 自動串 ?username=(v2.7.96)

bsk_xxx 的 siteUserInfoUrl 設成 base URL 即可,每次 BeeIn 開啟某個 user 的頁面時自動 append:

你的 base URLBeeIn 實際呼叫
https://your-site.com/user-info.phphttps://your-site.com/user-info.php?username=alice123
https://your-site.com/user-info.php?lang=zhhttps://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 使用者。
新串接請使用 HTML 會員頁。舊 JSON 會員查詢只適用於設定 supportedActions: ['member.query'] 的 key。
隱私重點: 整個流程主動方(大白鯨)完全看不到 @mark — BeeIn app 顯示對方時只會用站台給的 siteName「XXX 討論區的副站長 Mark」當作初次卡片標題;連動成功後雙方在 BeeIn 訊息對話時,主動方那端看到的就是 對方在 BeeIn 上自己設的身分資訊(displayName / 頭像 / 簽名)。被加方則完整看到對方 BeeIn @username + 站台 member 資訊。

#改連動會員顯示名(v2.7.96)

PATCH /linked-sites/member Bearer bsk_xxx

當站台使用者在你站上改名時,呼叫這支同步更新 BeeIn 端連動卡片顯示名 —— 不用知道 BeeIn 內部的 linkId,用你站上的 userName(就是建 QR 時帶的那個 unique ID)當 key:

curl -X PATCH https://webhook.beein.app/linked-sites/member \
  -H "Authorization: Bearer $BEEIN_SITE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"userName":"alice123","displayName":"Alice (VIP)"}'

回應:

{ "success": true, "updated": 1 }

錯誤碼

HTTPcode意義
401SITE_AUTH_INVALIDbsk_xxx 無效或被停用
404LINKED_MEMBER_NOT_FOUND這把 key 名下沒有 userName 對應的 active link
400(zod validation)body 缺欄位或型別錯

成功後 server 自動清掉相關連動的 member-info cache,下次 BeeIn UI refresh 就會看到新名字。

#連動站台 Webhook 簽章(v2.7.96)

連動站台設定 webhookSecret 後,BeeIn 會簽署送往 webhookUrl 的回呼。使用下方驗章範例,換成該站台的 secret。此回呼為盡力投遞,沒有持久化重試佇列;內容也不是官號事件的統一 envelope。

會帶簽章的事件

事件觸發
member.linked主動方掃 QR 完成 → BeeIn 通知你「連動成立」
link.created同一次連動另外送出的舊事件,event ID 不同。只處理其中一個事件名稱,或依 linkedSiteId 去重。

Headers

有設定 webhookSecret 時會送出兩組標頭。一般事件請驗證包含時間戳的 X-HereLink 簽章。

Header內容
X-HereLink-Event-Id穩定事件識別碼,不保證為 UUID;用於去重。
X-HereLink-TimestampUnix 秒數
X-HereLink-Signaturev1=<HMAC-SHA256(webhookSecret, timestamp + "." + body)>
x-beein-event事件名(舊版 header set)
x-beein-signaturesha256=<HMAC-SHA256(webhookSecret, body)> (舊版 — 只簽 body,不含 timestamp)
x-beein-timestampUnix 秒數
x-beein-delivery與 X-HereLink-Event-Id 相同的事件識別碼。

member.linked body 範例

{
  "event": "member.linked",
  "qrCodeId": "69f5...",
  "scannerId": "69ab...",
  "targetBeeInId": "69cd...",
  "linkedSiteId": "69ef...",
  "externalUserId": "whale123"
}
沒設 webhookSecret → 收到的 webhook 沒簽章。 建 bsk_xxx 時 webhookSecret 是選填,沒設的話 BeeIn 會 fire 出去 UNSIGNED (沒任何 X-HereLink-Signature / x-beein-signature header) — 你那邊驗章程式就會擋掉。 要啟用簽章,把 webhookSecret(隨機 32 byte hex 之類)填進 bsk_xxx 設定即可, BeeIn 內部會自動切到簽章路徑,不用改任何別的設定。
使用簽章範例,將環境變數 BEEIN_WEBHOOK_SECRET 設為此站台的 webhookSecret。採固定時間比對,並對已接受的事件 ID 去重。
WebView 的 beeinToken 使用 HS256:有設定時以站台 webhookSecret 簽署,否則使用 SHA-256(bsk token)。這個備援僅適用於 WebView Token,未設定 secret 的站台回呼不會因此自動有簽章。

#用 BeeIn 登入 — 簡介

讓追蹤你官號 (OA) 的使用者用 BeeIn App 掃 QR 登入你的網站。共 4 步:OA 設定 → 放登入按鈕 → 處理 callback → 用 authCode 換 user。同意畫面內建,scope (email / avatar / phone) 由使用者勾選後 server 才會送對應欄位。

Base URL: https://webhook.beein.app · PATCH /bot/login-config · manage_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"]
  }'

回傳目前設定,不含 webhookSecret。登入通知沒有簽章,必須視為未信任輸入。只有經官號 Token 驗證、綁定同官號的 /exchange 結果可用來確認身分;仍須核對 state 與後端登入 session。

欄位說明
notifyURL啟用時必填,必須使用 HTTPS。這是獨立的登入通知網址,不要求先註冊成官號事件 Webhook。
successURL瀏覽器轉向 — 同意後使用者帶 ?code=…&state=… 回到這。啟用時必填。
cancelURL瀏覽器轉向 — 取消或逾時帶 ?reason=cancelled|expired&state=…。啟用時必填。
allowedRedirectHosts白名單:successURL/cancelURL 的 host 必須在這 (防 open redirect)。子網域繼承。
allowedFrameAncestorsiframe 嵌入白名單。
requestedScopes["email","avatar","phone"] 子集。空陣列 = 只有 BeeIn userId + displayName。

讀目前設定:GET https://webhook.beein.app/bot/login-config(同樣的 token 認證)。

#Step 2 — 網站放登入按鈕

兩種方式擇一:

方式 A:redirect

<a href="https://webhook.beein.app/oa-login?oa=YOUR_OA_ID&state=RANDOM_NONCE">
  Sign in with BeeIn
</a>

方式 B:iframe 嵌入

<iframe id="beein-login"
  src="https://webhook.beein.app/oa-login?oa=YOUR_OA_ID&state=RANDOM_NONCE"
  width="380" height="520" frameborder="0"></iframe>
<script src="https://webhook.beein.app/oa-login/parent.js"></script>
<script>
const detach = BeeInLogin.attach(document.getElementById('beein-login'), {
  state: 'RANDOM_NONCE',
  callbackUrls: ['/auth/beein/callback', '/auth/beein/cancel']
});
</script>

使用 iframe 時,請一起載入 parent.js 並呼叫 BeeInLogin.attach,確認後才能由外層網頁自動跳轉。state 必須與 iframe 網址相同,callbackUrls 填入本站實際的成功與取消路徑。未接上時,部分瀏覽器會阻擋 iframe 自行跳轉,BeeIn 會顯示手動返回選項。

官方登入按鈕 (建議使用)

直接複製下面這段 HTML 到你的網頁,使用者一眼就知道是 BeeIn 登入。預覽:

BeeIn Sign in with BeeIn

<a href="https://webhook.beein.app/oa-login?oa=YOUR_OA_ID&state=RANDOM_NONCE"
   style="display:inline-flex;align-items:center;gap:10px;padding:10px 18px;
          border-radius:10px;background:#FFC107;color:#1a1a1a;
          font:600 15px/1 -apple-system,'Helvetica Neue','Noto Sans TC',sans-serif;
          text-decoration:none;box-shadow:0 1px 3px rgba(0,0,0,.12);">
  <img src="https://beein.app/assets/beein-signin-icon.png"
       alt="BeeIn" width="22" height="22"
       style="display:block;border-radius:5px;">
  Sign in with BeeIn
</a>

按鈕文字可改成你的語系(例:用 BeeIn 登入 / BeeIn でサインイン / 用 BeeIn 登录)。Icon 跟黃色 #FFC107 是 BeeIn 識別色,建議保留不要改 — 跟 LINE / Google / Apple 的官方登入按鈕同理,品牌一致使用者一眼認得。

state 必須 per-request 隨機,你自己存下來、callback 時對比。這是 CSRF 防護的標準做法,跟 OAuth state 一樣。建議 crypto.randomBytes(16).toString('base64url')。

URL 參數

參數必要?說明
oa必填你的 OA ID。
state必填CSRF 防護用,callback 會原封不動帶回。
lang選填強制 UI 語系。不填或填了不支援的值 → 自動偵測瀏覽器語系,認不出就退英文。
logoURL選填在 QR 頁上方顯示你網站的 logo (例如品牌標誌)。必須 https://、URL ≤256 字元;圖片 server 端強制 64×64 顯示,你給多大都不會跑版。沒填 = 不顯示。
mode選填UI 主題。dark = 暗色介面(深色背景、淺色文字),其他值或不填 = 預設亮色介面。QR 區塊不管 mode 一律白底(掃描器對比度需求)。

支援的 lang 值

值語言同義 (一律歸到此值)
enEnglishen-US / en-GB / en-…
zh-TW繁體中文 (Traditional Chinese)zh-HK / zh-MO / zh-Hant / zh (預設)
zh-CN簡體中文 (Simplified Chinese)zh-SG / zh-MY / zh-Hans
ja日本語 (Japanese)ja-JP / ja-…

例:?oa=XXX&state=YYY&lang=ja → 強制日文介面;?oa=XXX&state=YYY(不帶 lang) → 自動偵測。

頁面行為:每 180 秒自動換 QR、掃描後即時隱藏 QR (顯示「已掃描」)、同意後跳 successURL、取消或逾時跳 cancelURL。

#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 別名為了相容舊整合,任何一個都行。

  1. 驗證 state 跟 Step 2 存下來的一樣 (CSRF)。不一樣 → 拒絕。
  2. 拿 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": "000000000000000000000002",
  "displayName": "Example User",
  "username": "example_user",
  "avatarUrl": "https://your-cdn.example/avatar.webp",
  "email": "[email protected]",
  "oaId": "000000000000000000000001",
  "grantedScopes": [
    "email",
    "avatar"
  ],
  "approvedAt": "2026-09-18T00: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。

#登入通知回呼

同意後,伺服器會獨立於瀏覽器轉向,嘗試一次 POST notifyURL。此通知沒有簽章,也不保證必達。不可直接用其中 user/email 登入使用者;請在後端核對 state,再交換一次性 authCode。

POST <your notifyURL>
Content-Type: application/json

Body:

{
  "event": "oa_login.approved",
  "oaId": "000000000000000000000001",
  "sessionToken": "SESSION_TOKEN",
  "state": "RANDOM_NONCE",
  "user": {
    "userId": "000000000000000000000002",
    "displayName": "Example User",
    "avatarUrl": "https://your-cdn.example/avatar.webp"
  },
  "grantedScopes": [
    "email",
    "avatar"
  ],
  "email": "[email protected]",
  "authCode": "ONE_TIME_AUTH_CODE",
  "issuedAt": "2026-09-18T00:00:00Z",
  "approvedAt": "2026-09-18T00:00:05Z"
}
後端需協調通知與瀏覽器 callback:authCode 只能交換一次,成功後把已驗證結果存入該次登入 session;另一條路徑重用結果,不要再交換。登入通知失敗不自動重試。

#Scopes 規則

Scope含意未授權時
email分享 BeeIn 註冊 emailpayload.email 不存在
avatar分享頭像 URL (媒體 CDN, 公開)/exchange 不回傳 avatarUrl;通知 body 則為 user.avatarUrl=null。
phone分享手機號payload.phone 不存在
email 不勾 → 你拿不到 email。很多 OA 網站以 email 當帳號主鍵 → 直接登入失敗。BeeIn App 已在同意畫面警告,但 OA 端務必:支援 userId 當主鍵 (最穩),或萬一 payload 沒 email 回個清楚錯誤頁 (不要 500)。

#錯誤碼

HTTPcode意義
400OA_LOGIN_BAD_OAoaId 格式錯
400OA_LOGIN_BAD_CODEauthCode 格式錯
400OA_LOGIN_BAD_REDIRECTURL 不是 https
400OA_LOGIN_INCOMPLETE啟用時缺少 notifyURL、successURL 或 cancelURL。
403OA_LOGIN_NOT_ENABLEDOA 沒啟用 SSO
403OA_LOGIN_REDIRECT_NOT_ALLOWEDURL host 不在白名單
404OA_LOGIN_OA_NOT_FOUNDOA 不存在或停用
404OA_LOGIN_SESSION_NOT_FOUNDsession token 無效
404OA_LOGIN_CODE_INVALIDauthCode 無效 / 過期 / 已用過
409OA_LOGIN_NOT_PENDINGsession 已 approved/cancelled,不能再操作
409OA_LOGIN_NOT_CONFIGUREDOA 設定不完整
410OA_LOGIN_CANCELLEDsession 被取消
410OA_LOGIN_EXPIREDsession 超過 3 分鐘

#MiniApp 簡介

MiniApp 是嵌在嗶應 App 中的 WebView 頁面,由 OA 自家網站 host。當追蹤者在 BeeIn 內打開你 的 MiniApp(例如下單、預約、表單),你的網頁可以直接以 OA 名義回傳訊息 到該追蹤者的對話視窗 — 不需要他先離開頁面、不需要他自己再開 chat。

授權方式 — contactToken

BeeIn 開啟 MiniApp 時會以 ct 網址參數帶入短效 contactToken。請僅存記憶體並移除網址中的 ct。WebView 執行期間約每 25 分鐘嘗試更新,透過 beein:contactToken(及 postMessage)通知。背景暫停或斷線可能延後更新,過期後請重新開啟 MiniApp 或取得新 Token。

你的 MiniApp 拿到這個 token,就能呼叫下方 endpoint 對該追蹤者發送訊息。

const url = new URL(location.href);
let currentToken = window.beein?.getContactToken?.() || url.searchParams.get('ct');
url.searchParams.delete('ct');
history.replaceState(history.state, '', url.pathname + url.search + url.hash);

window.addEventListener('beein:contactToken', (event) => {
  if (typeof event.detail?.token === 'string') currentToken = event.detail.token;
});
// Read currentToken immediately before sending each request.
// Do not log it or embed third-party scripts on a token-bearing page.

限制

  • contactToken 有效 30 分鐘,綁定單一官號與追蹤者。它是持有者憑證,外洩者在有效期內可行使其能力;不可寫入日誌或分析工具。
  • 每個官號/追蹤者每小時 60 次計數操作。傳訊息與申請上傳 Token 共用額度,不能解讀為 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 以設定的 RSA 公鑰加密資料,後端解密 X-BeeIn-Auth。使用前請閱讀下方格式、驗證與信任限制。

#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 解密內容

目前格式為 base64(wrappedKey[256] + IV[16] + ciphertext)。使用 RSA-2048 OAEP-SHA256(MGF1-SHA256)解出 32-byte AES key,再以 AES-256-CBC/PKCS#7 解密。明文如下,不是 JWT。

{
  "iss":         "https://api.beein.app",
  "aud":         "<oaId or miniapp id>",
  "sub":         "<BeeIn userId>",
  "displayName": "Mark",
  "email":       "[email protected]",
  "scopes":      ["user.id", "user.email"],
  "iat": 1779000000,
  "exp": 1779000180,
  "nonce": "..."
}

必要驗證步驟:

  1. 解密成功(私鑰可解開)。
  2. iss === "https://api.beein.app"。
  3. 驗證 iat/exp 型別與目前時間,簽發最長 180 秒;在到期前以原子操作拒絕重複 nonce。此流程不產生 nbf 欄位。
  4. aud 對應到你的 OA / MiniApp。
  5. sub 是目前驗證的 BeeIn 使用者 ID;目前 handler 不送 personaId,請勿將其列為必填。

完成必要的身分與權限檢查後,再建立自家安全 session cookie;後續跳轉不可依賴首包標頭重送。

解密成功不等於 BeeIn 數位簽章;公鑰加密本身不能驗證發送者。不要單憑此 blob 授權高風險操作。需獨立驗證的帳號登入請使用後端登入交換,再配合自家 session 與權限檢查。

#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
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

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_miniapp 缺 path 等)
429MINIAPP_RATE_LIMITED同官號/追蹤者每小時的共用額度已用完(傳訊息與申請上傳 Token 合計)。

#MiniApp 媒體上傳(3 步驟)

先申請 upload token,再 PUT 原始檔案到回傳的 uploadUrl,最後把回傳 mediaId 帶入 /miniapp/messages。Worker 已在上傳成功時確認媒體;獨立 confirm 端點可在上傳後取回既有結果,不可先於檔案上傳呼叫。

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":   5242880,
  "mimeType":  "image/jpeg"
}

Step 2 — PUT bytes 到 uploadUrl

PUT https://webhook.beein.app/bot/media/upload
Authorization: Bearer <token from step 1>
Content-Type: image/jpeg
Body: <raw bytes>

選用:取回已確認的媒體結果

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 3 — 用 mediaId 發訊息

POST https://webhook.beein.app/miniapp/messages
{
  "contactToken": "eyJ...",
  "type":         "media",
  "mediaId":      "66f0...",
  "text":         "附上您的取貨單"
}
實際限制與官號上傳 MIME/大小表相同,不是路由 schema 的 200 MB 上限。JPEG 範例 maxSize=5242880。PUT 需 Authorization: Bearer upload-token(不是 X-Token),成功即回傳 mediaId。媒體期限與憑證期限不同;更新 Token 不會恢復已過期/刪除的檔案。

#最小可用範例

<!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>
    // Read and remove only ct; preserve other URL parameters.
    const url = new URL(location.href);
    let contactToken = window.beein?.getContactToken?.() || url.searchParams.get('ct');
    url.searchParams.delete('ct');
    history.replaceState(history.state, '', url.pathname + url.search + url.hash);

    window.addEventListener('beein:contactToken', (event) => {
      if (typeof event.detail?.token === 'string') contactToken = event.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) => {
      contactToken = beein.getContactToken?.() || contactToken;
      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 button = document.querySelector('#send');
      button.disabled = true;
      try {
      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 || '發送失敗');
      } catch { alert('連線失敗,請確認網路後再試。'); }
      finally { button.disabled = false; }
    });
  </script>
</body>
</html>

#對接檢查清單

項目負責方必要性
MiniApp 網站部署 HTTPS合作方必做
產生 RSA-2048 key pair,私鑰留在 server合作方必做
OA 管理區貼 URL / 公鑰 / icon / 顯示方式OA 管理員必做
在後端解密 X-BeeIn-Auth,並遵守文件列出的信任檢查。合作方必做
前端支援 beein:ready 與 window.beein合作方建議
讀 ct 並支援 token refresh event合作方要從 MiniApp 發訊息則必做
MiniApp 訊息走 webhook.beein.app/miniapp/*合作方必做
錯誤頁不曝光內部 URL / stack trace合作方必做

安全注意事項

  • contactToken 只放記憶體,不進 DB / log / 第三方分析。
  • 不可把前端 getUser() 當身分證明。MiniApp 加密資料須遵守其信任限制;需驗證登入時使用後端登入交換。
  • 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。

📦

訂單通知 + 收據圖片

訂單成立後,可用 /bot/message 發送文字,或以 oa.card 放入收據連結。MiniApp 內的媒體則使用 contactToken,依 MiniApp 上傳與發訊息流程處理。

🚸

HereLink 接送通知

HereLink 到點後送出已訂閱的 herelink.arrival.triggered Webhook,由你的系統處理。不可把通知等同裝置已接收、GPS 精準度或背景喚醒保證。

🛎️

排程通知 / 提醒

cron job 每天 9am 用 Bot API 發給特定追蹤者:「您今日的待辦」、「藥物提醒」、「會議 30 分鐘前」。一律走 /bot/message。

👥

真人接手 / AI 雙模式

AI webhook 接訊息自動回;管理員按「我來接手」→ webhook 暫停投遞 → 後續訊息只到 admin app;接手結束 → 恢復 webhook 投遞。完全在 BeeIn 後台處理,不用你寫狀態機。

📊

CRM 資料同步

收到追蹤事件後更新 CRM,取消追蹤則標記停用。請處理實際 follower.added/follower.removed 事件名,並定期用追蹤者 API 核對;Webhook 不是保證完整的帳本。

#錯誤碼

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 申請時不同
404OA_NOT_FOLLOWER收件者未追蹤官號、已被封鎖,或查無追蹤關係。
502ORIGIN_UNREACHABLE入口無法連到後端。暫時性錯誤可退避重試;支援冪等鍵的發送請使用同一 key 避免重複。

一般群組機器人

在 BeeIn「設定 → 我的機器人」或「機器人助手」建立以 _bot 結尾的帳號。建立後顯示一次 API Token;可邀請入群、設定權限、輪替 Token、停用或刪除。擁有者帳號完成刪除時,其所有機器人及憑證一併失效。

以下 API 使用 Authorization: Bearer beein_bot_...,與官號的 oa_ Token 不同。Base URL:https://beein.app/api/v1/account-bot。Token 只能保存在你的伺服器環境變數,不能放在前端或訊息內容。

MethodPathInput / Output
GET/meid, userId, username, displayName, commands, webhookUrl, lastDeliveryAt, lastDeliveryError
GET/groupsgroups: [{id, name, role}]
GET/updates?timeout=25&after=EVENT_IDevents, cursor; timeout: 0..25 seconds
PUT/webhook{url: "https://...", signingSecret: "32..128 random characters"} → {ok: true}
DELETE/webhookResume long polling
POST/messagesconversationId, clientMessageId (UUID), text; optional replyToMessageId, discussionId, keyboard, mediaId, mediaType
PATCH/messages/MESSAGE_IDtext, clientMessageId (UUID); bot-owned messages only
POST/callbacks/EVENT_ID/answer{text: "..."}; maximum 200 characters
POST/groups/GROUP_ID/members/USER_ID/kickAdministrator permission required
POST/groups/GROUP_ID/members/USER_ID/mute{until: ISO8601}; at most 30 days
GET/media/context?conversationId=GROUP_IDCurrent attachment encryption requirements
POST/media/upload-urlconversationId, filename, mimeType, size, encryptionMode; encrypted uploads also include encryption
GET/media/MEDIA_ID?conversationId=GROUP_IDAuthorized download URL and encryption manifest

事件含 id, type, conversationId, actorId, createdAt;訊息事件附 message,按鈕事件附 callbackData, messageId, languageCode。預設只接收加入後的指令、提及與回覆;群組管理者可明確授予「讀取群組新訊息」、踢除或禁言權限。沒有私密聊天或歷史訊息讀取權。

Webhook 與長輪詢擇一。輪詢每次最多 50 筆,成功處理後才把 cursor 帶入下一次 after。訊息事件保留 7 天、按鈕事件 1 天。Webhook 採簽章與握手驗證(同本頁 Webhook 規格),失敗會退避重試;必須以事件 id 去重。每節點每分鐘最多 120 次 API 請求;429 時至少等待至下一分鐘。Token 失效為 401,權限不足為 403,狀態或重複 ID 衝突為 409,按鈕過期為 410。

POST /api/v1/account-bot/messages
Authorization: Bearer YOUR_BOT_TOKEN
Content-Type: application/json

{
  "conversationId": "GROUP_OBJECT_ID",
  "clientMessageId": "GENERATE_A_NEW_UUID",
  "text": "請選擇",
  "keyboard": [[
    {"text": "Confirm", "labels": {"zh-TW": "確認"}, "callbackData": "confirm"},
    {"text": "Website", "url": "https://beein.app"}
  ]]
}

keyboard 最多 8 列、每列 3 個按鈕;每個按鈕只能有 callbackData 或 HTTPS url 其中之一。文字上限 80 字,callbackData 上限 128 字。討論串回覆使用 discussionId,僅接受文字;其通知遵守討論串參與者規則。附件先依 media/context 協商加密,上傳完成後以 mediaId 發送,禁止把解密金鑰放在訊息或按鈕中。

最後核對:2026-09-18 · 聯絡我們