Skip to content

Rich Messages Land, Ephemeral Replies Go Private, and Bot API 10.3 Spans the Stack

May 31 – August 25, 2026

This cycle carries three Telegram releases through the whole GramIO ecosystem. Bot API 10.1 introduced Rich Messages and join-request queries; 10.2 added structured rich input, ephemeral-message management, Communities, and subscription updates; 10.3 completed the model with private-delivery parameters, stoppable drafts, disabled buttons, richer quotations, documents, and tables.

GramIO now supports that surface from raw declarations all the way through contexts, formatting helpers, recursive uploads, keyboard builders, JSX, tests, and generated projects. This is a coordinated release train rather than a types-only regeneration.

Release status

The full release train is published. @gramio/types 10.3.1 exposes the same declarations on npm and JSR, and every dependent package listed below is available from npm with provenance.

Release train

OrderPackageTargetStatus
1@gramio/types10.3.1Published
2@gramio/contexts0.11.0Published
2@gramio/files0.8.0Published
2@gramio/format0.11.0Published
2@gramio/keyboards1.5.0Published
2@gramio/callback-data0.2.0Published
2wrappergram2.0.0Published
3gramio0.14.0Published
4@gramio/test0.8.0Published
4@gramio/jsx0.1.0Published
4@gramio/views0.2.1Published
5create-gramio2.3.0Published

The train was published in this order so every package consumed an already verified dependency layer. Unchanged plugins stay on their existing versions rather than being bumped for release-train symmetry; Views received a patch only because Bot API 10.3 changed its compile-time media union.

@gramio/types 10.1–10.3.1 — Three Telegram Releases, One Strict Contract

Bot API 10.1: Rich Messages and join-request queries

Bot API 10.1 introduced the Rich Messages read side: RichText*, RichBlock*, Message.rich_message, InputRichMessage, sendRichMessage, sendRichMessageDraft, and rich edits. The same release added join-request queries and link media for polls.

Bots can now decide a join request immediately or open a Mini App before resolving it:

ts
await bot.api.sendChatJoinRequestWebApp({
  chat_join_request_query_id: queryId,
  web_app_url: "https://example.com/join-review",
});

await bot.api.answerChatJoinRequestQuery({
  chat_join_request_query_id: queryId,
  result: "approve",
});

User.supportsJoinRequestQueries, ChatFullInfo.guardBot, and ChatJoinRequest.queryId expose the matching read-side capabilities in contexts.

Bot API 10.2: structured rich input, Communities, subscriptions, and ephemeral management

Rich Messages gained their complete write-side object model: paragraph, list, table, collage, slideshow, details, thinking, math, media, document, and voice-note blocks. Bot API 10.2 also added:

  • editEphemeralMessageText, editEphemeralMessageMedia, editEphemeralMessageCaption, editEphemeralMessageReplyMarkup, and deleteEphemeralMessage;
  • Message.receiver_user, Message.ephemeral_message_id, and ephemeral reply targets;
  • Community, community_chat_added, and community_chat_removed;
  • the top-level subscription update with active, canceled, and failed states.

Dedicated edit and delete methods keep their address fields at the top level:

ts
await bot.api.editEphemeralMessageText({
  chat_id: chatId,
  receiver_user_id: userId,
  ephemeral_message_id: ephemeralId,
  text: "Updated private result",
});

await bot.api.deleteEphemeralMessage({
  chat_id: chatId,
  receiver_user_id: userId,
  ephemeral_message_id: ephemeralId,
});

BREAKING: Bot API 10.3 nests ephemeral send parameters

Send methods no longer accept receiver_user_id or callback_query_id at the top level. GramIO intentionally mirrors Telegram one-to-one and provides no deprecated aliases.

ts
// Before
await bot.api.sendMessage({
  chat_id: 42,
  text: "Private result",
  receiver_user_id: userId,
  callback_query_id: callbackQueryId,
});

