Bot API reference
What a successful send actually proves
A successful `sendMessage` returns the created `Message` object, which confirms Telegram accepted and stored it. It does not confirm that the recipient's device received it, that the chat was opened, or that anyone read it. The Bot API exposes no read receipts and no per-recipient delivery status.
No delivery or read receipts: the exact figures
- Read receipts
- None. Not exposed by the Bot API
- Delivery receipts
- None per recipient. A 200 means Telegram accepted it
- Observable signals
- Button taps, replies, deep-link payloads
- Explicit failure
- Blocked-by-user, returned as a catchable error
As of 2025-10-01, Telegram Bot API 13.4
What it means in practice
This is a deliberate privacy property rather than a gap, and it is worth stating plainly because it contradicts what stakeholders expect from a messaging channel. Anyone who has bought SMS or email campaigns arrives expecting a delivered rate and an open rate. Neither exists here. A vendor dashboard showing "98% delivered" for a Telegram broadcast is reporting how many API calls returned 200, which is a different quantity wearing a familiar name.
What a bot can observe is interaction, and it is more honest than an open rate. A tapped inline button produces a `callback_query` naming the user and the message. A deep link with a payload records which entry point was used. A reply is unambiguous. These are real signals of attention rather than proxies for it, and for most products they answer the question the open rate was standing in for.
The one explicit negative signal is a block. Sending to a user who has blocked the bot fails with a specific, catchable error, which means the subscriber list is self-cleaning in a way an email list is not — there is no soft bounce and no ambiguity. Recording that error against the subscriber and stopping future sends is both good practice and the closest thing to a delivery signal the platform offers.
Designing around the absence usually improves the product. If a message must be acknowledged, ask for the acknowledgement: a single inline button turns an unknowable read into a recorded event, and takes the recipient one tap. Where an action must be confirmed, the confirmation is a tap rather than an inference. Where escalation depends on someone having seen a notification, the escalation should trigger on the absence of a tap rather than on a guessed delivery window.
One further constraint follows and is easy to overlook. Because there is no read state, "unread count" style features cannot be built from the platform. A bot can track what it has sent and what has been interacted with, and everything else is inference. Promising a stakeholder anything more than that is promising something the API does not expose.
Handling it in code
// The honest engagement signal: an explicit acknowledgement, not an inferred read.
await bot.api.sendMessage(chatId, 'Your delivery is 10 minutes away.', {
reply_markup: {
inline_keyboard: [[{ text: 'Got it', callback_data: 'ack:' + jobId }]],
},
})
bot.on('callback_query:data', async (ctx) => {
const [kind, id] = (ctx.callbackQuery.data ?? '').split(':')
if (kind !== 'ack' || id === undefined) return
await recordAcknowledged(id, ctx.from.id)
// Answering closes the client-side spinner. Skipping it leaves the button
// looking broken for several seconds, which reads as an unresponsive bot.
await ctx.answerCallbackQuery({ text: 'Thanks' })
})Questions this raises
Can I tell whether a specific user opened the chat?
No. There is no per-user read state available to a bot, by design. The only way to know a message was seen is for the user to do something — tap a button, reply, follow a link — which is why an acknowledgement button earns its place on anything that matters.
What does a "delivered" figure in a broadcast tool mean, then?
It means the API accepted the send. That is a useful number — it excludes blocked users and failed calls — but it is not delivery in the sense an SMS report means it, and presenting it as such sets an expectation the platform cannot meet.
Is there any way to know a user is inactive?
Only through what they do not do. A subscriber who has never tapped anything across many campaigns is probably not reading, and that is inference rather than measurement. The one definitive signal is a block, which is reported explicitly and should immediately stop further sends.
Does the message object returned by sendMessage help at all?
It gives you the `message_id`, which is what you need to edit or delete the message later, and a timestamp for when Telegram accepted it. Both are useful for operating the bot. Neither says anything about the recipient.
Related limits
The single explicit delivery failure the API does report is documented in blocked-by-user errors.
How escalation is designed when delivery cannot be confirmed is set out in the dispatch build.