BMH DocsDeveloper documentation
Telegram 10.3GitHub

BMH Bot SDK · Telegram

Build Telegram bots in code and Visual Flow.

A typed SDK that keeps code and canvas synchronized, while still exposing every Telegram Bot API method when you need lower-level control.

Visual when useful

Move between Code and Flow without maintaining two different bot definitions.

Typed escape hatch

Use concise helpers or call any documented Bot API method directly.

Portable runtime

Use Web Standard requests, responses, FormData, and AbortSignal locally.

185Bot API methods
27Update types
10.3Telegram Bot API
2Delivery modes

Start

Quick start

Create a command, reply to a user, and add an inline button.
Hosted projects need no package installation.

The editor already resolves @bmhien/bot. For local development, install the public package with npm install @bmhien/bot. The SDK repository also accepts direct installs from its main branch.

main.ts
import { Bot } from "@bmhien/bot";

const bot = new Bot();

bot.command("start", async (ctx) => {
  await ctx.reply("Welcome to my bot 👋");
  await ctx.telegram.sendButtons("Choose an action", [
    { text: "Continue", data: "continue" },
  ]);
});

bot.callback("continue", async (ctx) => {
  await ctx.telegram.answerCallback("Done");
});

export default bot;
  1. 1
    Create the bot

    Declare one Bot instance and export it as the default value.

  2. 2
    Register handlers

    Use command, callback, or on for Telegram updates.

  3. 3
    Save and inspect the flow

    Supported statements appear as nodes immediately; diagnostics stay attached to their source block.

Start

Project structure

New projects begin with three editable files. Folders are optional and fully configurable.
BOT PROJECT3
main.tsEntrypoint + flow
bot.config.jsonProject config
README.mdYour notes

Use any layout you want

Move files or create folders from Explorer, then point entrypoint and visual_flow at the correct file. Resolvable relative imports are rewritten when a module or folder moves. Generated declarations live under the hidden, read-only .bmh directory.

bot.config.json
{
  "framework": "@bmhien/bot",
  "version": 1,
  "compatibility_date": "2026-08-25",
  "platform": "telegram",
  "entrypoint": "main.ts",
  "visual_flow": "main.ts",
  "runtime": { "delivery": "webhook" },
  "project": {
    "include": ["**/*"],
    "exclude": ["node_modules/**", "dist/**"]
  }
}
FieldPurpose
entrypoint

File used to start the bot.

visual_flow

Source file synchronized with the canvas. It may be the same as the entrypoint.

project.include

Glob patterns for editable project files.

project.exclude

Patterns ignored by the project and editor.

Build

Events & commands

Register explicit handlers. A Telegram update contains at most one update payload.
bot.command(name, handler)

Runs for a slash command. A leading slash is optional and bot mentions are normalized.

bot.callback(data, handler)

Runs for inline keyboard callback data. Use an empty string as a catch-all.

bot.on(updateType, handler)

Runs for any of the 27 documented Telegram update types.

Event handlers
bot.command("help", (ctx) => ctx.reply("How can I help?"));

bot.callback("confirm", async (ctx) => {
  await ctx.telegram.answerCallback("Confirmed");
  await ctx.telegram.editMessage("✅ Confirmed");
});

bot.on("chat_member", async (ctx) => {
  await ctx.reply("Membership changed");
});
Not every update has a destination chat.

Poll answers, inline queries, shipping queries, pre-checkout queries, paid-media purchases, and managed-bot updates identify an actor but do not guarantee a chat. Use answerInlineQuery(), answerShippingQuery(), or answerPreCheckoutQuery() for their matching query lifecycle instead of ctx.reply().

Build

Context & templates

Handlers receive one normalized context so common code does not depend on raw Telegram payload shapes.
PropertyDescription
ctx.update

The original typed Telegram update.

ctx.updateType

The update field that was present, such as message or callback_query.

ctx.text

Normalized message text, caption, query, callback data, or poll question.

ctx.chatId

Available only when Telegram supplies a real destination chat.

ctx.userId

The actor ID. It is intentionally not treated as a chat ID.

ctx.messageId

Current message identifier when the update includes a message.

ctx.ephemeralMessageId

Current per-receiver ephemeral message identifier, when supplied by Telegram.

ctx.draftId

The stopped draft identifier supplied by stopped_message_generation.

ctx.businessConnectionId

