Telegram Media Attachments in a Unified Webhook: Photos, Voice, Documents, Contacts, and Locations
Telegram’s official Bot API represents media as Telegram-specific Message fields such as photo, document, voice, contact, and location. If your receiving path is a UnifyPort unified webhook, you do not model each Telegram field directly. Store the message.received envelope, read text from data.message.text, media from data.message.attachments[], contacts from data.message.contact, and coordinates from data.message.location.
Key takeaways
- Telegram media is not just “a message with text”; the official Bot API documents separate optional fields for photos, documents, voice messages, contacts, and locations.
- UnifyPort normalizes inbound Telegram content into the same
message.receivedenvelope used across WhatsApp, LINE, TikTok, Zalo, and X. - Media files appear under
data.message.attachments[]with fields such astype,url,mimetype, and optional size or duration metadata. - Verify
X-Device-Signatureagainst the raw request body before reading any message content. - Pair this implementation with the Telegram Bot API webhook vs unified inbound webhook decision guide if you are still choosing the receiving path.
What Telegram sends vs what your inbox should store
The official Telegram Bot API defines a Message object with many optional content fields. That is correct for a Telegram bot: your code branches on Telegram’s own shape.
A shared support inbox usually has a different goal. It wants one storage contract that can accept Telegram today and LINE, WhatsApp, Zalo, TikTok, or X tomorrow. That is why the first production record should be the UnifyPort standard event envelope documented in 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" }
}
}
If the incoming content is a photo, voice note, video, or document, use data.message.attachments[]. If it is a shared contact, use data.message.contact. If it is a shared location, use data.message.location with longitude and latitude.
Storage map for Telegram media
| Incoming content | UnifyPort field to store | Notes |
|---|---|---|
| Text or caption | data.message.text | Captions can sit beside attachments. Do not require text for every message. |
| Photo, image, audio, video, document, file | data.message.attachments[] | Each item carries a normalized type; media URLs can be temporary, so copy or process them promptly according to your retention rules. |
| Voice/audio duration | attachments[].duration_ms when present | Treat duration as optional. |
| Document title | attachments[].title when present | Use it for display, not as a unique identifier. |
| Shared contact | data.message.contact | Telegram contact payloads can include phone, names, vCard, and user id. |
| Location | data.message.location | Store { longitude, latitude } separately from free-text address fields. |
| Message identity | data.message.id | Use this for message-level actions or projections; use top-level id for delivery idempotency. |
This is the implementation-focused companion to the broader webhook-first inbound checklist. Create the receiver first, store the normalized event, and only then fan out to search indexing, CRM, AI triage, or file processing.
Verify first, parse second
The delivery docs define X-Device-Timestamp and X-Device-Signature. When signing_secret is enabled, the signature is a hex HMAC-SHA256 over:
<X-Device-Timestamp>.<raw request body>
Use raw-body middleware, then parse JSON only after verification. Node’s official crypto docs cover crypto.createHmac() and crypto.timingSafeEqual(), while the Express docs describe express.raw() as the built-in middleware that parses payloads into a Buffer.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const signingSecret = process.env.WEBHOOK_SIGNING_SECRET;
if (!signingSecret) throw new Error('WEBHOOK_SIGNING_SECRET is required');
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);
});
Replace the sample queues with your database or worker system. The important boundary is that the event is durably stored before slow media downloads or AI enrichment run. For replay protection and retry details, use the deeper Webhook delivery and signature verification guide and the HMAC replay-protection tutorial.
Where UnifyPort fits
UnifyPort gives an unofficial interface for inbound Telegram messages from a connected messaging account and delivers them as normalized events. You still decide how long to retain media, whether to copy temporary file URLs, and which workers can access attachments.
The payoff is schema reuse. The same receiver that handles Telegram message.received can later accept WhatsApp images, LINE photos, Zalo support messages, TikTok DMs, or X messages without making Telegram’s Message object the center of your database.
Limitations and trade-offs
Use the official Bot API when you are building a Telegram bot identity, need bot commands, inline keyboards, BotFather configuration, or Telegram-specific bot behavior. Use the unified webhook path when the job is inbound intake for an existing messaging account or a cross-channel queue.
Also avoid assuming that every provider has every Telegram media shape. Normalize the storage layer, but keep provider-specific capability checks at the edges.
FAQ
Are Telegram photos stored in data.message.text?
No. Text and captions use data.message.text; media files use data.message.attachments[] with a normalized attachment type.
Can the same webhook receive Telegram contacts and locations?
Yes. A message.received event can carry structured fields such as data.message.contact or data.message.location when the incoming message includes that content.
Should I download media before returning 2xx?
Usually no. Store the verified event first, enqueue media work, then return 2xx. Slow downloads should not block acknowledgement.
Is this the same as a Telegram Bot API webhook?
No. A Telegram Bot API webhook receives Telegram Update objects for a bot token. A UnifyPort webhook receives normalized events for a connected messaging account and can share the same receiver across providers.
Next step
Open the Create webhook endpoint reference, subscribe to message.received, enable signing_secret, and test text, attachment, contact, and location messages before connecting the handler to production workflows.
Sources checked on 2026-09-08
- Telegram Bot API: https://core.telegram.org/bots/api
- Node.js Crypto documentation: https://nodejs.org/api/crypto.html
- Express middleware documentation: https://expressjs.com/en/5x/guide/using-middleware
- UnifyPort Standard event types and payload: /docs/receiving-events/webhook-events/
- UnifyPort Webhook delivery and signature verification: /docs/receiving-events/webhook-delivery/
Turn messaging integration into a stable product pipeline.
Start by sending through one API, then bring every inbound message back into your business system with standard events.