Go up

Migram Bot API

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 /newbot in a chat with @botmigi before any of this becomes relevant.

Recent changes

July 17, 2026

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.

July 10, 2026

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.

Authorizing your bot

@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>

Getting a conversation ID

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:

  • Call getUpdates with your bot token and read message.conversation_id from the returned update.
  • Set a webhook and read that same 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.

Making requests

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:

  • Message-sending methods -- sendMessage, sendPhoto, and the rest of the send* family -- nest the new message's identifiers under a result object: { "ok": true, "result": { "messageId": "...", "conversationId": "..." } }
  • Methods with nothing to report back -- most edit, delete, and answer methods -- respond with exactly { "ok": true } and nothing else.
  • Read and configuration methods each wrap their one payload under a single named key that sits alongside 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.
  • The four update-delivery methods -- getUpdates, setWebhook, deleteWebhook, and getWebhookInfo -- always use { "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:

StatusMeaning
200Success. The body contains "ok": true.
400Bad request -- a required parameter is missing or failed validation. The error string names the problem.
401Unauthorized -- the token is missing ("unauthorized: missing bearer token") or invalid ("unauthorized: invalid token").
403Forbidden -- the conversation named by conversationId doesn't belong to your bot ("conversation does not belong to bot").
404Not found -- the method name, conversation, or message doesn't exist.
405Method not allowed -- wrong HTTP verb for this method. See each method section for its accepted verb.
409Conflict -- polling and webhook delivery are mutually exclusive, or another getUpdates request replaced the active poller.
429Too many requests -- see the rate-limit note above. Back off for at least retry_after seconds.
502Bad gateway -- the bot platform couldn't reach the Migram backend.

Getting updates

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.

Update

One update, describing one thing that happened. At most one of the payload fields below is present, matching whichever type the update carries.

FieldTypeDescription
update_idInteger or StringInteger 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.
typeStringWhat 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.
messageMessageOptional. Present when type is message.
callback_queryCallbackQueryOptional. Present when type is callback_query -- an inline-keyboard button was pressed.
inline_queryInlineQueryOptional. Present when type is inline_query.

Message

A message your bot received inside an Update of type message, delivered by getUpdates or webhook.

FieldTypeDescription
idStringUnique identifier for this message.
fromUserWho sent it.
conversation_idStringWhich conversation this message belongs to.
textStringOptional. The message text.
photo_urlStringOptional. URL of an attached photo, set on photo-upload flows that forward an image instead of text (for example @botmigi's /setuserpic).
photo_idStringOptional. File ID of the attached photo, alongside photo_url.
timestampStringWhen the message was sent, RFC 3339.

CallbackQuery

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.

FieldTypeDescription
idStringIdentifies this specific button press.
fromUserWho pressed it.
conversation_idStringConversation containing the message the button is attached to.
dataStringThe pressed button's callbackData, unchanged, up to 64 bytes.
message_idStringThe message the button belongs to.

InlineQuery

Delivered as a user types after @yourbot in any chat's message field. Reply with answerInlineQuery.

FieldTypeDescription
idStringIdentifies this query.
fromUserWho's typing.
queryStringThe text typed so far, up to 256 characters.
offsetStringPagination cursor your bot previously returned as nextOffset; empty on the first request for a given query text.
conversation_idStringWhere the query is being typed.

Available types

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.

User

The sender of something your bot received -- shows up in the from field of Update payloads (Message, CallbackQuery, InlineQuery).

FieldTypeDescription
idStringThis user's identifier.
displayNameStringTheir display name.

InlineKeyboardMarkup

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).

FieldTypeDescription
inlineKeyboardArray of Array of InlineKeyboardButtonButton rows, outer array top to bottom, inner array left to right within a row.

InlineKeyboardButton

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.

FieldTypeDescription
textStringThe label shown on the button.
callbackDataStringOptional. 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.
urlStringOptional. Opens this HTTP or app URL when pressed.
webAppWebAppInfoOptional. Launches this Mini App when pressed -- a single-field object holding a url.
switchInlineQueryStringOptional. 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.
styleStringOptional. Visual accent: "primary", "danger" or "success".
iconCustomEmojiIdStringOptional. A custom emoji shown as the button's icon.

BotCommand

One entry in your bot's command menu, set via setMyCommands.

FieldTypeDescription
commandStringThe command text. Required -- can't be blank.
descriptionStringWhat it does, shown next to it in the menu. Required -- can't be blank.

BotCommandScope

Targets a command set to a particular audience. Used by setMyCommands, getMyCommands, and deleteMyCommands.

FieldTypeDescription
typeStringOne 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).
chatIdStringOptional. The target chat. Required when type is "chat" or "chat_member".
userIdStringOptional. The target user. Required when type is "chat_member".