Current Telegram Business connection, inherited automatically by checklist helpers.

ctx.callbackQueryId

Required by answerCallback().

ctx.inlineQueryId

Identifier used automatically by answerInlineQuery().

ctx.shippingQueryId

Identifier used automatically by answerShippingQuery().

ctx.preCheckoutQueryId

Identifier used automatically by answerPreCheckoutQuery().

ctx.guestQueryId

Identifier used automatically by answerGuestQuery().

ctx.successfulPayment

Normalized completed invoice with payload, amount, recurring state, and durable charge IDs.

ctx.messageThreadId

Current forum topic thread, used automatically by topic lifecycle helpers.

ctx.telegram

Telegram helpers and the complete typed method escape hatch.

Canvas template values

Visual node fields can reference normalized values. Unknown or unavailable values render as an empty string.

{{update.type}}{{update.id}}{{message.text}}{{message.id}}{{message.ephemeral_id}}{{message.thread_id}}{{user.id}}{{user.first_name}}{{user.username}}{{chat.id}}{{chat.title}}{{callback.data}}{{poll.id}}{{business.connection_id}}{{payment.payload}}{{payment.currency}}{{payment.total_amount}}{{payment.telegram_charge_id}}{{payment.provider_charge_id}}{{payment.subscription_expiration_date}}{{inline.query_id}}{{shipping.query_id}}{{payment.pre_checkout_query_id}}{{guest.query_id}}{{draft.id}}

Named action results

HTTP requests, generic Telegram API calls, forum-topic creation, and Business checklist actions can save their returned value for every later block on the same trigger path. The same syntax works inside a reusable defineFlowFunction() module.

Capture and reuse output
const topic = await ctx.telegram.createForumTopic("Support");
await ctx.telegram.closeForumTopic(
  "{{result.topic.message_thread_id}}",
);

const lookup = await ctx.http.request("https://api.example.com/user", {
  method: "GET",
});
await ctx.reply("Status {{result.lookup.status}}: {{result.lookup.body.name}}");
Results are scoped and bounded.

Each matching trigger receives an isolated result scope. HTTP captures expose status, ok, and body; Telegram captures expose the returned Bot API object directly. createForumTopic() also returns a typed TelegramForumTopic in local SDK code. Names are safe identifiers up to 32 characters. Captures are limited to 64 KB, five nested levels, 100 fields, and 4,096 characters per template value.

Core SDK

Reusable flow functions

Compose a large bot from named project modules without losing Code ↔ Flow synchronization.
flows/welcome.ts
import { defineFlowFunction } from "@bmhien/bot";

export const welcome = defineFlowFunction(
  "Welcome sequence",
  async (ctx) => {
    await ctx.telegram.sendChatAction("typing");
    await ctx.reply("Welcome to BMH");
  },
);
main.ts
import { Bot } from "@bmhien/bot";
import { welcome } from "./flows/welcome";

const bot = new Bot();
bot.command("start", async (ctx) => {
  await ctx.run(welcome);
});

export default bot;
Project functions palette

Every valid named export appears as a draggable block with its module and function name.

Safe module moves

Explorer rewrites resolvable static and dynamic relative imports when files or folders move.

Bounded composition

Cycles, unresolved exports, more than 20 nested levels, and more than 200 compiled steps are rejected.

Propagated diagnostics

Chat, callback, query-answer, exactly-once, and terminal-action conflicts inside a function surface on its canvas node.

Use named relative imports for visual modules.

The synchronized subset discovers export const name = defineFlowFunction(...). Imports may pass through an index.ts barrel using a named alias or export *; generated code preserves that public import while cycles and ambiguous exports are rejected. Unrestricted application code can remain in other files and use the complete SDK normally.

Core SDK

Telegram client

Use the low-level client when running locally or when a specialist Bot API method does not need a dedicated helper.

Public exports

ExportPurpose
Bot

Registers commands, update listeners, and callback-query handlers.

TelegramClient

Sends validated JSON or multipart requests to the Telegram Bot API.

createTelegramWebhookHandler

Turns a bot and client into a Web Standard request handler.

runTelegramPolling

Runs cancellable long polling with offset and retry handling.

dispatchTelegramUpdate

Normalizes and dispatches one already-parsed update.

createTelegramContext

Builds the normalized context used by handlers.

defineFlowFunction

Declares a reusable project function that runs through ctx.run().

