← 所有文章
教學

Telegram 媒體附件統一 Webhook:照片、語音、文件、聯絡人與位置

Telegram 官方 Bot API 會把媒體內容放在 Telegram 自己的 Message 欄位裡,例如 photodocumentvoicecontactlocation。如果你的接收層是 UnifyPort 統一 Webhook,就不必把這些 Telegram 專屬欄位直接變成資料庫核心模型。先存 message.received:文字讀 data.message.text,媒體讀 data.message.attachments[],聯絡人讀 data.message.contact,位置讀 data.message.location

重點整理

  • Telegram 媒體訊息不是單純「有文字的訊息」;官方 Telegram Bot API 將照片、文件、語音、聯絡人、位置等內容列為不同的可選欄位。
  • UnifyPort 會把 Telegram 入站內容標準化為 message.received,同一個事件外殼也可用於 WhatsApp、LINE、TikTok、Zalo 與 X。
  • 媒體附件在 data.message.attachments[],常見欄位包含 typeurlmimetype,以及可選的大小或時長資訊。
  • 讀取內容、下載附件或交給 AI 分流之前,先以原始 request body 驗證 X-Device-Signature
  • 若你還在選 Telegram Bot API webhook、getUpdates 或統一入站 Webhook,先看接收路徑比較

Telegram 的原始形狀,和收件匣需要的形狀

Telegram Bot API 的 Message 物件很適合機器人應用程式:你的程式依照 Telegram 欄位分支處理即可。但客服、CRM 或跨通路收件匣通常需要一個可以重複使用的資料合約,而不是只服務 Telegram 的結構。

UnifyPort 的 Standard event types and payload 文件定義了固定外殼:

{
  "id": "evt_b1a7c3e5f8",
  "type": "message.received",
  "provider": "telegram",
  "account_id": "acc_8c21d0",
  "occurred_at": "2026-06-08T12:37:00Z",
  "data": {
    "conversation": { "id": "5005", "type": "user" },
    "sender": { "id": "4004", "type": "user", "name": "Jordan Lee" },
    "message": {
      "id": "3003",
      "direction": "inbound",
      "sent_at": "2026-06-08T12:37:00Z",
      "contact": {
        "phone_number": "+8600000000000",
        "first_name": "Demo",
        "last_name": "User",
        "vcard": "BEGIN:VCARD\nVERSION:3.0\nFN:Demo User\nEND:VCARD",
        "user_id": 4004
      }
    },
    "event": { "kind": "message_received" }
  }
}

照片、語音、影片與文件進入 data.message.attachments[];共享聯絡人進入 data.message.contact;共享位置進入 data.message.location,並以 longitudelatitude 表示座標。

Telegram 媒體的儲存映射

入站內容建議儲存欄位說明
文字或 captiondata.message.text不要要求每則媒體訊息一定有文字。
圖片、音訊、影片、文件、檔案data.message.attachments[]每個附件都有標準 type;媒體 URL 可能是暫時的,應依你的留存政策及早處理。
語音/音訊時長attachments[].duration_ms欄位可能不存在,請當作 optional。
文件標題attachments[].title可供 UI 顯示,不適合當唯一鍵。
聯絡人data.message.contactTelegram 聯絡人可包含電話、姓名、vCard 與 user id。
位置data.message.location經緯度應獨立儲存,不要只靠文字地址。
訊息 IDdata.message.id訊息層級動作用它;Webhook 投遞去重用頂層 id

如果你還沒建立接收端,先照著 Webhook-first 入站整合清單做:建立 Webhook、啟用簽名、存標準事件,再把附件交給後續 worker。

先驗簽,再解析 JSON

啟用 signing_secret 後,UnifyPort 會送出 X-Device-TimestampX-Device-Signature。簽名內容是:

<X-Device-Timestamp>.<raw request body>

