This reference covers every HTTP method the Migram bot platform exposes, plus the update payloads your bot receives by polling or webhook.
New here? Work through the Bot Manual first -- it walks through creating a bot with/newbotin a chat with @botmigi before any of this becomes relevant.
Porting an existing Telegram bot instead? Jump straight to Telegram-compatible mode, or read Migrating from Telegram for the full walkthrough.
Telegram-compatible mode
Migram now also answers requests shaped like Telegram's own Bot API -- same URL pattern, envelope, and parameter names, reached at https://api.migram.org/bot<token>/METHOD_NAME -- alongside the native calling convention documented throughout the rest of this page, which is unchanged. See the new Telegram-compatible mode section below and the Migrating from Telegram guide.
Reference rewrite
This page has been rewritten throughout. The sendSticker parameter table now matches what the platform actually accepts: a catalog stickerId, not a URL. Making requests and Available methods now describe the real response envelope -- including which single key each read or configuration method nests its data under -- and document the outbound rate limits and webhook retry behavior for the first time. The Update type table has also been trimmed down to the three update types the platform currently delivers.
Bot API 1.0
Initial public release of the Migram Bot API reference. It documents the complete bot-platform HTTP surface: messaging, media, chat actions, command management, callback and inline answers, webhook updates, and Mini App helpers.
Already have a Telegram bot? Migram also answers requests shaped exactly like Telegram's own Bot API -- same URL pattern, the same { "ok": ... } envelope, and Telegram's own parameter names -- so an existing python-telegram-bot, grammY, aiogram, or Telebot client often needs nothing more than a new base URL. This section is the quick reference; Migrating from Telegram walks through every divergence in depth, including what Migram deliberately doesn't support.
Swap Telegram's host for Migram's and keep everything else about the URL the same:
https://api.telegram.org/bot<token>/METHOD_NAME -> https://api.migram.org/bot<token>/METHOD_NAME
The token sits in the path instead of an Authorization header. Method names are matched case-insensitively here, matching Telegram's own behavior -- unlike the case-sensitive native /bot/METHOD_NAME path described in Making requests below. Both GET and POST are accepted no matter which verb the underlying native method expects; any other verb gets a 405. A request body may be JSON, application/x-www-form-urlencoded, or multipart/form-data -- whichever your existing Telegram client library already sends.
Method names themselves are identical between the two calling conventions -- sendMessage, getUpdates, and the rest of the methods documented in Available methods below answer to the same name either way. Only the URL shape, authentication, parameter names, and response envelope change.
Every bot has a permanent numeric alias alongside its native id, and that number is what goes in a compat-mode token: <numeric id>:<secret>, the same secret as always, just with the number in front instead of the 24-character native id. @botmigi's /newbot, /token, and /revoke responses show this numeric form first, with the native <botId>:<secret> form underneath.
This isn't cosmetic. Some Telegram libraries validate the token client-side before making any HTTP call at all -- aiogram, for one, checks that the part before the colon parses as an integer and derives its own idea of bot.id from it. Those libraries need the numeric form; handing them the native hex id fails before a request is ever sent. Libraries that don't inspect the token's shape accept either form in the URL. getMe's own result.id is that same numeric alias in compat mode, so a library that computed bot.id from the token prefix sees the same number confirmed back.
Every response, success or failure, is one JSON object shaped exactly like Telegram's -- unlike the native envelope in Making requests, where a failure is a bare {"error": "..."} string with no numeric code:
{ "ok": true, "result": ... }
{ "ok": false, "error_code": 429, "description": "Too Many Requests: retry after 3", "parameters": { "retry_after": 3 } }
A compat-mode success always nests its payload under result -- there's no per-method named key the way native read/configuration methods use bot, commands, or menuButton (see Making requests); the compat layer folds every method's native response into that single result shape. A failure always carries error_code and description, plus a parameters object when there's something structured to attach, such as a 429's retry_after; error_code always matches the HTTP status. Unlike the native error shape, a compat failure has no separate top-level error field -- description is the whole story. Anything Migram knows about a result that Telegram's own shape has no field for -- native string ids, for instance -- rides under an additive migram key alongside the Telegram-shaped fields; a standard Telegram client library ignores keys it doesn't recognize, so this never breaks parsing.
| Status | Meaning |
|---|---|
| 200 | Success. The body contains "ok": true. |
| 400 | Bad Request -- a required parameter is missing or fails validation, names a chat or message Migram can't find, or supplies a parameter Migram has no way to honor (see Parameter names below). Telegram itself answers "not found" with 400, not 404, and compat mode matches that. |
| 401 | Unauthorized -- the token is missing or doesn't match a registered bot. |
| 403 | Forbidden -- the bot isn't a participant in the named conversation. |
| 404 | Not Found -- either the method name doesn't exist at all, or it names a real Telegram method Migram has no equivalent for (see Unsupported Telegram methods below). |
| 405 | Method Not Allowed -- an HTTP verb other than GET or POST. |
| 409 | Conflict -- the same getUpdates/webhook mutual exclusion described in Getting updates. |
| 429 | Too Many Requests -- back off for parameters.retry_after seconds. |
| 500 | Internal Server Error -- something failed inside Migram itself. |
| 502 | Bad Gateway -- the bot platform couldn't reach the Migram backend. |
Send Telegram's own snake_case parameter names and compat mode maps each one to the native camelCase parameter documented in Available methods. Every method has a closed, per-method list of what it accepts -- a parameter Telegram documents that isn't on that list, and isn't empty, is rejected with 400 naming it, rather than silently dropped or partially honored:
{ "ok": false, "error_code": 400, "description": "Bad Request: parameter reply_to_message_id is not supported by Migram. See https://migram.org/docs/bots/migrating" }
A short list of boolean hints Telegram itself treats as advisory -- disable_notification, protect_content, disable_web_page_preview, and a few others -- are the one exception: omitted or sent as false, they're accepted as a no-op, since that's already the only behavior Migram has; sent as explicit true, they're rejected the same 400 way as any other parameter Migram can't honor, rather than pretending to apply a hint it can't act on. This table covers the names shared across most methods; it isn't exhaustive -- Migrating from Telegram has the full per-method table.
| Telegram parameter | Maps to | Notes |
|---|---|---|
| chat_id | conversationId | Accepts either the numeric per-bot alias a Telegram library expects (see Numeric IDs below) or Migram's native conversation id directly. |
| message_id | messageId | Same aliasing as chat_id. |
| text | text | Unchanged. |
| parse_mode | parseMode | Accepts Markdown or HTML, the same two modes native formatting options support. Migram has no separate MarkdownV2 mode -- sending it is rejected with 400 naming the unsupported value, rather than silently downgraded to Markdown. |
| reply_markup | replyMarkup | When sent as a form or multipart field (as Telegram libraries do outside plain JSON), decoded from its JSON-string encoding first. The button-row array shape is identical to native's -- see InlineKeyboardButton -- and Telegram's inline_keyboard wrapper key is unwrapped automatically. |
| callback_data | callbackData | Same 64-byte cap. |
| reply_to_message_id | -- no native equivalent -- | Migram doesn't model message replies today, so this parameter is rejected with 400 rather than silently ignored -- accepting it without effect would misrepresent what your bot actually sent. |
| disable_notification, protect_content | -- no native equivalent -- | Accepted as a no-op when omitted or false; rejected with 400 when sent as true, since Migram has no silent-send or forwarding-lock behavior to apply. |
| photo, document, video, voice, animation, sticker | the matching native *Url parameter, or an uploaded/catalog reference | An HTTP(S) URL string works exactly like native's own URL parameters. See Media uploads below for multipart bytes and file-id reuse. |
| offset, limit, timeout | offset, limit, timeout | Same names, same semantics, on getUpdates. timeout is clamped to 50 seconds either way. |
| url | url | Unchanged, on setWebhook. |
| drop_pending_updates | dropPendingUpdates | Unchanged. |
| callback_query_id | callbackQueryId | Unchanged. |
| inline_query_id | inlineQueryId | Unchanged. |
Telegram's own libraries assume chat.id and from.id are 64-bit integers and message_id is a plain integer. Migram's real identifiers are strings, not numbers. Compat mode bridges the two with a per-bot numeric alias, allocated the first time your bot sees a given conversation, message, or user arrive through a getUpdates poll -- not the moment you first send to one. Once that alias exists, send the number back as chat_id or message_id and it's translated back to the underlying conversation or message before the request reaches the same code path a native call would use. A conversation's chat.type in any response or update is always the real type Migram has on file for it -- never a guess and never defaulted to private when the actual type is something else.
A send-first bot -- one that hasn't polled yet -- can't address a chat by a number it has never seen: that returns 400 ("chat not found"), even though the conversation is real. Poll once first, or pass Migram's own native id directly as a string in chat_id or message_id instead -- Telegram's own field type for both is already "Integer or String", so this is a legal value, and it works whether or not a numeric alias has been allocated yet. Aliases are permanent once allocated and are never reused for a different chat, message, or user. The bot's own numeric alias, described in Token format above, is a separate kind of alias from these conversation/message/user ones, though it's allocated and stored the same way. The update_id you already get back from getUpdates is a separate, existing per-bot counter -- unrelated to this aliasing scheme and unaffected by it.
Send media a Telegram library's way: as an HTTP(S) URL string, exactly like native's own photoUrl/documentUrl/videoUrl parameters, or as raw bytes in a multipart/form-data field. A URL is fetched by the recipient's client the same way native URL sends already work today -- Migram's server doesn't fetch it on your behalf. Multipart bytes, by contrast, are ingested into Migram's own media store first; the resulting message carries an ordinary Migram-hosted attachment, and Migram issues a file_id for it that you can reuse in a later media parameter to resend the same file without re-uploading it.
A file_id copied from an actual Telegram chat is a different platform's opaque string and is rejected with 400 ("wrong file identifier") rather than silently accepted and ignored -- the two platforms don't share a file registry.
Documented limitation: dimensions and similar media metadata are only ever real values, from a file Migram actually ingested and measured through multipart or attach://. A plain URL send never gets measured server-side -- Migram passes the URL through exactly like a native call does today -- so those fields are simply absent from the response rather than filled in with an invented width, height, or size.
Compat mode only answers for the methods Migram actually implements, listed in Available methods below. Calling a genuine Telegram method Migram has no equivalent for -- forwardMessage, copyMessage, banChatMember, and getChat are common examples -- returns a 404 that names the method and points at the migration guide instead of pretending to succeed:
{ "ok": false, "error_code": 404, "description": "Not Found: method forwardMessage is not supported by Migram. See https://migram.org/docs/bots/migrating" }
See Migrating from Telegram for the full list of what's different and why, including what Migram doesn't attempt to support at all. Compat mode covers Telegram's HTTP Bot API only -- Migram has no MTProto transport and no equivalent of Telegram's separate client/userbot API, in either calling convention.
getUpdates keeps the same numeric, ever-increasing update_id, the same 1-100 (default 100) limit, and the same 0-50 second timeout window described in Getting updates. Compat mode additionally rewrites each returned update's message, callback_query, or inline_query payload into Telegram's own field names and integer id aliases before handing it back, so a stock Telegram update handler can read it unmodified.
Migram also has a couple of its own update kinds Telegram has no concept of at all -- a bot being added to or removed from a chat, and a verification-flow event. On this route, one of those arrives as an Update carrying only its update_id and nothing else your library recognizes; a stock Telegram handler skips it and advances past it exactly as it would for any update type it doesn't know, which is honest enough for a poller that only cares about messages and button presses. The full event, with its real content, stays available on the native surface.
Migram's own webhook deliveries are unaffected by any of this, regardless of which calling convention your bot otherwise uses -- they keep their existing signed X-Migi-* headers, snake_case field names, and UUID update_id. See Migrating from Telegram for the exact payload differences if you're porting a webhook receiver rather than a poller.
@botmigi hands your bot a single authentication token the moment it's created, in response to /newbot. The token has two parts joined by a colon: the bot's ID, then a secret.
65f2c8a1b3e4d5f6a7b8c9d0:Xy7Kp2mQ9nR4sT6vW8zA1bC3dE5fG7hJ
Treat it like a password -- whoever holds it can act as your bot. Lost track of it? /token shows it again. Think it leaked? /revoke kills it and issues a fresh one.
Every request needs this token in an Authorization header, using the Bearer scheme:
Authorization: Bearer <botId>:<secret>
Want to see what a conversation id looks like before writing any code? Message @idbot in Migram -- it replies with your user id and the id of that very conversation, as a quick sanity check of the shape you're working with. @idbot's own conversation id only ever identifies YOUR chat with @idbot, though -- it isn't reusable for your bot, so this is an inspection aid, not the actual solution below.
Before your bot can reply, it needs the conversation's ID. Open your bot in Migram, send it /start, then read the incoming message update in either of these ways:
message.conversation_id from the returned update.message.conversation_id field from the signed webhook payload.curl "https://api.migram.org/bot/getUpdates?limit=1" \ -H "Authorization: Bearer 65f2c8a1b3e4d5f6a7b8c9d0:EXAMPLE-TOKEN-DO-NOT-USE"
{
"ok": true,
"result": [
{
"update_id": 1042,
"type": "message",
"message": {
"id": "6620a2b4e4d5f6a7b8c9d0e2",
"from": { "id": "661fa901e4d5f6a7b8c9d0df", "displayName": "Alex" },
"conversation_id": "6620a1f3e4d5f6a7b8c9d0e1",
"text": "/start",
"timestamp": "2026-07-20T18:30:00Z"
}
}
]
}
Copy the value of conversation_id and pass it as the camelCase request parameter conversationId to sendMessage and the other conversation methods. Request parameters are camelCase, including conversationId, parseMode, and dropPendingUpdates; webhook and getUpdates payload fields are snake_case, including update_id, conversation_id, and callback_query. This asymmetry is intentional.
This section documents Migram's native calling convention: camelCase request bodies, authenticated with an Authorization: Bearer header. Porting existing Telegram bot code instead? See Telegram-compatible mode above.
Every call to the public Migram Bot API is a plain HTTP request shaped like this:
https://api.migram.org/bot/METHOD_NAME
For example, sendMessage is reached at:
https://api.migram.org/bot/sendMessage
Method names are matched exactly and are case-sensitive -- /bot/SendMessage is a 404, not an alias. Nearly every method takes a POST with a JSON body (Content-Type: application/json). getMyCommands and getWebhookInfo use GET with query-string parameters; getUpdates accepts either GET query parameters or a POST JSON body. Body parameter names are camelCase throughout. Every call needs an Authorization: Bearer <botId>:<secret> header.
A successful call always returns a JSON object with "ok": true, but what comes with it depends on the method:
result object: { "ok": true, "result": { "messageId": "...", "conversationId": "..." } }{ "ok": true } and nothing else.ok, and that key differs per method: getMe uses "bot"; setMyCommands, getMyCommands, and deleteMyCommands use "commands"; setChatMenuButton uses "menuButton"; validateWebAppData uses "params"; generateWebAppInitData uses "initData". Nothing is ever spread as flat top-level fields next to ok -- check each method's own section for the key it uses.{ "ok": true, "result": ... } on success.Except for the four update-delivery methods, a failed call returns a single error string and nothing else, paired with a non-200 status:
{ "error": "descriptive message" }
For those legacy methods, there's no numeric error code and no separate description field to parse -- the HTTP status and the error string are all you get. The four update-delivery methods instead return { "ok": false, "error_code": <status>, "description": "...", "error": "..." }, with error_code matching the HTTP status. Hitting a path that doesn't match any method at all (a typo'd name) falls through to a plain-text 404 page not found instead of either JSON shape, so don't assume every non-2xx body parses as JSON.
Sending too fast also produces a non-2xx response. Every send, edit, delete, action, and answer route is metered: a bot may make at most 30 requests per second in total, and at most 20 requests per minute against any single conversation. Both caps apply per bot. Crossing either returns HTTP 429 with a Retry-After header and a body carrying a matching retry_after field, both counted in seconds:
{ "error": "rate limit exceeded", "retry_after": 3 }
The full set of statuses you can get back:
| Status | Meaning |
|---|---|
| 200 | Success. The body contains "ok": true. |
| 400 | Bad request -- a required parameter is missing or failed validation. The error string names the problem. |
| 401 | Unauthorized -- the token is missing ("unauthorized: missing bearer token") or invalid ("unauthorized: invalid token"). |
| 403 | Forbidden -- the conversation named by conversationId doesn't belong to your bot ("conversation does not belong to bot"). |
| 404 | Not found -- the method name, conversation, or message doesn't exist. |
| 405 | Method not allowed -- wrong HTTP verb for this method. See each method section for its accepted verb. |
| 409 | Conflict -- polling and webhook delivery are mutually exclusive, or another getUpdates request replaced the active poller. |
| 429 | Too many requests -- see the rate-limit note above. Back off for at least retry_after seconds. |
| 502 | Bad gateway -- the bot platform couldn't reach the Migram backend. |
Migram delivers updates in one of two mutually exclusive ways: poll getUpdates, or register a public HTTPS endpoint with setWebhook. A message, inline-button press, or inline query becomes an Update with the same snake_case shape in either delivery mode.
Every webhook delivery carries three headers so you can confirm it really came from Migram: X-Migi-Signature: sha256=<hex>, an HMAC-SHA256 of the delivery timestamp and body keyed with your bot's webhook secret; X-Migi-Timestamp, the same unix-seconds timestamp folded into that signature, which stops a captured payload from being replayed later under a stale signature; and X-Migi-Bot-Id, your bot's ID, sent unsigned purely for routing.
A delivery that doesn't get a 2xx back -- including a deliberate 4xx, and including no response at all within 10 seconds -- is retried up to three more times, after 1, 4, and then 16 seconds, for four attempts total. There's currently no status code that tells Migram to stop retrying, so build your handler to tolerate seeing the same update more than once.
Your webhook can answer synchronously with a response payload in the same HTTP response, or just acknowledge the delivery and reply later through the HTTP API.
One naming note: webhook and getUpdates payload fields are snake_case (update_id, conversation_id, callback_query), while parameters sent to HTTP API methods are camelCase (conversationId, parseMode, dropPendingUpdates). This asymmetry is intentional.
One update, describing one thing that happened. At most one of the payload fields below is present, matching whichever type the update carries.
| Field | Type | Description |
|---|---|---|
| update_id | Integer or String | Integer in getUpdates results -- increases for each update belonging to this bot; webhook deliveries instead carry an opaque UUID string in this field. Use offset acking only with getUpdates. |
| type | String | What kind of update this is: message, callback_query, or inline_query. The wire format reserves a few additional values for future use, but nothing currently live ever assigns them, so these three are the only ones your update handler needs to branch on today. |
| message | Message | Optional. Present when type is message. |
| callback_query | CallbackQuery | Optional. Present when type is callback_query -- an inline-keyboard button was pressed. |
| inline_query | InlineQuery | Optional. Present when type is inline_query. |
A message your bot received inside an Update of type message, delivered by getUpdates or webhook.
| Field | Type | Description |
|---|---|---|
| id | String | Unique identifier for this message. |
| from | User | Who sent it. |
| conversation_id | String | Which conversation this message belongs to. |
| text | String | Optional. The message text. |
| photo_url | String | Optional. URL of an attached photo, set on photo-upload flows that forward an image instead of text (for example @botmigi's /setuserpic). |
| photo_id | String | Optional. File ID of the attached photo, alongside photo_url. |
| timestamp | String | When the message was sent, RFC 3339. |
| forwarded_from | ForwardInfo | Optional. Present only when this message is itself a forward. |
The original source of a forwarded Message. Always already resolved against the origin's privacy settings before your bot ever sees it. original_sender_id is omitted both when the original sender's privacy settings denied a link back to their account AND when Migram itself simply couldn't resolve the origin -- your bot cannot and should not try to tell these two cases apart; treat a missing id as "not available here," never as proof of a specific reason.
When original_sender_id is absent, original_sender_name and original_timestamp are ALSO omitted, not just the id. Both are stable across every forward of the same underlying message, so a bot that received one visible-origin copy and one hidden-origin copy of the identical forward could otherwise join them on name + exact timestamp and recover the "withheld" identity -- so neither is sent at all once the id is withheld. This is deliberately stricter than Telegram's own equivalent (which does still send a name for a hidden-origin forward). What this can NOT hide: the message's own text and, for a forwarded photo, its underlying media reference are unavoidably identical across every copy of the same forward -- that's inherent to what forwarding IS (true in Telegram too), not something this field controls.
| Field | Type | Description |
|---|---|---|
| original_sender_id | String | Optional. Omitted when unavailable (privacy-denied link, or an unresolvable origin) -- never fabricated. |
| original_sender_name | String | Optional. The original sender's name as Migram had it at forward time. Omitted together with original_sender_id when that's absent. |
| original_timestamp | String | Optional. When the original message was sent, RFC 3339. Omitted together with original_sender_id when that's absent. |
Telegram-compatible mode (see below) translates this into Telegram's own forward_origin shape: a visible origin becomes {"type": "user", "date": ..., "sender_user": {...}} (the id aliased the same way every other user id is in compat mode). A withheld origin becomes {"type": "hidden_user", "date": ..., "sender_user_name": ""} -- both fields Telegram marks required on MessageOriginHiddenUser ARE present (so strict/schema-validated client libraries don't reject the update), but neither carries anything correlatable: date here is the forward message's own event time, never the original sender's send time (which is identical across every copy of the same forward and is exactly the correlation risk described above), and sender_user_name is always an empty string, never the real name. This "correlation-safe parity" shape is a deliberate compat-mode divergence from literal Telegram behavior (which does send a real name for a hidden-origin forward) -- documented here rather than silent.
Delivered when a user presses a button on an inline keyboard your bot sent. Acknowledge it with answerCallbackQuery -- clients show a loading state on the button until you do.
| Field | Type | Description |
|---|---|---|
| id | String | Identifies this specific button press. |
| from | User | Who pressed it. |
| conversation_id | String | Conversation containing the message the button is attached to. |
| data | String | The pressed button's callbackData, unchanged, up to 64 bytes. |
| message_id | String | The message the button belongs to. |
Delivered as a user types after @yourbot in any chat's message field. Reply with answerInlineQuery.
| Field | Type | Description |
|---|---|---|
| id | String | Identifies this query. |
| from | User | Who's typing. |
| query | String | The text typed so far, up to 256 characters. |
| offset | String | Pagination cursor your bot previously returned as nextOffset; empty on the first request for a given query text. |
| conversation_id | String | Where the query is being typed. |
Every type in the Migram Bot API is a plain JSON object -- there's no binary encoding or custom serialization to worry about.
Fields marked Optional below are left out of the JSON entirely when they don't apply, rather than sent as
null.
The sender of something your bot received -- shows up in the from field of Update payloads (Message, CallbackQuery, InlineQuery).
| Field | Type | Description |
|---|---|---|
| id | String | This user's identifier. |
| displayName | String | Their display name. |
| username | String | Optional. Their @username, when they have one. |
| first_name | String | Optional. Their first name, when Migram has it recorded separately from displayName. Accounts created before the first/last split existed don't carry it -- use displayName as the fallback rather than assuming it's missing. |
| last_name | String | Optional. Their last name, same caveat as first_name. |
A wrapped inline keyboard. This is the shape InlineQueryResult's replyMarkup field expects -- note that sendMessage's own replyMarkup parameter skips the wrapper and takes the button rows directly (see InlineKeyboardButton).
| Field | Type | Description |
|---|---|---|
| inlineKeyboard | Array of Array of InlineKeyboardButton | Button rows, outer array top to bottom, inner array left to right within a row. |
One button. replyMarkup on methods like sendMessage takes an Array of Array of these -- each inner array is one row (see InlineKeyboardMarkup for the wrapped form used elsewhere). Give the button behavior by setting exactly one of the optional fields below.
| Field | Type | Description |
|---|---|---|
| text | String | The label shown on the button. |
| callbackData | String | Optional. Sent back to your bot as a callback query when pressed, up to 64 bytes. Emoji cost 4 bytes each, so a single emoji already uses a quarter of the budget. |
| url | String | Optional. Opens this HTTP or app URL when pressed. |
| webApp | WebAppInfo | Optional. Launches this Mini App when pressed -- a single-field object holding a url. |
| switchInlineQuery | String | Optional. When set, pressing the button lets the user pick one of their chats and drops the bot's username plus this text into that chat's input field. |
| style | String | Optional. Visual accent: "primary", "danger" or "success". |
| iconCustomEmojiId | String | Optional. A custom emoji shown as the button's icon. |
One entry in your bot's command menu, set via setMyCommands.
| Field | Type | Description |
|---|---|---|
| command | String | The command text. Required -- can't be blank. |
| description | String | What it does, shown next to it in the menu. Required -- can't be blank. |
Targets a command set to a particular audience. Used by setMyCommands, getMyCommands, and deleteMyCommands.
| Field | Type | Description |
|---|---|---|
| type | String | One of "default" (the fallback when nothing more specific matches), "all_private_chats", "all_group_chats", "chat" (needs chatId), or "chat_member" (needs chatId and userId). |
| chatId | String | Optional. The target chat. Required when type is "chat" or "chat_member". |
| userId | String | Optional. The target user. Required when type is "chat_member". |
A bot's menu button in a private chat, shown next to the message input field. Set via setChatMenuButton. Unlike InlineKeyboardButton's webApp field, which nests a whole WebAppInfo object, MenuButton keeps its Mini App URL directly on its own url field -- there's no wrapper here.
| Field | Type | Description |
|---|---|---|
| type | String | "default" (no custom button), "commands" (opens the command list), or "web_app" (launches a Mini App). |
| text | String | Optional. Button label. Defaults to "Menu" for "commands", "Open" for "web_app". |
| url | String | Optional. Mini App URL to open. Required when type is "web_app". |
Points at a Mini App -- Migram's term for an in-chat web app -- to launch. Referenced from InlineKeyboardButton's webApp field.
| Field | Type | Description |
|---|---|---|
| url | String | The Mini App's URL. |
A poll message, as sent by sendPoll.
| Field | Type | Description |
|---|---|---|
| question | String | The poll question, up to 300 bytes. Bytes, not characters -- emoji and non-Latin scripts eat several bytes each, so the real character budget runs smaller than the number suggests. |
| options | Array of PollOption | Between 2 and 10 answer options. |
| type | String | "regular" or "quiz". |
| isAnonymous | Boolean | Whether voters are hidden from each other. |
| allowsMultipleAnswers | Boolean | Whether a voter can pick more than one option. |
| correctOptionId | Integer | Optional. 0-based index of the correct option. Only meaningful for "quiz" polls. |
| explanation | String | Optional. Shown to anyone who picks a wrong answer in a "quiz" poll. |
One answer choice inside a Poll.
| Field | Type | Description |
|---|---|---|
| text | String | Up to 100 bytes (see the byte-vs-character note on Poll). |
| voterCount | Integer | How many users picked this option. |
One item inside a sendMediaGroup call. A group needs 2-10 of these.
| Field | Type | Description |
|---|---|---|
| type | String | "photo" or "video". |
| media | String | Either an HTTP URL to fetch, or the fileId of something already in Migram's media store -- reusing a fileId skips re-uploading the file. |
| caption | String | Optional. Caption for this one item. |
What to send instead of the media or link, when a user picks an InlineQueryResult that carries one of these.
| Field | Type | Description |
|---|---|---|
| messageText | String | Optional. The text to send. |
| parseMode | String | Optional. How to parse formatting out of messageText (Markdown or HTML). |
One entry in an inline-query answer, returned via answerInlineQuery. An answer can carry at most 50 of these.
| Field | Type | Description |
|---|---|---|
| type | String | "article", "photo", "gif", "video", "document", "voice", or "sticker". |
| id | String | Distinguishes this result from the others in the same answer. |
| title | String | Optional. Shown as the result's title. |
| description | String | Optional. A short line shown under the title. |
| url | String | Optional. Associated URL, used for "article" results. |
| thumbUrl | String | Optional. Thumbnail image URL. |
| mediaUrl | String | Optional. What to send if this result is picked, for "photo", "gif", "video", "document", or "voice" results. |
| fileId | String | Optional. An existing file's ID on Migram's media store, in place of mediaUrl, to avoid re-uploading. |
| inputMessageContent | InputMessageContent | Optional. Overrides what gets sent when this result is picked -- an object with messageText and an optional parseMode. |
| replyMarkup | InlineKeyboardMarkup | Optional. Inline keyboard to attach to the message this result produces. |
Marks up one span of message text -- a bold run, a link, a custom emoji, and so on. Offsets and lengths count UTF-16 code units, the same units JavaScript uses to index strings; most emoji take up 2 units, not 1, so don't assume one character equals one unit.
| Field | Type | Description |
|---|---|---|
| type | String | "bold", "italic", "code", "pre", "text_link", "spoiler", "blockquote", "strikethrough", "underline", "mention", "url", "bot_command", or "custom_emoji". |
| offset | Integer | Where the entity starts, in UTF-16 code units. |
| length | Integer | How long the entity runs, in UTF-16 code units. |
| url | String | Optional. Opened when the user taps the text. Only set for "text_link". |
| language | String | Optional. Programming language of the code block. Only set for "pre". |
| customEmojiId | String | Optional. Which custom emoji. Only set for "custom_emoji". |
Describes your bot itself -- what getMe returns.
| Field | Type | Description |
|---|---|---|
| id | String | This bot's identifier. |
| username | String | Its username. |
| displayName | String | Its display name. |
| description | String | Shown on its profile before someone starts a chat with it. |
| isBot | Boolean | True, always -- getMe only ever describes a bot. |
| commands | Array of BotCommand | Optional. Its currently configured command list. |
| menuButton | MenuButton | Optional. Its currently configured menu button, when one is set. |
Call any method athttps://api.migram.org/bot/METHOD_NAME(for examplehttps://api.migram.org/bot/sendMessage), with anAuthorization: Bearer <botId>:<secret>header and a JSON body.
Most methods use POST. getMyCommands and getWebhookInfo use GET; getUpdates accepts GET or POST.
Every response is a JSON object. Success always includes"ok": true: message-sending methods nest the new message's identifiers under aresultobject, read and configuration methods nest their one payload under a single named key specific to that method (see Making requests for the full list), and methods with nothing to report respond with just{ "ok": true }. The four update-delivery methods below always use aresulton success and includeok,error_code,description, anderroron failure. Other methods keep their existing response shapes.
Method names below are the same whichever calling convention you use; this section documents native parameter names and response shapes. Calling these methods Telegram-style instead? See Telegram-compatible mode.
Confirms the bot's token works and hands back the bot's own profile. Takes no parameters. On success, the profile comes back nested under a "bot" key -- not spread as top-level fields -- as a Bot object. When nothing has been configured yet, commands and menuButton come back as null rather than omitted, since getMe always includes both keys.
curl -X POST https://api.migram.org/bot/getMe \ -H "Authorization: Bearer 65f2c8a1b3e4d5f6a7b8c9d0:EXAMPLE-TOKEN-DO-NOT-USE"
{
"ok": true,
"bot": {
"id": "65f2c8a1b3e4d5f6a7b8c9d0",
"username": "qatestbot",
"displayName": "QA Test Bot",
"description": "",
"isBot": true,
"commands": null,
"menuButton": null
}
}
Receives incoming updates with polling. Send a GET with query parameters or a POST with a JSON body to https://api.migram.org/bot/getUpdates. Run exactly one poller for each bot.
| Parameter | Type | Required | Description |
|---|---|---|---|
| offset | Integer (int64) | Optional | Identifier of the first update to return. An update is confirmed and deleted server-side as soon as this is greater than its update_id. A negative value -N returns only the newest N updates and forgets all earlier updates. |
| limit | Integer | Optional | Number of updates to return, from 1 to 100. Defaults to 100. |
| timeout | Integer | Optional | Long-poll timeout in seconds, from 0 to 50. The default, 0, makes a short poll. A positive value holds the request until an update arrives or the timeout expires. |
Success returns { "ok": true, "result": [updates] }. Each Update has a numeric, per-bot increasing update_id, a type, and one matching message, callback_query, or inline_query object. These objects use the same snake_case fields as webhook payloads.
{
"ok": true,
"result": [
{
"update_id": 1042,
"type": "message",
"message": {
"id": "6620a2b4e4d5f6a7b8c9d0e2",
"from": { "id": "661fa901e4d5f6a7b8c9d0df", "displayName": "Alex" },
"conversation_id": "6620a1f3e4d5f6a7b8c9d0e1",
"text": "/start",
"timestamp": "2026-07-20T18:30:00Z"
}
}
]
}
Repeated calls without advancing offset return the same unconfirmed updates. After processing update 1042, call again with offset=1043 to confirm it. Updates are stored for at most 24 hours. With a negative offset, for example offset=-10, only the newest 10 updates are returned and any earlier queued updates are forgotten.
If a webhook is active, the method returns HTTP 409 with description Conflict: can't use getUpdates method while webhook is active; use deleteWebhook to delete the webhook first. Clear it with deleteWebhook before polling.
A newer getUpdates call terminates an older concurrent call. The older request receives HTTP 409 with description Conflict: terminated by other getUpdates request; make sure that only one bot instance is running. Run exactly one poller.
Failures use { "ok": false, "error_code": <HTTP status>, "description": "...", "error": "..." }, and the HTTP status always matches error_code.
Registers the URL that receives signed update payloads. Send a POST JSON body to https://api.migram.org/bot/setWebhook.
| Parameter | Type | Required | Description |
|---|---|---|---|
| url | String | Yes | A public HTTPS URL. Internal and localhost targets are rejected in production. An empty string behaves like deleteWebhook. |
| dropPendingUpdates | Boolean | Optional | Set to true to discard all queued updates. Defaults to false, keeping queued updates and delivering them to the new webhook. |
While a webhook is set, getUpdates returns HTTP 409. Success returns { "ok": true, "result": true, "description": "Webhook was set" }. Errors return { "ok": false, "error_code": <HTTP status>, "description": "...", "error": "..." } with the matching HTTP status.
Clears the current webhook so queued and future updates can be read with getUpdates. Send a POST JSON body to https://api.migram.org/bot/deleteWebhook.
| Parameter | Type | Required | Description |
|---|---|---|---|
| dropPendingUpdates | Boolean | Optional | Set to true to discard queued updates. Defaults to false, making the queue available to getUpdates after the webhook is cleared. |
Success returns { "ok": true, "result": true, "description": "Webhook was deleted" }. Errors return { "ok": false, "error_code": <HTTP status>, "description": "...", "error": "..." } with the matching HTTP status.
Returns the bot's current webhook status. Send a GET request with no parameters to https://api.migram.org/bot/getWebhookInfo.
{
"ok": true,
"result": {
"url": "https://example.com/migram-webhook",
"has_custom_certificate": false,
"pending_update_count": 3
}
}
When no webhook is set, url is empty. pending_update_count is the number of updates waiting for delivery. Errors return { "ok": false, "error_code": <HTTP status>, "description": "...", "error": "..." } with the matching HTTP status.
Sends a text message to a conversation. On success, the new message's messageId and conversationId come back in a result object.
| Parameter | Type | Required | Description |
|---|---|---|---|
| conversationId | String | Yes | Which conversation to post into. |
| text | String | Yes | The message body. |
| parseMode | String | Optional | How to parse formatting out of text. See formatting options. |
| replyMarkup | Array of Array of InlineKeyboardButton | Optional | An inline keyboard, one row per inner array. Each button's callbackData tops out at 64 bytes (emoji count as 4 bytes each). |
| messageEffectId | String | Optional | Plays this effect when the message lands: one of confetti, fire, heart, party, thumbsup, or thumbsdown. |
curl -X POST https://api.migram.org/bot/sendMessage \
-H "Authorization: Bearer 65f2c8a1b3e4d5f6a7b8c9d0:EXAMPLE-TOKEN-DO-NOT-USE" \
-H "Content-Type: application/json" \
-d '{
"conversationId": "6620a1f3e4d5f6a7b8c9d0e1",
"text": "Pick an option:",
"replyMarkup": [
[
{ "text": "Option A", "callbackData": "opt_a" },
{ "text": "Option B", "callbackData": "opt_b" }
]
]
}'
{
"ok": true,
"result": {
"messageId": "6620a2b4e4d5f6a7b8c9d0e2",
"conversationId": "6620a1f3e4d5f6a7b8c9d0e1"
}
}
Streams a draft into a conversation before it's finalized -- useful for a typing-style incremental reply. Responds with { "ok": true } once accepted.
| Parameter | Type | Required | Description |
|---|---|---|---|
| conversationId | String | Yes | The target conversation. |
| messageId | String | Yes | An ID you assign and keep reusing across updates, so each call replaces the same draft bubble instead of creating a new one. |
| text | String | Optional | The draft's current text. |
| parseMode | String | Optional | Formatting mode for the draft text. See formatting options. |
| isFinal | Boolean | Optional | True to seal the draft into a regular, persisted message. |
Shows a transient status indicator (like "typing...") in the conversation, so the user knows a reply is coming before it arrives. The indicator clears automatically once your bot actually sends something. Responds with { "ok": true }.
| Parameter | Type | Required | Description |
|---|---|---|---|
| conversationId | String | Yes | Where to show the indicator. |
| action | String | Yes | Which indicator to show, matched to what's coming: typing for text, upload_photo for photos, record_video or upload_video for videos, record_voice or upload_voice for voice notes, upload_document for files, choose_sticker for stickers, or find_location for location data. |
Sends a photo by URL. On success, the new message's messageId and conversationId come back in a result object.
| Parameter | Type | Required | Description |
|---|---|---|---|
| conversationId | String | Yes | Destination conversation. |
| photoUrl | String | Yes | HTTP URL Migram will fetch the photo from. |
| caption | String | Optional | Text shown under the photo. |
Sends any file as a generic document. On success, the new message's messageId and conversationId come back in a result object.
| Parameter | Type | Required | Description |
|---|---|---|---|
| conversationId | String | Yes | Which conversation gets the file. |
| documentUrl | String | Yes | HTTP URL of the file. |
| fileName | String | Optional | Name shown to the recipient. |
| caption | String | Optional | Text shown under the file. |
Sends a video by URL. On success, the new message's messageId and conversationId come back in a result object.
| Parameter | Type | Required | Description |
|---|---|---|---|
| conversationId | String | Yes | Destination conversation. |
| videoUrl | String | Yes | HTTP URL of the video. |
| caption | String | Optional | Text shown under the video. |
| duration | Integer | Optional | Length in seconds, if known. |
| width | Integer | Optional | Pixel width, if known. |
| height | Integer | Optional | Pixel height, if known. |
Sends a voice note by URL. On success, the new message's messageId and conversationId come back in a result object.
| Parameter | Type | Required | Description |
|---|---|---|---|
| conversationId | String | Yes | Destination conversation. |
| voiceUrl | String | Yes | HTTP URL of the audio. |
| caption | String | Optional | Text shown under the voice note. |
| duration | Float | Optional | Length in seconds, if known. |
Sends a looping animation (GIF-style) by URL. On success, the new message's messageId and conversationId come back in a result object.
| Parameter | Type | Required | Description |
|---|---|---|---|
| conversationId | String | Yes | Destination conversation. |
| animationUrl | String | Yes | HTTP URL of the animation. |
| caption | String | Optional | Text shown under the animation. |
Sends a sticker from Migram's catalog. On success, the new message's messageId and conversationId come back in a result object.
| Parameter | Type | Required | Description |
|---|---|---|---|
| conversationId | String | Yes | Destination conversation. |
| stickerId | String | Yes | ID of a catalog sticker. There's no URL upload path for stickers -- pick one that already exists in Migram's sticker catalog. |
Sending a stickerUrl field instead is rejected outright with HTTP 400 ("stickerUrl is no longer supported -- send a catalog sticker by stickerId"); that parameter was removed, and stickerId is now the only way to say which sticker to send.
Drops a pin at a fixed latitude and longitude. On success, the new message's messageId and conversationId come back in a result object.
| Parameter | Type | Required | Description |
|---|---|---|---|
| conversationId | String | Yes | Destination conversation. |
| lat | Float | Yes | Latitude. Not range-checked server-side, so validate it yourself before sending. |
| lng | Float | Yes | Longitude. Not range-checked server-side, so validate it yourself before sending. |
| caption | String | Optional | Text shown alongside the pin. |
Sends a named place, distinct from a bare sendLocation pin. On success, the new message's messageId and conversationId come back in a result object.
| Parameter | Type | Required | Description |
|---|---|---|---|
| conversationId | String | Yes | Destination conversation. |
| latitude | Float | Yes | Venue latitude. |
| longitude | Float | Yes | Venue longitude. |
| title | String | Yes | Venue name. |
| address | String | Yes | Venue address. |
| foursquareId | String | Optional | Foursquare place ID, if you have one. |
Shares a phone contact card. On success, the new message's messageId and conversationId come back in a result object.
| Parameter | Type | Required | Description |
|---|---|---|---|
| conversationId | String | Yes | Destination conversation. |
| phoneNumber | String | Yes | The contact's phone number. |
| firstName | String | Yes | The contact's first name. |
| lastName | String | Optional | The contact's last name. |
Sends an animated emoji that lands on a random value, like a physical dice roll or slot pull. On success, the new message's messageId and conversationId come back in a result object.
| Parameter | Type | Required | Description |
|---|---|---|---|
| conversationId | String | Yes | Where to roll. |
| emoji | String | Optional | One of 🎲 🎯 🏀 ⚽ 🎰 🎳; defaults to 🎲. The value it lands on ranges 1-6 for 🎲, 🎯, and 🎳; 1-5 for 🏀 and ⚽; and 1-64 for 🎰. |
Posts a native poll. On success, the new message's messageId and conversationId come back in a result object.
| Parameter | Type | Required | Description |
|---|---|---|---|
| conversationId | String | Yes | Where to post the poll. |
| question | String | Yes | 1-300 bytes (UTF-8). Emoji and non-Latin text use several bytes per character, so they use up the budget faster than plain ASCII. |
| options | Array of String | Yes | 2-10 answer strings, each 1-100 bytes (UTF-8) -- same byte-counting caveat as question. |
| type | String | Optional | regular or quiz. Defaults to regular. |
| isAnonymous | Boolean | Optional | Whether to hide who voted for what. |
| allowsMultipleAnswers | Boolean | Optional | Whether a voter can pick more than one option. Ignored for quiz polls. |
| correctOptionId | Integer | Optional | 0-based index of the right answer. Required for quiz polls. |
| explanation | String | Optional | Shown to anyone who picks the wrong answer in a quiz poll. |
Sends 2 to 10 photos, videos, or documents together as a single album. On success, the album's messageId and conversationId come back in a result object.
| Parameter | Type | Required | Description |
|---|---|---|---|
| conversationId | String | Yes | Destination conversation. |
| media | Array of InputMedia | Yes | 2-10 items to include in the album. |
Replaces the bot's command list. Every entry needs a non-blank command and description, or the whole call is rejected. On success, the updated list comes back under a commands key.
| Parameter | Type | Required | Description |
|---|---|---|---|
| commands | Array of BotCommand | Yes | The full command list to set, as JSON. |
| scope | BotCommandScope | Optional | Which chats this list applies to. Omit it to set the default scope. |
| languageCode | String | Optional | Two-letter ISO 639-1 code. Empty defaults to en. |
curl -X POST https://api.migram.org/bot/setMyCommands \
-H "Authorization: Bearer 65f2c8a1b3e4d5f6a7b8c9d0:EXAMPLE-TOKEN-DO-NOT-USE" \
-H "Content-Type: application/json" \
-d '{
"commands": [
{ "command": "start", "description": "Start the bot" },
{ "command": "help", "description": "Show available commands" }
]
}'
{
"ok": true,
"commands": [
{ "command": "start", "description": "Start the bot" },
{ "command": "help", "description": "Show available commands" }
]
}
Looks up the bot's current commands for a scope and language. Unlike almost every other method, this one is a GET with the parameters below passed as query-string arguments, not a JSON body. On success, the matching list comes back under a commands key, as an array of BotCommand.
| Parameter | Type | Required | Description |
|---|---|---|---|
| scope | String | Optional | Which BotCommandScope type to look up (default, all_private_chats, all_group_chats, chat, chat_member). Only the type is matched -- this can't distinguish between different chats or users sharing the same scope type. |
| languageCode | String | Optional | Two-letter ISO 639-1 code. Empty defaults to en. |
Leave scope out, or request a scope/language pair nothing was ever set for via setMyCommands, and this falls back to the bot's default-scope list.
Clears the bot's default-scope command list. Takes no parameters. On success, returns the now-empty list: { "ok": true, "commands": [] }.
Changes the bot's MenuButton. This is bot-wide, not per-chat. On success, returns the updated button (null when its type is default).
| Parameter | Type | Required | Description |
|---|---|---|---|
| type | String | Optional | default, commands, or web_app. Defaults to default when left out. |
| text | String | Optional | Button label. Defaults to Menu for type commands and Open for type web_app; ignored for type default. |
| url | String | Optional | HTTPS URL to open as a Web App. Required when type is web_app. |
Acknowledges a button press, optionally showing the user something -- a brief banner at the top of the screen, or a blocking alert. Responds with { "ok": true }.
| Parameter | Type | Required | Description |
|---|---|---|---|
| callbackQueryId | String | Yes | Which query to answer. |
| text | String | Optional | Message to show. Left out, nothing is shown. Capped at 200 bytes (UTF-8) -- non-Latin text eats into that faster than plain ASCII. |
| showAlert | Boolean | Optional | True to show a blocking alert instead of a banner. |
| url | String | Optional | URL for the client to open. |
| cacheTime | Integer | Optional | How many seconds the client may cache this answer for. |
These two methods change a message that's already in the conversation history, instead of sending a new one.
Replaces the text of a message your bot already sent. Editing to the exact same text it already has is a harmless no-op. Responds with { "ok": true }.
| Parameter | Type | Required | Description |
|---|---|---|---|
| conversationId | String | Yes | Conversation containing the message. |
| messageId | String | Yes | Which message to edit. |
| text | String | Yes | The replacement text. |
Removes a message your bot sent. Responds with { "ok": true }.
| Parameter | Type | Required | Description |
|---|---|---|---|
| conversationId | String | Yes | Conversation containing the message. |
| messageId | String | Yes | Which message to delete. |
Typing @yourbot followed by a query, in any chat's message field, opens inline mode without the user ever starting a direct conversation with the bot. Inline mode has to be turned on first, with /setinline to @botmigi. Every keystroke sends an updated InlineQuery through your configured update-delivery mode; reply to each one with answerInlineQuery.
Returns a results list for an inline query. Responds with { "ok": true }.
| Parameter | Type | Required | Description |
|---|---|---|---|
| inlineQueryId | String | Yes | Which query this answers. |
| results | Array of InlineQueryResult | Yes | Up to 50 results, sent as JSON. |
| cacheTime | Integer | Optional | How long clients may cache these results, in seconds. Defaults to 300. |
| isPersonal | Boolean | Optional | True if these results should only be cached for the querying user, not shared across users. |
| nextOffset | String | Optional | A cursor for the next page. Clients echo this back as offset on the follow-up query. |
Set a MenuButton's type to web_app to attach a Mini App that opens in place when the button is pressed. On launch, Migram hands the Mini App a signed initData string identifying the user who opened it; your backend should run it through the methods below before trusting anything in it.
Checks an initData string's signature and age, then parses it into its individual fields. On success, returns the parsed fields.
| Parameter | Type | Required | Description |
|---|---|---|---|
| initData | String | Yes | The initData string your Mini App received on launch. |
| maxAge | Integer | Optional | How many seconds old initData is allowed to be before it's rejected as expired. Defaults to 3600. The platform caps this at 24 hours regardless of what you pass -- a larger value doesn't buy you a longer window. |
Produces a signed initData string for testing your Mini App outside of Migram's own clients. On success, returns the generated string.
| Parameter | Type | Required | Description |
|---|---|---|---|
| userId | String | Yes | Whose launch data to generate. |
| firstName | String | Yes | That user's first name. |
| username | String | Optional | That user's username. |
| startParam | String | Optional | Deep-link start parameter to bake into the generated data. |
| queryId | String | Optional | An identifier to embed, so the Mini App can reference this session when it later posts a result back. |
Message text supports lightweight formatting. Pass parseMode to sendMessage or editMessageText to have Migram's clients parse it out of the text; leave parseMode out and the text is sent exactly as written, with no parsing.
Markdown
parseMode: "Markdown" enables **bold** for bold text, `code` for monospace, and [text](url) for links. Bare URLs, @usernames, and /commands are auto-linked without any extra markup.
HTML
parseMode: "HTML" enables a sanitized subset of HTML tags. Anything not on the allowed list -- tags or attributes -- is stripped before the message is stored, not merely hidden on render.
Either way, formatting can also be specified precisely with an array of MessageEntity objects instead, each carrying an offset and a length. Both are counted in UTF-16 code units, not characters -- most emoji take up 2 units.
@botmigi -- Migram's answer to BotFather -- is where bots get created and managed, entirely through chat. Send /newbot to start: you'll pick a display name and a username ending in bot, and @botmigi replies once with the token, in <botId>:<secret> form. It isn't shown again automatically -- use /token to have it re-sent, or /revoke to invalidate it and get a new one.
@botmigi commands:
/newbot -- create a bot, get its token/mybots -- list and manage the bots you own/setname -- rename a bot/setdescription -- change what's shown before someone starts a chat with the bot/setabouttext -- change the short blurb on the bot's profile/setuserpic -- change a bot's profile photo/setcommands -- set the command list shown when someone types / in a chat with the bot/deletebot -- permanently remove a bot/token -- re-send a bot's current token/revoke -- invalidate the current token and issue a new one/setinline -- turn on inline mode and set its placeholder text/setjoingroups -- allow or block adding the bot to group chats/setprivacy -- choose whether the bot sees every group message or just commands and replies aimed at it/setwebhook -- point update deliveries at a URL/help -- list what @botmigi can doFor how bot and user data get handled, see Migram's Bot Privacy Policy.
Migram is an independent project, not affiliated with, endorsed by, or associated with Telegram FZ-LLC or Telegram Messenger Inc.