Telegraft

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' })
})
One tap converts an unobservable read into a recorded event with a user and a timestamp — which is what the open rate was being used to approximate anyway.

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