TELEGRAM_METHODS

Readonly catalog of all 185 recognized method names.

TELEGRAM_UPDATE_TYPES

Readonly catalog of all 27 recognized update types.

TELEGRAM_CHAT_PERMISSION_FIELDS

Readonly whitelist for current Telegram chat permission keys.

TELEGRAM_ADMINISTRATOR_RIGHT_FIELDS

Readonly whitelist for current administrator-right keys.

Constructor and request options

client.ts
const telegram = new TelegramClient(token, {
  apiRoot: "https://api.telegram.org", // optional
  fetch: globalThis.fetch,                 // optional
});

const me = await telegram.call("getMe", {}, {
  signal: abortController.signal,
});
call(method, params, options?)

Sends JSON and returns Telegram's unwrapped result.

upload(method, formData, options?)

Sends multipart data without overriding the generated boundary.

getFileUrl(fileId)

Resolves a downloadable file URL after calling getFile.

A file URL contains the bot token.

Treat the value returned by getFileUrl() as a credential: do not persist it, log it, or send it to an untrusted client.

Build

Telegram helpers

Common operations use concise methods and map cleanly to visual nodes.
HelperWhat it does
ctx.reply(text)

Send a text message to the current chat.

sendButtons(text, buttons)

Send one inline keyboard row per button.

sendPhoto / sendDocument

Send media by file ID or supported URL with an optional caption.

sendAudio / sendVideo / sendAnimation

Send common media formats.

sendVoice / sendVideoNote

Send Telegram voice and video-note messages.

sendLocation / sendVenue / sendContact

Send structured location or contact data.

sendPoll / sendDice

Create a poll or send a Telegram dice animation.

sendMessageDraft

Stream a temporary partial response in a private chat, with an optional Stop button.

editEphemeralMessageText / editEphemeralRichMessage

Replace per-receiver ephemeral text or structured rich content.

editEphemeralMessageMedia / editEphemeralMessageCaption

Replace ephemeral media or update its caption.

editEphemeralMessageReplyMarkup / deleteEphemeralMessage

Replace/remove buttons or delete the current ephemeral message.

sendChecklist / editChecklist

Create or replace a validated Telegram Business checklist with 1–30 tasks.

getAvailableGifts / sendGift

Read the gift catalog and send a validated gift to one user or channel.

giftPremiumSubscription

Gift exactly 3, 6, or 12 months of Telegram Premium at the matching Stars price.

getUserGifts / getChatGifts

List owned gifts for the current or explicitly selected user or chat.

getBusinessAccountGifts / getBusinessAccountStarBalance

Read typed gift inventory and Stars balance for the current Business connection.

convertGiftToStars / upgradeGift / transferGift

Manage the lifecycle and ownership of a Business account's gifts.

editMessage / deleteMessage

Modify or remove the current/configured message.

deleteMessages

Delete 1-100 unique message IDs with one validated request.

setReaction

Set an emoji reaction on a message.

forwardMessage / copyMessage

Bring a source message into the current chat.

pinMessage / unpinMessage

Manage pinned messages in the current chat.

unpinAllMessages

Clear every pinned message in the current chat.

banMember / unbanMember / restrictMember

Moderate a member with validated IDs, permissions, and expiry timestamps.

promoteMember / setAdministratorTitle

Manage administrator rights and custom titles.

setMemberTag / setDefaultPermissions

Manage regular member tags and chat-wide defaults.

approveJoinRequest / declineJoinRequest

Resolve a pending request exactly once on a compatible trigger path.

answerInlineQuery

Return 0-50 validated results with caching, pagination, and an optional results button.

answerShippingQuery

Return shipping options when accepted, or a required customer-facing error when rejected.

answerPreCheckoutQuery

Approve checkout or reject it with a required error message.

answerGuestQuery

Return one inline result as the guest-mode reply.

sendInvoice

Send a validated Stars or provider-backed invoice to the current chat.

refundStarPayment

Refund a Stars charge, defaulting to the current completed payment context.

editStarSubscription

Cancel or restore renewal using the current completed payment context.

createForumTopic / editForumTopic

Create, rename, recolor, or remove the custom icon of a forum topic.

closeForumTopic / reopenForumTopic

Change the current or explicitly selected topic lifecycle.

unpinAllForumTopicMessages

Clear pinned messages inside the current topic.

deleteForumTopic