// Bot API 10.3
await bot.api.sendMessage({
  chat_id: 42,
  text: "Private result",
  ephemeral_message_parameters: {
    receiver_user_id: userId,
    callback_query_id: callbackQueryId,
    replace_callback_query_message: true,
  },
});

The nesting rule applies only to sends. The dedicated edit/delete methods shown above still use top-level receiver and ephemeral IDs because those fields address an existing message rather than configure a new delivery.

Fixtures that construct ChatAdministratorRights or ChatMemberAdministrator must now include the required welcome-message permission:

ts
const fixture = {
  ...existingAdministrator,
  can_send_welcome_messages: false,
};

Draft stop controls are guarded against regeneration regressions

Both draft methods expose the same controls:

ts
await bot.api.sendRichMessageDraft({
  chat_id: 42,
  draft_id: 1001,
  rich_message: { markdown: "## Generating…" },
  can_stop: true,
  keep_on_stop: true,
});

SendMessageDraftParams and SendRichMessageDraftParams are now protected by generation assertions, so a future schema/parser change cannot silently remove can_stop or keep_on_stop from either declaration.

See the Ephemeral Messages guide for the complete send, replace, edit, delete, and reply lifecycle.

@gramio/contexts 0.11 — Every New Update Gets a Real Context

Stop-generation updates need no opt-in

MessageGenerationStoppedContext handles stopped_message_generation and exposes draftId, threadId, chat, chatId, chatType, cloning, and thread-aware send helpers.

ts
bot.on("stopped_message_generation", async (ctx) => {
  console.log(ctx.draftId, ctx.threadId, ctx.chatId, ctx.chatType);
  await ctx.send("Generation stopped.");
});

Communities and subscriptions become first-class events

CommunityChatAddedContext, CommunityChatRemovedContext, and the new CommunityChatJoinedContext model the full Community lifecycle. Subscription updates expose the user, invoice payload, state, and convenience predicates:

ts
bot.on("community_chat_joined", (ctx) =>
  ctx.send(`Joined community ${ctx.community.id}`),
);

bot.on("subscription", (ctx) => {
  if (ctx.isFailed) {
    console.warn(`Subscription failed for ${ctx.user.id}`);
  }
});

Administrator/member wrappers add canSendWelcomeMessages(). UniqueGiftInfo adds text, wrapped entities, and isPrivate. Keyboard structures expose forceReply and disabled-button state.

Rich Messages flatten back to useful plain text

Context inspection and fallback text extraction now include rich buttons, button rows, expandable quotations, credits, and document captions. Bots that log, search, summarize, or test rich messages no longer lose those visible strings.

gramio 0.14 — Strict Raw API, Complete Routing, and Better Files

Core moves the whole dependency line together and keeps bot.api.* one-to-one with Telegram. stopped_message_generation is included in AllowedUpdatesFilter.default, AllowedUpdatesFilter.all, and filters derived from registered handlers. Unlike chat_member or reactions, it is not an opt-in update.

Also shipped since the previous changelog

The 10.1/10.2 cycle delivered two useful framework improvements beyond the Bot API surface:

  • ctx.download() and bot.downloadFile() return a lazy, Response-like handle with .bytes(), .text(), .json(), .blob(), .stream(), .toFile(), .link(), and .info();
  • callback-query contexts now expose topic/thread information, so send mixins correctly retain the originating thread.
ts
await ctx.download().toFile("./photo.jpg");
const metadata = await ctx.download().info();

@gramio/format 0.11 — Rich Messages Become Pleasant to Author

gramio/rich and @gramio/format/rich provide escaped, composable builders instead of forcing applications to hand-build every InputRichBlock* object.

ts
import { bold, format } from "gramio";
import {
  button,
  buttonRow,
  document,
  heading,
  paragraph,
  quote,
  rich,
  table,
} from "gramio/rich";

