Menu QaldyDocs

Telegram

Send messages to a chat or channel through your own Telegram bot.

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

  1. In Telegram, open @BotFather → /newbot → pick a name and a username ending in bot.
  2. BotFather replies with a token like 7712345678:AAH…. Copy the whole line.
  3. 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.
  4. Press Test connection. The gate calls getMe with the token: "Works. Telegram answered 200 in … ms", or "Telegram refused the key (401)" (a mistyped or revoked token: replace it).
  5. Add the bot to the chat or channel the app should post to (for a channel, as an admin).
  6. Get the chat id: send any message in that chat, then open https://api.telegram.org/bot<token>/getUpdates in your own browser and read chat.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.

ts
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

ActionWhat it doesInput
sendMessageA text message, with an optional inline keyboard{ chatId, text, silent?, inlineKeyboard? }
sendPhotoA photo by https URL{ chatId, photoUrl, caption?, silent?, inlineKeyboard? }
sendDocumentA file by https URL{ chatId, documentUrl, caption?, silent?, inlineKeyboard? }
editMessageTextChanges the text (and buttons) of a message the bot sent{ chatId, messageId, text, inlineKeyboard? }
answerCallbackQueryAnswers 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.

ts
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, reason action_not_supported. setWebhook or getUpdates could 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 https on port 443 with a host on the app's egress allowlist (the manifest lists the domain and a person approves it): otherwise 403 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)

  1. Connect the bot on Connectors → Telegram (above).
  2. Settings → Telegram → Turn on Telegram, pick the connection. Qaldy asks the bot who it is (getMe), then sets its webhook to https://<api>/v1/telegram/<id> with a fresh secret_token. The token stays sealed at the gate throughout; the API keeps only a peppered digest of the secret_token.
  3. Check delivery asks Telegram how deliveries are going (pending updates, the last error). Turn off Telegram removes the webhook; people stay linked.
  1. Settings → Telegram → Link my Telegram shows a one-time code (QLD- and eight characters, ten minutes, once) and an Open in Telegram link.
  2. 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.
  3. 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, with via: telegram in 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:

CommandAnswers
/approvalswhat is waiting for you, as messages with the buttons
/runsthe 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")
/capthis 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
/unlinkstop 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 seeWhyDo
Needs attention, "Telegram would not take the address"The address is not https or not reachable from the internetFix QALDY_API_PUBLIC_URL, then Turn on again
Check delivery shows pending updates and a 401The webhook secret changed (Telegram still has an old one)Turn on again
"This bot already sends its messages to …"The bot has another webhookUse a separate bot, or confirm the replacement
The bot says "isn't linked"That Telegram account never sent a codeLink it in Settings
The bot does not answer in a groupA bot in a group only sees commands, and answers /alerts onlySend /alerts

When it breaks

You seeWhyDo
Expired — reconnectTelegram answered 401/404 to a testReplace the key with a fresh token from @BotFather (/token)
chat not found from the appThe bot isn't in that chat, or the id is wrongAdd the bot, re-read chat.id
connector_not_connectedNo token storedConnect it on /connectors
Blocked: action_not_declaredThe app's manifest doesn't list sendMessageAdd 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.

Type to search. Up and down to choose, Enter to open, Esc to close.