Delete a topic and all messages inside it.

leaveChat

Leave the current chat and terminate the active flow path.

sendChatAction

Show typing, upload, recording, or location activity.

Call any Bot API method

Typed generic API
await ctx.telegram.call("banChatMember", {
  chat_id: -1001234567890,
  user_id: 123456789,
  revoke_messages: true,
});

In hosted flows, delivery lifecycle methods—getUpdates, setWebhook, deleteWebhook, logOut, and close—are reserved for the runtime.

Core SDK

Common recipes

Small, complete patterns for the workflows most bots need first.

Inline button and callback acknowledgement

Always answer a callback query promptly so Telegram can dismiss the loading indicator.

buttons.ts
bot.command("confirm", (ctx) =>
  ctx.telegram.sendButtons("Continue?", [
    { text: "Yes", data: "confirm:yes" },
  ])
);

bot.callback("confirm:yes", async (ctx) => {
  await ctx.telegram.answerCallback("Saved");
  await ctx.telegram.editMessage("✅ Confirmed");
});

Chat action around slow work

activity.ts
bot.on("message", async (ctx) => {
  await ctx.telegram.sendChatAction("typing");
  const result = await createAnswer(ctx.text ?? "");
  await ctx.reply(result);
});

Stream generated output

A message draft is a temporary private-chat preview. Reuse the same non-zero draft ID as output changes, then send the completed message so it persists.

streaming.ts
bot.on("message", async (ctx) => {
  await ctx.telegram.sendMessageDraft(1, "Thinking…", {
    canStop: true,
    keepOnStop: true,
  });
  const answer = await createAnswer(ctx.text ?? "");
  await ctx.reply(answer); // persists the final output
});

bot.on("stopped_message_generation", async (ctx) => {
  await cancelGeneration(ctx.draftId);
});
Drafts expire after about 30 seconds.

They are available only in private chats and are not durable messages. Always call ctx.reply() or sendMessage with the final text.

Edit and delete ephemeral messages

Telegram ephemeral messages use a receiver ID and a separate ephemeral_message_id. The SDK inherits both from the current update, or accepts an explicit target saved from an earlier result.

ephemeral-lifecycle.ts
bot.on("message", async (ctx) => {
  await ctx.telegram.editEphemeralMessageText("Updated");
  await ctx.telegram.editEphemeralRichMessage({ markdown: "**Ready**" });
  await ctx.telegram.editEphemeralMessageMedia({
    type: "photo",
    media: "https://example.com/result.jpg",
    caption: "Result",
  });
  await ctx.telegram.editEphemeralMessageCaption("Final caption");
  await ctx.telegram.editEphemeralMessageReplyMarkup(); // removes buttons
  await ctx.telegram.deleteEphemeralMessage();
});
The lifecycle is validated before delivery.

Targets must use positive IDs. Text and caption limits, InputMedia types, entity/parse-mode exclusivity, callback-data bytes, exact button actions, and Telegram's ban on login_url for ephemeral keyboards are enforced locally.

Telegram Business checklist

Business-message updates expose their connection automatically. A named return value lets the next block edit the exact message that was created.

business-checklist.ts
bot.on("business_message", async (ctx) => {
  const checklistMessage = await ctx.telegram.sendChecklist({
    title: "Launch tasks",
    tasks: [
      { id: 1, text: "Review" },
      { id: 2, text: "Ship" },
    ],
    othersCanMarkTasksAsDone: true,
  });

  await ctx.telegram.editChecklist({
    title: "Updated tasks",
    tasks: [{ id: 1, text: "Shipped" }],
  }, { messageId: "{{result.checklistMessage.message_id}}" });
});
Checklist input is validated before delivery.

Titles support 1–255 characters. Each checklist requires 1–30 tasks with unique positive IDs and 1–100 characters of text. The helper requires a Telegram Business connection ID, defaulting to ctx.businessConnectionId.

Telegram Gifts and Business Stars

Gift catalog, balance, and owned-gift list helpers return typed data. Give a result a name in Visual Flow—or assign it in code—to use it in later blocks and imported flow functions.

