Telegram
Send messages to a chat or channel through your own Telegram bot.
- Status: Available
- Shelf: Messaging
- How you connect: Bot token
- Regions: global and Kazakhstan
- The service’s own API reference: https://core.telegram.org/bots/api
Apps send messages to a chat or channel through your own bot. The gate calls
api.telegram.org with the token; app code only ever sees { messageId }.
Connect it in two minutes#
- In Telegram, open @BotFather →
/newbot→ pick a name and a username ending inbot. - BotFather replies with a token like
7712345678:AAH…. Copy the whole line. - Qaldy → Connectors → Telegram → Connect (or Replace the key) → paste it → Seal and connect. It is sealed to the gate's key and never shown again.
- Press Test connection. The gate calls
getMewith the token: "Works. Telegram answered 200 in … ms", or "Telegram refused the key (401)" (a mistyped or revoked token: replace it). - Add the bot to the chat or channel the app should post to (for a channel, as an admin).
- Get the chat id: send any message in that chat, then open
https://api.telegram.org/bot<token>/getUpdatesin your own browser and readchat.id(groups and channels are negative, like-1001234567890). Do this on your machine only; the token is in that address.
Use it from an app#
Manifest: { "id": "telegram", "actions": ["sendMessage"], "scope": "#roastery" } (the scope is
the human name shown in the permission sentence). List only the actions the app calls.
await ctx.connectors.telegram.sendMessage({ chatId: '-1001234567890', text: 'Beans below 5 kg' })The gate forwards the Bot API body (chat_id, text, disable_notification, reply_markup) to
POST https://api.telegram.org/bot<token>/sendMessage and replaces the token with [redacted] in
the reply, including its percent-encoded form.
Actions#
| Action | What it does | Input |
|---|---|---|
sendMessage | A text message, with an optional inline keyboard | { chatId, text, silent?, inlineKeyboard? } |
sendPhoto | A photo by https URL | { chatId, photoUrl, caption?, silent?, inlineKeyboard? } |
sendDocument | A file by https URL | { chatId, documentUrl, caption?, silent?, inlineKeyboard? } |
editMessageText | Changes the text (and buttons) of a message the bot sent | { chatId, messageId, text, inlineKeyboard? } |
answerCallbackQuery | Answers a button press | { callbackQueryId, text?, showAlert? } |
All five are writes. The SDK checks Telegram's own limits before calling (text ≤ 4096 and caption
≤ 1024 characters, ≤ 8 buttons a row and 100 in all, callbackData 1 to 64 bytes, an https
url, exactly one of the two per button), so a bad keyboard fails with invalid_input and
nothing is sent.
const sent = await ctx.connectors.telegram.sendMessage({
chatId,
text: 'Approve order 42?',
inlineKeyboard: [
[
{ text: 'Approve', callbackData: 'ok:42' },
{ text: 'Reject', callbackData: 'no:42' },
],
[{ text: 'Open the order', url: 'https://app.example.com/orders/42' }],
],
})
await ctx.connectors.telegram.editMessageText({
chatId,
messageId: sent.messageId,
text: 'Order 42 approved',
})A person's press arrives as a callback_query update with an id; the app answers it with
answerCallbackQuery({ callbackQueryId }). (Receiving updates in an app, the webhook trigger, is planned.
Qaldy's own approvals do receive them: see "Approvals and alerts in Telegram" below.)
What the gate holds Telegram to#
- Only these five Bot API methods are forwarded. A policy that lists any other (
getUpdates,setWebhook,deleteMessage,banChatMember…) is still refused:403 egress_blocked, reasonaction_not_supported.setWebhookorgetUpdatescould redirect or read the bot's whole inbox. - A photo or document URL is egress. Telegram's own servers fetch it, outside the gate's reach,
so it could carry data to any host. The gate therefore requires the URL to be
httpson port 443 with a host on the app's egress allowlist (the manifest lists the domain and a person approves it): otherwise403 egress_blocked(domain_not_allowed, the host named in the audit row, the URL never recorded).attach://…, plain http, IP literals, userinfo and non-string values are refused (media_not_allowed). A Telegram file id (an earlier upload) passes: nothing is fetched. The gate forwards the body it parsed (re-serialised), so a key sent twice can't slip a second, unchecked URL past it. - Message text can still send data to the chat the app names; that is what the connector is for, and the manifest sentence says which chat.
Approvals and alerts in Telegram (Preview)#
Qaldy can use the same bot to put your team's approvals on a phone, send alerts to a chat and answer a few questions. Preview: built against Telegram's Bot API documentation and a fake Bot API in tests; the first run against a live bot may need a fix. Turn it on in Settings → Telegram.
Use a bot of its own for this. A bot has exactly one webhook. Qaldy sets it so Telegram sends messages and button presses to your workspace; if the bot already sends somewhere else (an automation's Telegram trigger, n8n, another tool), Qaldy says so and asks before replacing that. Make a second bot with @BotFather and connect it as a second Telegram connection.
Turn it on (an admin)#
- Connect the bot on Connectors → Telegram (above).
- Settings → Telegram → Turn on Telegram, pick the connection. Qaldy asks the bot who it is
(
getMe), then sets its webhook tohttps://<api>/v1/telegram/<id>with a freshsecret_token. The token stays sealed at the gate throughout; the API keeps only a peppered digest of thesecret_token. - Check delivery asks Telegram how deliveries are going (pending updates, the last error). Turn off Telegram removes the webhook; people stay linked.
Link your account (everyone)#
- Settings → Telegram → Link my Telegram shows a one-time code (
QLD-and eight characters, ten minutes, once) and an Open in Telegram link. - Press Start in the bot's chat (the link sends
/start <code>for you), or send the code in a private chat. A code posted in a group is cancelled on sight, because anyone there could use it. - The page notices by itself. Send me a test approval sends a message shaped like a real one; its buttons go through every check a real press does, then decide nothing and say so. Unlink removes the link.
A Telegram account is one person per workspace. Linking proves one thing: whoever sent the code is
whoever held the console session that made it. From then on the bot knows your messages by
Telegram's own from.id, and asks Qaldy fresh, every time, whether you are still a member.
Approvals#
When an approval is asked of you, the bot sends a message with the approval's title, the app, who asked, what the run already did and what happens next if you approve, with Approve and Reject buttons and a link to the run. Pressing a button:
- is accepted only from the Telegram account linked to the person it was asked of, in the chat the message was sent to, on a message Qaldy sent (the button data is signed for the workspace);
- decides the approval exactly as a click in the console would (
approval.decide, withvia: telegramin the audit row), and the waiting run carries on; - is safe to press twice, or after someone decided in the console: the second answer is "already approved/rejected". The message is rewritten to say how it ended and loses its buttons, whoever decided and however it ended (also when it ran out of time).
A message that could not be sent is retried a minute later, three attempts in all. The approval itself is never touched by a failed send: it can always be decided in the console.
Commands#
In your private chat with the bot, answered with your own permissions:
| Command | Answers |
|---|---|
/approvals | what is waiting for you, as messages with the buttons |
/runs | the latest runs of the apps you can open |
/status <app> | how one app is doing and when it last ran (apps you cannot open are "not found") |
/cap | this month's use against your plan; spend figures only for admins |
/alerts | (admins) send the workspace's alerts to this chat; /alerts off stops it |
/unlink | stop using this chat for Qaldy |
In a group the bot answers /alerts (from a linked admin) and nothing else. The bot answers in the
language you linked in (English, Russian or Kazakh) and writes plain text only.
Alerts and the digest#
Every alert rule keeps its own places. The workspace's ops chat hears them as well: error-rate,
quiet and budget alerts, and heartbeat alerts for automations. Choose it by sending /alerts in a
chat the bot is in (an admin, in a group or in your own chat), or type a chat id or @channel in
Settings (the bot writes a test message first, and nothing is saved if it cannot). Send the weekly
digest there too adds a short version of the Monday digest; the email is unchanged. Anyone in the
ops chat can read these alerts, so choose it deliberately.
When it breaks#
| You see | Why | Do |
|---|---|---|
| Needs attention, "Telegram would not take the address" | The address is not https or not reachable from the internet | Fix QALDY_API_PUBLIC_URL, then Turn on again |
| Check delivery shows pending updates and a 401 | The webhook secret changed (Telegram still has an old one) | Turn on again |
| "This bot already sends its messages to …" | The bot has another webhook | Use a separate bot, or confirm the replacement |
| The bot says "isn't linked" | That Telegram account never sent a code | Link it in Settings |
| The bot does not answer in a group | A bot in a group only sees commands, and answers /alerts only | Send /alerts |
When it breaks#
| You see | Why | Do |
|---|---|---|
| Expired — reconnect | Telegram answered 401/404 to a test | Replace the key with a fresh token from @BotFather (/token) |
chat not found from the app | The bot isn't in that chat, or the id is wrong | Add the bot, re-read chat.id |
connector_not_connected | No token stored | Connect it on /connectors |
Blocked: action_not_declared | The app's manifest doesn't list sendMessage | Add it and publish a new version |
The City shows every call as a parcel between the app and the Telegram gate; failures and blocks drop the gate's barrier.