MenuButton

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.

FieldTypeDescription
typeString"default" (no custom button), "commands" (opens the command list), or "web_app" (launches a Mini App).
textStringOptional. Button label. Defaults to "Menu" for "commands", "Open" for "web_app".
urlStringOptional. Mini App URL to open. Required when type is "web_app".

WebAppInfo

Points at a Mini App -- Migram's term for an in-chat web app -- to launch. Referenced from InlineKeyboardButton's webApp field.

FieldTypeDescription
urlStringThe Mini App's URL.

Poll

A poll message, as sent by sendPoll.

FieldTypeDescription
questionStringThe 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.
optionsArray of PollOptionBetween 2 and 10 answer options.
typeString"regular" or "quiz".
isAnonymousBooleanWhether voters are hidden from each other.
allowsMultipleAnswersBooleanWhether a voter can pick more than one option.
correctOptionIdIntegerOptional. 0-based index of the correct option. Only meaningful for "quiz" polls.
explanationStringOptional. Shown to anyone who picks a wrong answer in a "quiz" poll.

PollOption

One answer choice inside a Poll.

FieldTypeDescription
textStringUp to 100 bytes (see the byte-vs-character note on Poll).
voterCountIntegerHow many users picked this option.

InputMedia

One item inside a sendMediaGroup call. A group needs 2-10 of these.

FieldTypeDescription
typeString"photo" or "video".
mediaStringEither an HTTP URL to fetch, or the fileId of something already in Migram's media store -- reusing a fileId skips re-uploading the file.
captionStringOptional. Caption for this one item.

InputMessageContent

What to send instead of the media or link, when a user picks an InlineQueryResult that carries one of these.

FieldTypeDescription
messageTextStringOptional. The text to send.
parseModeStringOptional. How to parse formatting out of messageText (Markdown or HTML).

InlineQueryResult

One entry in an inline-query answer, returned via answerInlineQuery. An answer can carry at most 50 of these.

FieldTypeDescription
typeString"article", "photo", "gif", "video", "document", "voice", or "sticker".
idStringDistinguishes this result from the others in the same answer.
titleStringOptional. Shown as the result's title.
descriptionStringOptional. A short line shown under the title.
urlStringOptional. Associated URL, used for "article" results.
thumbUrlStringOptional. Thumbnail image URL.
mediaUrlStringOptional. What to send if this result is picked, for "photo", "gif", "video", "document", or "voice" results.
fileIdStringOptional. An existing file's ID on Migram's media store, in place of mediaUrl, to avoid re-uploading.
inputMessageContentInputMessageContentOptional. Overrides what gets sent when this result is picked -- an object with messageText and an optional parseMode.
replyMarkupInlineKeyboardMarkupOptional. Inline keyboard to attach to the message this result produces.

MessageEntity

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.

FieldTypeDescription
typeString"bold", "italic", "code", "pre", "text_link", "spoiler", "blockquote", "strikethrough", "underline", "mention", "url", "bot_command", or "custom_emoji".
offsetIntegerWhere the entity starts, in UTF-16 code units.
lengthIntegerHow long the entity runs, in UTF-16 code units.
urlStringOptional. Opened when the user taps the text. Only set for "text_link".
languageStringOptional. Programming language of the code block. Only set for "pre".
customEmojiIdStringOptional. Which custom emoji. Only set for "custom_emoji".

Bot

Describes your bot itself -- what getMe returns.

FieldTypeDescription
idStringThis bot's identifier.
usernameStringIts username.
displayNameStringIts display name.
descriptionStringShown on its profile before someone starts a chat with it.
isBotBooleanTrue, always -- getMe only ever describes a bot.
commandsArray of BotCommandOptional. Its currently configured command list.
menuButtonMenuButtonOptional. Its currently configured menu button, when one is set.

Available methods

Call any method at https://api.migram.org/bot/METHOD_NAME (for example https://api.migram.org/bot/sendMessage), with an Authorization: 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 a result object, 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 a result on success and include ok, error_code, description, and error on failure. Other methods keep their existing response shapes.

getMe

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
  }
}

getUpdates

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.

ParameterTypeRequiredDescription
offsetInteger (int64)OptionalIdentifier 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.
limitIntegerOptionalNumber of updates to return, from 1 to 100. Defaults to 100.
timeoutIntegerOptionalLong-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.

setWebhook

Registers the URL that receives signed update payloads. Send a POST JSON body to https://api.migram.org/bot/setWebhook.

ParameterTypeRequiredDescription
urlStringYesA public HTTPS URL. Internal and localhost targets are rejected in production. An empty string behaves like deleteWebhook.
dropPendingUpdatesBooleanOptionalSet 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.

deleteWebhook

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.