gifts.ts
bot.on("business_message", async (ctx) => {
  const catalog = await ctx.telegram.getAvailableGifts();
  await ctx.telegram.sendGift(catalog.gifts[0].id);
  await ctx.telegram.giftPremiumSubscription(3);

  const balance = await ctx.telegram.getBusinessAccountStarBalance();
  const owned = await ctx.telegram.getBusinessAccountGifts({ limit: 20 });
  await ctx.telegram.upgradeGift(owned.gifts[0].owned_gift_id!, {
    keepOriginalDetails: true,
  });
});
Targets follow the current update.

Send and Premium helpers default to ctx.userId; user/chat lists default to their current IDs; Business settings, balance, transfer, list, conversion, upgrade, and gift transfer default to ctx.businessConnectionId. Every target may be explicit for out-of-band work.

Gift state still belongs to Telegram.

The SDK rejects conflicting targets and filters, unsupported formatted-text entities, invalid 1–100 page sizes, invalid Stars amounts, and Premium durations outside 3/6/12 months before delivery. Conversion, upgrade, and transfer can still fail when Telegram reports an incompatible gift state or insufficient business rights.

Inline query results

inline.ts
bot.on("inline_query", async (ctx) => {
  await ctx.telegram.answerInlineQuery([
    {
      type: "article",
      id: "help",
      title: "Help",
      input_message_content: { message_text: "How can I help?" },
    },
  ], { cacheTime: 60, isPersonal: true });
});

Validated moderation helpers

moderation.ts
await ctx.telegram.banMember(ctx.userId!, {
  revokeMessages: true,
});

await ctx.telegram.promoteMember(ctx.userId!, {
  can_manage_chat: true,
  can_delete_messages: true,
});

Telegram Stars payment lifecycle

payments.ts
bot.command("buy", (ctx) =>
  ctx.telegram.sendInvoice({
    title: "Premium access",
    description: "Unlock premium access",
    payload: "premium-order",
    currency: "XTR",
    prices: [{ label: "Premium", amount: 100 }],
  })
);

bot.on("pre_checkout_query", (ctx) =>
  ctx.telegram.answerPreCheckoutQuery(true)
);

bot.on("successful_payment", async (ctx) => {
  await deliverPurchase(ctx.successfulPayment!.invoice_payload);
});
Deliver only after completion.

pre_checkout_query validates the order; it does not prove payment. Deliver goods only from successful_payment. This semantic SDK event is nested inside Telegram's raw message update, so use message in allowedUpdates.

Forum topic lifecycle

forum.ts
await ctx.telegram.createForumTopic("Support", {
  iconColor: 7322096,
});

// These default to ctx.messageThreadId.
await ctx.telegram.editForumTopic({ name: "Help desk" });
await ctx.telegram.closeForumTopic();
await ctx.telegram.reopenForumTopic();
await ctx.telegram.unpinAllForumTopicMessages();
Topic deletion is destructive.

deleteForumTopic() removes the topic together with every message inside it. Use an explicit thread ID when the current update does not belong to the intended topic.

Use the actor and destination IDs deliberately.

ctx.userId identifies who caused an update; ctx.chatId identifies where a chat action can be sent. They are not interchangeable.

Build

Visual Flow

The canvas is a view of your configured source file, not a second copy of the program.
TriggerMessage receivedtrigger.message
LogicCondition branchaction.condition
TelegramSend messageaction.send_message
Two-way selection

Click a node to reveal its source range. Move the code cursor to highlight the matching node.

Persistent diagnostics

Node errors survive view changes and refreshes for the same source revision.

Safe connections

The graph blocks cycles, duplicate triggers, chatless actions, mismatched or repeated query answers, terminal-action children, and overlapping paths that would execute twice.

Stable layout

Saving code preserves node positions. Moving nodes changes layout without rewriting source.

Keep the visual file deterministic.

The synchronized file accepts literal handler arguments and supported SDK statements. Put unrestricted application code in other project files.

Run locally

Webhook

Create a framework-neutral handler that consumes and returns Web Standard objects.
webhook.ts
import {
  Bot,
  TelegramClient,
  createTelegramWebhookHandler,
} from "@bmhien/bot";

const bot = new Bot();
const telegram = new TelegramClient(
  process.env.TELEGRAM_BOT_TOKEN!,
);

bot.on("message", (ctx) =>
  ctx.reply(`Received: ${ctx.text ?? ""}`)
);

export const handleTelegram = createTelegramWebhookHandler(
  bot,
  telegram,
  {
    secretToken: process.env.TELEGRAM_WEBHOOK_SECRET,
    parseMode: "HTML",
  },
);
BehaviorResult
GET /webhook