await ctx.send(
  rich([
    heading(1, "Release report"),
    paragraph(format`Status: ${bold`ready`}`),
    quote("Expandable details", {
      expandable: true,
      credit: "Build system",
    }),
    document({
      url: "https://example.com/report.pdf",
      caption: "Full report",
    }),
    table(
      [
        ["Package", "Version"],
        ["gramio", "0.14.0"],
      ],
      { compact: true, align: ["left", "right"] },
    ),
    buttonRow(
      [
        button("Open", {
          type: "url",
          url: "https://gramio.dev",
        }),
        button("Soon", { type: "disabled" }),
      ],
      { align: "right" },
    ),
  ]),
);

The generated formatting mutator also covers rich content in ephemeral edits and the new document/caption paths.

@gramio/files 0.8 — Uploads Follow Rich Content Recursively

File extraction now walks references, unions, arrays, and recursive rich blocks. It understands uploads in sendRichMessage.rich_message.blocks, .media, nested media metadata, and InputRichBlockDocument.

ts
import { MediaUpload } from "gramio";

await bot.api.sendRichMessage({
  chat_id: chatId,
  rich_message: {
    html: '<tg-document src="tg://document?id=report"></tg-document>',
    media: [
      {
        id: "report",
        media: {
          type: "document",
          media: await MediaUpload.path("./report.pdf"),
        },
      },
    ],
  },
});

The legacy Extractor shape remains available for downstream integrations; generated metadata adds wildcard path descriptors for deeper content. sendRichMessageDraft remains deliberately excluded because Telegram forbids direct uploads in drafts—reuse a file_id there.

@gramio/jsx 0.1 — Rich Layouts in JSX

The rich JSX runtime adds <button>, <button-row>, <document>, expandable credited <blockquote>, and compact <table> elements.

tsx
/** @jsxImportSource @gramio/jsx/rich */

const report = (
  <rich>
    <h1>Release report</h1>
    <blockquote expandable credit="Build system">
      Expandable details
    </blockquote>
    <document url="https://example.com/report.pdf" caption="Full report" />
    <table compact align={["left", "right"]}>
      <tr>
        <th>Package</th>
        <th>Version</th>
      </tr>
      <tr>
        <td>gramio</td>
        <td>0.14.0</td>
      </tr>
    </table>
    <button-row align="right">
      <button type="url" url="https://gramio.dev">
        Open
      </button>
      <button type="disabled">Soon</button>
    </button-row>
  </rich>
);

await ctx.send(report);

The regular keyboard JSX runtime gains disabled buttons and forceReply support as well.

@gramio/keyboards 1.5 — Disabled Actions and Force Reply

Builders now expose Telegram's disabled inline buttons and force_reply flag:

ts
import { InlineKeyboard, Keyboard } from "gramio";

const inline = new InlineKeyboard()
  .text("Run", "run")
  .disabled("Unavailable", { style: "primary" })
  .forceReply();

const reply = new Keyboard().text("Share status").forceReply();

.forceReply(false) explicitly serializes force_reply: false, which is useful when a shared builder is conditionally configured.

@gramio/callback-data 0.2 — Hidden Payloads for Reply Keyboards

Inline keyboards have callback_data, but reply-keyboard buttons send their visible label back as ordinary message text. The new zero-width codec can attach a packed payload to that label without changing what the user sees:

ts
import { embed, extract } from "@gramio/callback-data";
import { CallbackData, Keyboard } from "gramio";

const navigation = new CallbackData("reply-navigation").enum("to", ["settings"]);

const keyboard = new Keyboard().text(
  embed("⚙️ Settings", navigation.pack({ to: "settings" })),
);

bot.on("message", (ctx) => {
  const embedded = extract(ctx.text ?? "");
  if (!embedded) return;

  const action = navigation.safeUnpack(embedded.data);
  if (!action.success) return;

  return ctx.send(`Opening ${action.data.to}`);
});

encode() and decode() expose the raw invisible codec, while embed() and extract() operate on complete visible labels. Treat the embedded payload as transport data and validate it with CallbackData.safeUnpack() before using it.

@gramio/test 0.8 — Stop Drafts and Assert Private Sends

The user actor can now stop an in-progress draft. The test environment delivers the same typed update your production handler receives:

ts
import { TelegramTestEnvironment } from "@gramio/test";