Node.js 官方文件說明 crypto.createHmac()crypto.timingSafeEqual();Express 官方文件說明 express.raw() 會把 payload 解析成 Buffer。因此接收端應保留原始 bytes 到驗簽完成。

import crypto from 'node:crypto';
import express from 'express';

const app = express();
const signingSecret = process.env.WEBHOOK_SIGNING_SECRET;

app.post('/webhooks/unifyport', express.raw({ type: 'application/json' }), async (req, res) => {
  const timestamp = req.get('X-Device-Timestamp') ?? '';
  const signature = req.get('X-Device-Signature') ?? '';
  const expected = crypto.createHmac('sha256', signingSecret).update(timestamp + '.').update(req.body).digest();
  const supplied = /^[0-9a-f]{64}$/i.test(signature) ? Buffer.from(signature, 'hex') : Buffer.alloc(0);
  if (supplied.length !== expected.length || !crypto.timingSafeEqual(supplied, expected)) return res.sendStatus(401);

  const event = JSON.parse(req.body.toString('utf8'));
  await storeEvent(event.id, event);
  if (event.type !== 'message.received' || event.provider !== 'telegram') return res.sendStatus(202);

  const message = event.data.message;
  for (const attachment of message.attachments ?? []) {
    await mediaQueue.enqueue({
      eventId: event.id,
      messageId: message.id,
      conversationId: event.data.conversation.id,
      type: attachment.type,
      url: attachment.url,
      mimetype: attachment.mimetype,
      title: attachment.title,
      durationMs: attachment.duration_ms
    });
  }
  if (message.contact) await contactQueue.enqueue(message.contact);
  if (message.location) await geoQueue.enqueue(message.location);
  return res.sendStatus(202);
});

實務上,請把 mediaQueuecontactQueuegeoQueue 換成你的資料庫或工作佇列。重點是先把已驗簽事件持久化,再做慢速下載、索引、OCR 或 AI 摘要。更完整的投遞、重試與排序邊界,請看 Webhook delivery and signature verification;若還要處理表情反應,可搭配統一 Webhook 的 message.reaction 教學

UnifyPort 放在架構哪一層

UnifyPort 提供非官方接口,將已連線訊息帳號上的 Telegram 入站訊息交付為標準事件。它不替你決定媒體保留多久,也不替你決定哪些內部服務能讀附件;這些仍是你的產品與合規設計。

它解決的是接收層的形狀問題:同一個 receiver 今天處理 Telegram 圖片,之後也能處理 WhatsApp 圖片、LINE 照片、Zalo 訊息、TikTok DM 或 X 訊息。

限制與取捨

如果你要做的是 Telegram bot 身分、指令、inline keyboard、BotFather 設定或其他機器人能力,官方 Bot API 是更直接的選擇。統一 Webhook 適合既有訊息帳號入站與多通路客服佇列。也不要假設每個平台都有 Telegram 的所有媒體型別;儲存層可統一,能力判斷仍應放在平台邊界。

FAQ

Telegram 照片會存在 data.message.text 嗎?

不會。文字和 caption 使用 data.message.text;媒體檔案使用 data.message.attachments[]

同一個 Webhook 能收聯絡人和位置嗎?

可以。message.received 可以帶 data.message.contactdata.message.location 這類結構化欄位。

要下載完媒體再回 2xx 嗎?

通常不需要。先存已驗簽事件,把下載與處理排進佇列,再回 2xx。

這和 Telegram Bot API webhook 一樣嗎?

不一樣。Bot API webhook 收的是 bot token 對應的 Telegram Update;UnifyPort webhook 收的是已連接訊息帳號的標準事件。

下一步

打開 Create webhook endpoint,訂閱 message.received,啟用 signing_secret,並分別測試文字、附件、聯絡人和位置訊息。

Sources checked on 2026-09-08

UnifyPort API

讓訊息接入變成一條穩定的產品管線。

先用統一 API 跑通傳送,再用標準事件把所有入站訊息接回業務系統。