ParameterTypeRequiredDescription
dropPendingUpdatesBooleanOptionalSet 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.

getWebhookInfo

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.

sendMessage

Sends a text message to a conversation. On success, the new message's messageId and conversationId come back in a result object.

ParameterTypeRequiredDescription
conversationIdStringYesWhich conversation to post into.
textStringYesThe message body.
parseModeStringOptionalHow to parse formatting out of text. See formatting options.
replyMarkupArray of Array of InlineKeyboardButtonOptionalAn inline keyboard, one row per inner array. Each button's callbackData tops out at 64 bytes (emoji count as 4 bytes each).
messageEffectIdStringOptionalPlays 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"
  }
}

sendMessageDraft

Streams a draft into a conversation before it's finalized -- useful for a typing-style incremental reply. Responds with { "ok": true } once accepted.

ParameterTypeRequiredDescription
conversationIdStringYesThe target conversation.
messageIdStringYesAn ID you assign and keep reusing across updates, so each call replaces the same draft bubble instead of creating a new one.
textStringOptionalThe draft's current text.
parseModeStringOptionalFormatting mode for the draft text. See formatting options.
isFinalBooleanOptionalTrue to seal the draft into a regular, persisted message.

sendChatAction

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 }.

ParameterTypeRequiredDescription
conversationIdStringYesWhere to show the indicator.
actionStringYesWhich 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.

sendPhoto

Sends a photo by URL. On success, the new message's messageId and conversationId come back in a result object.

ParameterTypeRequiredDescription
conversationIdStringYesDestination conversation.
photoUrlStringYesHTTP URL Migram will fetch the photo from.
captionStringOptionalText shown under the photo.

sendDocument

Sends any file as a generic document. On success, the new message's messageId and conversationId come back in a result object.

ParameterTypeRequiredDescription
conversationIdStringYesWhich conversation gets the file.
documentUrlStringYesHTTP URL of the file.
fileNameStringOptionalName shown to the recipient.
captionStringOptionalText shown under the file.

sendVideo

Sends a video by URL. On success, the new message's messageId and conversationId come back in a result object.

ParameterTypeRequiredDescription
conversationIdStringYesDestination conversation.
videoUrlStringYesHTTP URL of the video.
captionStringOptionalText shown under the video.
durationIntegerOptionalLength in seconds, if known.
widthIntegerOptionalPixel width, if known.
heightIntegerOptionalPixel height, if known.

sendVoice

Sends a voice note by URL. On success, the new message's messageId and conversationId come back in a result object.

ParameterTypeRequiredDescription
conversationIdStringYesDestination conversation.
voiceUrlStringYesHTTP URL of the audio.
captionStringOptionalText shown under the voice note.
durationFloatOptionalLength in seconds, if known.

sendAnimation

Sends a looping animation (GIF-style) by URL. On success, the new message's messageId and conversationId come back in a result object.

ParameterTypeRequiredDescription
conversationIdStringYesDestination conversation.
animationUrlStringYesHTTP URL of the animation.
captionStringOptionalText shown under the animation.

sendSticker

Sends a sticker from Migram's catalog. On success, the new message's messageId and conversationId come back in a result object.

ParameterTypeRequiredDescription
conversationIdStringYesDestination conversation.
stickerIdStringYesID 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.

sendLocation

Drops a pin at a fixed latitude and longitude. On success, the new message's messageId and conversationId come back in a result object.

ParameterTypeRequiredDescription
conversationIdStringYesDestination conversation.
latFloatYesLatitude. Not range-checked server-side, so validate it yourself before sending.
lngFloatYesLongitude. Not range-checked server-side, so validate it yourself before sending.
captionStringOptionalText shown alongside the pin.

sendVenue

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.

ParameterTypeRequiredDescription
conversationIdStringYesDestination conversation.
latitudeFloatYesVenue latitude.
longitudeFloatYesVenue longitude.
titleStringYesVenue name.
addressStringYesVenue address.
foursquareIdStringOptionalFoursquare place ID, if you have one.

sendContact

Shares a phone contact card. On success, the new message's messageId and conversationId come back in a result object.

ParameterTypeRequiredDescription
conversationIdStringYesDestination conversation.
phoneNumberStringYesThe contact's phone number.
firstNameStringYesThe contact's first name.
lastNameStringOptionalThe contact's last name.

sendDice

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.

ParameterTypeRequiredDescription
conversationIdStringYesWhere to roll.
emojiStringOptionalOne of 🎲 🎯 🏀 ⚽ 🎰 🎳; defaults to 🎲. The value it lands on ranges 1-6 for 🎲, 🎯, and 🎳; 1-5 for 🏀 and ⚽; and 1-64 for 🎰.

sendPoll

Posts a native poll. On success, the new message's messageId and conversationId come back in a result object.