const env = new TelegramTestEnvironment(bot);
const user = env.createUser({ first_name: "Ada" });

await user.stopMessageGeneration(1001, {
  messageThreadId: 7,
});

Mocked ephemeral sends read the nested parameters and return realistic receiver_user and ephemeral_message_id fields:

ts
const sent = await bot.api.sendMessage({
  chat_id: user.payload.id,
  text: "Private result",
  ephemeral_message_parameters: {
    receiver_user_id: user.payload.id,
  },
});

expect(sent.receiver_user?.id).toBe(user.payload.id);
expect(sent.ephemeral_message_id).toBeNumber();

Compile fixtures also assert that legacy top-level ephemeral fields fail, preventing accidental compatibility aliases from returning later.

wrappergram 2.0 — Middleware Core and the Correct 10.3 Raw Line

Wrappergram 2.0 keeps the Telegram class, but changes how calls, errors, and optional capabilities work. The old version returned the raw { ok, result } envelope and bundled file handling. Version 2 returns the API result directly, throws TelegramError by default, and moves files/formatting into opt-in middleware.

ts
// Before: raw response envelope, file handling bundled in
const response = await telegram.api.sendMessage({
  chat_id: chatId,
  text: "Hello",
});

if (!response.ok) console.error(response.description);
else console.log(response.result.message_id);
ts
// Wrappergram 2.0
import { Telegram, TelegramError } from "wrappergram";
import { filesMiddleware } from "@gramio/files/middleware";
import { formatMiddleware } from "@gramio/format/middleware";

const telegram = new Telegram(token, {
  middlewares: [formatMiddleware, filesMiddleware],
});

const result = await telegram.api.sendMessage({
  chat_id: chatId,
  text: "Hello",
  suppress: true,
});

if (result instanceof TelegramError) {
  console.error(result.method, result.code, result.payload);
} else {
  console.log(result.message_id);
}

requestOptions becomes global fetchOptions, while every API method also accepts per-request fetch options as its second argument. gramio users are unaffected by this migration; it matters only to direct Wrappergram consumers.

@gramio/views 0.2.1 — Bot API 10.3 Media Compatibility

Bot API 10.3 adds InputMediaLivePhoto, which carries separate photo and video inputs. Views intentionally models one uploaded file per .media() item, so 0.2.1 excludes live photos from that type boundary instead of accepting an object it cannot serialize correctly.

The patch also preserves media-specific fields such as thumbnails and streaming flags when a view sends or edits supported media:

ts
return this.response.media({
  type: "video",
  media: videoFileId,
  thumbnail: thumbnailFileId,
  supports_streaming: true,
});

Use the raw sendLivePhoto method for live photos. Existing photo, video, animation, audio, document, sticker, voice, and video-note views remain supported.

create-gramio 2.3 — New Projects Start on the New Line

New projects use gramio ^0.14.0 and @gramio/test ^0.8.0. Generator fixtures assert those versions so templates cannot silently drift back to the previous Bot API line.

bash
npm create gramio@latest my-bot
cd my-bot
bun install
bunx tsc --noEmit

Compatibility, documentation, and rollout

  • TDLight compiles against the local Types 10.3.1 declaration set.
  • Scenes, Session, i18n, Auto Answer Callback Query, Onboarding, Rate Limit, PostHog, and Views pass source-stack compatibility runs.
  • Views 0.2.1 excludes the new two-file InputMediaLivePhoto from its one-file media abstraction and preserves complete editable media objects.
  • Ecosystem CI passes 17 downstream suites with Types built from source and 24 suites against the published latest line.
  • The Telegram API reference was regenerated for Bot API 10.3.
  • New EN/RU guides cover Ephemeral Messages and Rich Messages.
  • The GramIO AI skill adds a rich-message reference and a runnable tested example; upgrade data now covers every package in the train.

The complete train is available from npm, and Types 10.3.1 has declaration parity between npm and JSR. Every layer, generated project, unchanged consumer, and published package line has now passed its release gate.