Returns 405 Method Not Allowed.

Invalid secret

Returns 401 Unauthorized before parsing the body.

Malformed update

Returns 400 without dispatching a handler.

Future update type

Returns 200 safely so Telegram does not retry forever.

Handler throws

The request fails, allowing Telegram to retry delivery.

Run locally

Long polling

Receive updates locally with safe offsets, retry metadata, and graceful cancellation.
polling.ts
import { Bot, TelegramClient, runTelegramPolling } from "@bmhien/bot";

const bot = new Bot();
const telegram = new TelegramClient(process.env.TELEGRAM_BOT_TOKEN!);
const controller = new AbortController();

bot.command("start", (ctx) => ctx.reply("Running locally"));

await runTelegramPolling(bot, telegram, {
  signal: controller.signal,
  offset: 0,
  timeout: 30,
  limit: 100,
  allowedUpdates: ["message", "callback_query"],
  onError: console.error,
});
  • Offsets advance only after successful dispatch.
  • Duplicate and out-of-order update IDs are skipped.
  • Telegram retry_after controls the retry delay.
  • Aborting the signal cancels an in-flight request immediately.
  • timeout and limit are clamped to Telegram's accepted ranges.

Delivery

Security

Protect the token, authenticate webhook delivery, and keep sensitive values out of logs and browser code.
Token ownership

Anyone holding the bot token controls the bot. Store it only in environment variables or a dedicated secret store and rotate it through BotFather if it is exposed.

Verify every webhook

Set a random secretToken and configure the same value with Telegram. The handler verifies the request header before parsing the body.

Limit update types

Use allowedUpdates for polling—and the equivalent webhook setting—to receive only events your bot handles.

Sanitize observability

Never log tokens, environment values, request endpoints containing tokens, or download URLs produced by getFileUrl().

Webhook verification contract

CheckSDK behavior
HTTP method

Only POST is accepted; other methods return 405.

Secret format

Must contain 1–256 letters, digits, underscores, or hyphens.

Secret header

Compared against X-Telegram-Bot-Api-Secret-Token before JSON parsing.

Update identifier

update_id must be a non-negative safe integer.

Unknown future update

A valid but unsupported payload is acknowledged safely instead of entering a retry loop.

Use one delivery mode at a time.

Telegram does not deliver getUpdates results while a webhook is configured. Remove the webhook before starting local polling.

Run locally

Files & uploads

Use JSON for existing file IDs and multipart FormData for new file bytes.
Multipart upload
const form = new FormData();
form.set("chat_id", "123456789");
form.set("document", new Blob([bytes]), "report.pdf");

await telegram.upload("sendDocument", form);

const downloadUrl = await telegram.getFileUrl(fileId);
Do not set Content-Type manually for FormData.

The client leaves multipart headers unset so the Fetch implementation can add the correct boundary.

Reference

Bot API methods

The generic client recognizes every method documented in Telegram Bot API 10.3.
185 methods

Reference

Update types

Pass any update type below to bot.on(type, handler).
Semantic message event.

bot.on("successful_payment", handler) runs when a raw message contains Telegram's completed-payment object. It is an SDK event, not a value accepted by Telegram's allowed_updates.

Reference

Errors & limits

Validate early, preserve Telegram retry metadata, and never place the bot token in an error message.
Error handling
try {
  await telegram.call("sendMessage", {
    chat_id: 123456789,
    text: "Hello",
  });
} catch (error) {
  if (error instanceof TelegramClientError) {
    console.error(error.errorCode, error.retryAfter, error.migrateToChatId);
  }
}
Message text1–4096 characters
Media caption0–1024 characters
Callback notice0–200 characters
Callback data1–64 UTF-8 bytes
Poll question1–300 characters
Poll option1–100 characters
Poll choices1–12 options
Webhook secret1–256 safe characters
Polling batch1–100 updates
Inline results0–50 per answer
Inline next offset0–64 UTF-8 bytes
Pre-checkout answerWithin 10 seconds
Invoice title1–32 characters
Invoice description1–255 characters
Invoice payload1–128 UTF-8 bytes
Stars invoice pricesExactly 1 labeled price
Keep credentials outside source control.

Read bot tokens and webhook secrets from environment variables. The SDK sanitizes transport errors, but your own logs must avoid printing request URLs or environment values.

Copied to clipboard