ParameterTypeRequiredDescription
conversationIdStringYesWhere to post the poll.
questionStringYes1-300 bytes (UTF-8). Emoji and non-Latin text use several bytes per character, so they use up the budget faster than plain ASCII.
optionsArray of StringYes2-10 answer strings, each 1-100 bytes (UTF-8) -- same byte-counting caveat as question.
typeStringOptionalregular or quiz. Defaults to regular.
isAnonymousBooleanOptionalWhether to hide who voted for what.
allowsMultipleAnswersBooleanOptionalWhether a voter can pick more than one option. Ignored for quiz polls.
correctOptionIdIntegerOptional0-based index of the right answer. Required for quiz polls.
explanationStringOptionalShown to anyone who picks the wrong answer in a quiz poll.

sendMediaGroup

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.

ParameterTypeRequiredDescription
conversationIdStringYesDestination conversation.
mediaArray of InputMediaYes2-10 items to include in the album.

setMyCommands

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.

ParameterTypeRequiredDescription
commandsArray of BotCommandYesThe full command list to set, as JSON.
scopeBotCommandScopeOptionalWhich chats this list applies to. Omit it to set the default scope.
languageCodeStringOptionalTwo-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" }
  ]
}

getMyCommands

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.

ParameterTypeRequiredDescription
scopeStringOptionalWhich 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.
languageCodeStringOptionalTwo-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.

deleteMyCommands

Clears the bot's default-scope command list. Takes no parameters. On success, returns the now-empty list: { "ok": true, "commands": [] }.

setChatMenuButton

Changes the bot's MenuButton. This is bot-wide, not per-chat. On success, returns the updated button (null when its type is default).

ParameterTypeRequiredDescription
typeStringOptionaldefault, commands, or web_app. Defaults to default when left out.
textStringOptionalButton label. Defaults to Menu for type commands and Open for type web_app; ignored for type default.
urlStringOptionalHTTPS URL to open as a Web App. Required when type is web_app.

answerCallbackQuery

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 }.

ParameterTypeRequiredDescription
callbackQueryIdStringYesWhich query to answer.
textStringOptionalMessage to show. Left out, nothing is shown. Capped at 200 bytes (UTF-8) -- non-Latin text eats into that faster than plain ASCII.
showAlertBooleanOptionalTrue to show a blocking alert instead of a banner.
urlStringOptionalURL for the client to open.
cacheTimeIntegerOptionalHow many seconds the client may cache this answer for.

Updating messages

These two methods change a message that's already in the conversation history, instead of sending a new one.

editMessageText

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 }.

ParameterTypeRequiredDescription
conversationIdStringYesConversation containing the message.
messageIdStringYesWhich message to edit.
textStringYesThe replacement text.

deleteMessage

Removes a message your bot sent. Responds with { "ok": true }.

ParameterTypeRequiredDescription
conversationIdStringYesConversation containing the message.
messageIdStringYesWhich message to delete.

Inline mode

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.

answerInlineQuery

Returns a results list for an inline query. Responds with { "ok": true }.

ParameterTypeRequiredDescription
inlineQueryIdStringYesWhich query this answers.
resultsArray of InlineQueryResultYesUp to 50 results, sent as JSON.
cacheTimeIntegerOptionalHow long clients may cache these results, in seconds. Defaults to 300.
isPersonalBooleanOptionalTrue if these results should only be cached for the querying user, not shared across users.
nextOffsetStringOptionalA cursor for the next page. Clients echo this back as offset on the follow-up query.

Web Apps

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.

validateWebAppData

Checks an initData string's signature and age, then parses it into its individual fields. On success, returns the parsed fields.

ParameterTypeRequiredDescription
initDataStringYesThe initData string your Mini App received on launch.
maxAgeIntegerOptionalHow 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.

generateWebAppInitData

Produces a signed initData string for testing your Mini App outside of Migram's own clients. On success, returns the generated string.

ParameterTypeRequiredDescription
userIdStringYesWhose launch data to generate.
firstNameStringYesThat user's first name.
usernameStringOptionalThat user's username.
startParamStringOptionalDeep-link start parameter to bake into the generated data.
queryIdStringOptionalAn identifier to embed, so the Mini App can reference this session when it later posts a result back.

Formatting options

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.

Local development

This appendix is only for contributors running the bot-platform service itself. Bots using Migram's public API should call https://api.migram.org. A locally running service uses http://localhost:3002, so the local endpoint pattern is:

http://localhost:3002/bot/METHOD_NAME

The request shape and Authorization: Bearer <botId>:<secret> header are otherwise unchanged. Production webhook URLs must still be public HTTPS URLs; a local target is only useful with a local bot-platform instance.

BotMigi

@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 do

For 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.