Migram Bot API

Every Migram bot starts life inside a chat with @botmigi, then connects to your code through the public Bot API.

Need the exhaustive version -- every method, type, and webhook shape? The full Bot API reference covers all of it on one page.

Getting started with BotMigi

This walkthrough takes a fresh idea to a working bot: create it, confirm it's alive, find a conversation, send a message, choose how to receive updates, and react to a button press. The token below is a placeholder -- swap in your own and the same calls will work as shown.

1. Create the bot

Open a direct chat with @botmigi and send /newbot. It asks for a display name first, then a username. The username has to be lowercase, start with a letter, and end in bot -- for example qatestbot.

2. Save the access token

Once the username is accepted, BotMigi hands back the token your requests will authenticate with, shaped as <botId>:<secret>:

Here's the token you'll use for the HTTP API:
65f2c8a1b3e4d5f6a7b8c9d0:EXAMPLE-TOKEN-DO-NOT-USE

Treat this string like a password -- anyone holding it can act as your bot. If it ever leaks, run /revoke in @botmigi to kill it and mint a replacement; /token re-shows the current one.

3. Confirm it's alive with getMe

All Bot API calls use the public https://api.migram.org/bot/<method> endpoint:

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": [],
    "menuButton": { "type": "default" }
  }
}

4. Get your conversation ID

A bot needs a conversation ID before it can send a message. The quickest route is:

  1. Create your bot with @botmigi and copy its token.
  2. Open the new bot's chat in Migram.
  3. Send /start to the bot.
  4. Call getUpdates with the token:
    curl -X POST https://api.migram.org/bot/getUpdates \
      -H "Authorization: Bearer 65f2c8a1b3e4d5f6a7b8c9d0:EXAMPLE-TOKEN-DO-NOT-USE" \
      -H "Content-Type: application/json" \
      -d '{"timeout":0}'
  5. Read result[0].message.conversation_id, then pass that value as conversationId to sendMessage.

5. Send your first message

Address a conversation the bot belongs to by ID. Text-sending methods answer with the new message's ID nested under result:

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":"8f1e2d3c4b5a6978","text":"Hello from my bot"}'
{
  "ok": true,
  "result": {
    "messageId": "5f3e2a1b9c8d7e6f5a4b3c2d",
    "conversationId": "8f1e2d3c4b5a6978"
  }
}

6. Choose how to receive updates

For the simplest setup, keep calling getUpdates; polling needs no public server. For push delivery, call setWebhook with a public HTTPS URL. The two modes are mutually exclusive, so getUpdates returns HTTP 409 while a webhook is set. See Receiving updates for setup details and payloads.

7. Handle a button press end to end

Send a message with an inline keyboard, then react once someone taps it:

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": "8f1e2d3c4b5a6978",
    "text": "Pick one:",
    "replyMarkup": [[
      { "text": "Yes", "callbackData": "confirm:yes" },
      { "text": "No",  "callbackData": "confirm:no" }
    ]]
  }'

A tap on either button produces a callback_query update. The sample below is shown as returned by getUpdates; webhook deliveries carry the same fields but with a UUID string update_id instead:

{
  "update_id": 1042,
  "type": "callback_query",
  "callback_query": {
    "id": "7a6b5c4d3e2f1908",
    "from": { "id": "9c1a2b3d4e5f6071", "displayName": "Jordan" },
    "conversation_id": "8f1e2d3c4b5a6978",
    "data": "confirm:yes",
    "message_id": "5f3e2a1b9c8d7e6f5a4b3c2d"
  }
}

Clear the button's spinner right away, then swap the message content in place:

curl -X POST https://api.migram.org/bot/answerCallbackQuery \
  -H "Authorization: Bearer 65f2c8a1b3e4d5f6a7b8c9d0:EXAMPLE-TOKEN-DO-NOT-USE" \
  -H "Content-Type: application/json" \
  -d '{"callbackQueryId":"7a6b5c4d3e2f1908","text":"Got it"}'

curl -X POST https://api.migram.org/bot/editMessageText \
  -H "Authorization: Bearer 65f2c8a1b3e4d5f6a7b8c9d0:EXAMPLE-TOKEN-DO-NOT-USE" \
  -H "Content-Type: application/json" \
  -d '{"conversationId":"8f1e2d3c4b5a6978","messageId":"5f3e2a1b9c8d7e6f5a4b3c2d","text":"Confirmed: yes"}'

Both calls reply with a bare {"ok": true} -- there's nothing further to report.

Authentication

Authenticate every call with the token BotMigi gave you, sent as a bearer credential:

POST https://api.migram.org/bot/sendMessage
Authorization: Bearer <botId>:<secret>
Content-Type: application/json

The public base URL is https://api.migram.org/bot/<method>. Method names are matched exactly and are case-sensitive, so a mistyped path such as /bot/Sendmessage will not resolve.

A missing or malformed Authorization header, or a token that doesn't match a registered bot, gets a 401 before your request body is even parsed.

Receiving updates

Migram delivers the same Update objects in two ways: getUpdates polling or webhooks. Polling is the simplest because it needs no public server; webhooks push updates to a public HTTPS URL. They are mutually exclusive: getUpdates returns HTTP 409 while a webhook is set.

Polling with getUpdates

Call https://api.migram.org/bot/getUpdates with your bearer token. For efficient long polling, set timeout to a value up to 50 seconds. After processing a batch, acknowledge it by making the next call with offset equal to the last update_id + 1. Omit offset on the first call.

curl -X POST https://api.migram.org/bot/getUpdates \
  -H "Authorization: Bearer 65f2c8a1b3e4d5f6a7b8c9d0:EXAMPLE-TOKEN-DO-NOT-USE" \
  -H "Content-Type: application/json" \
  -d '{"timeout":50,"offset":1043}'

A successful response has {"ok": true, "result": [...]}. Request parameters such as timeout and offset follow the API's camelCase convention; fields inside each returned update use snake_case.

Webhooks

Call https://api.migram.org/bot/setWebhook with your bearer token and a public HTTPS url. Migram then POSTs an Update object to that URL whenever your bot gets a message, a button press, or an inline query. In practice only three type values arrive: message, callback_query, and inline_query. See the full Bot API reference for method parameters and response details.

{
  "update_id": "b3c2d1e0-4f5a-4b3c-9d8e-7a6f5b4c3d2e",
  "type": "message" | "callback_query" | "inline_query",
  "message":        { "id", "from": { "id", "displayName" }, "conversation_id", "text", "timestamp" },
  "callback_query": { "id", "from", "conversation_id", "data", "message_id" },
  "inline_query":   { "id", "from", "query", "offset", "conversation_id" }
}

Webhook and getUpdates payload fields are snake_case -- the opposite convention from the camelCase you send when calling Bot API methods.

Your webhook can answer synchronously in the same HTTP response (a response or responses payload, or an inlineQueryAnswer for inline queries), or it can acknowledge and reply later through the ordinary HTTP methods.

Verifying a delivery

Each request carries three headers so you can confirm it really came from Migram: X-Migi-Signature: sha256=<hex>, X-Migi-Timestamp (unix seconds), and X-Migi-Bot-Id. The signature is an HMAC-SHA256 of timestamp + "." + body, keyed with your bot's webhook secret -- recompute it and compare before trusting the payload. Folding the timestamp into the signature keeps a captured request from being replayed later under a stale signature.

A delivery that fails or times out is retried up to three more times with growing gaps between attempts (roughly one second, then four, then sixteen) before Migram gives up on it. Return a 2xx as soon as you've accepted the update and do any slow work afterward, not before responding.

Methods

All 28 methods below are live. Every one is a POST with a JSON body except getMyCommands, which takes its parameters on the query string instead. Responses come back in one of three shapes: a nested result for the send-style methods and getUpdates, a bare {"ok": true} when there's nothing to report, or a single named key -- bot, commands, menuButton, params, or initData, depending on the method -- for read and configuration calls. See Getting started for a live example, or the full reference for every parameter and response.

MethodWhat it does
getUpdatesReceive updates by polling, with an optional long-poll timeout up to 50 seconds and an acknowledgement offset.
setWebhookPush updates to a public HTTPS url instead of polling.
sendMessageSend text, with parseMode of Markdown or HTML and an optional replyMarkup inline keyboard.
sendMessageDraftStream a message into existence by re-sending the same messageId; set isFinal to persist it.
editMessageTextReplace a sent message's text in place.
deleteMessageRemove one of the bot's own messages.
sendChatActionShow a transient status, such as "typing", in the conversation.
sendPhoto, sendDocument, sendLocation, sendPoll, sendAnimation, sendContact, sendDice, sendVenue, sendVideo, sendVoice, sendMediaGroupRich message types, one call per media kind.
sendStickerSend a catalog sticker by stickerId. A hosted stickerUrl is rejected -- stickers must come from the catalog.
setMyCommands / getMyCommands / deleteMyCommandsManage the "/" command list shown in the composer.
setChatMenuButtonConfigure the composer's menu button: the default, a commands shortcut, or a web app.
answerCallbackQueryClear an inline button's spinner and, optionally, show a toast or alert.
answerInlineQueryReturn results for an @yourbot query inline search.
getMeFetch the bot's own profile, nested under a bot key.
validateWebAppData / generateWebAppInitDataVerify or mint the signed init data used by Mini Apps.

Rate limits

Outbound calls are capped at 30 requests per second per bot, and separately at 20 requests per minute per bot per conversation. Cross either limit and you get a 429 with a Retry-After header and a JSON body carrying retry_after in seconds -- back off and retry rather than hammering the endpoint.

HTTP/1.1 429 Too Many Requests
Retry-After: 3

{ "error": "rate limit exceeded", "retry_after": 3 }

Inline mode, location requests & feedback

Turn on inline mode per bot with /setinline in @botmigi. Once it's on, anyone can type @yourbot <query> in any chat; polling or your webhook gets an inline_query update and you answer it with answerInlineQuery. Inline location requests and inline feedback are reserved settings in BotMigi that aren't wired up on Migram yet.

Text formatting

parseMode: "Markdown" understands **bold**, `code`, and [text](url) links; bare URLs, @usernames, and /commands sitting in plain text get auto-linked without any markup at all. parseMode: "HTML" accepts a restricted, sanitized subset of HTML instead.

BotMigi commands

/newbot, /mybots, /setname, /setdescription, /setabouttext, /setuserpic, /setcommands, /deletebot, /token, /revoke, /setinline, /setjoingroups, /setprivacy, and /setwebhook cover bot creation, profile editing, and settings between them. Send /help to @botmigi for the running, annotated list.

Bot Privacy Policy

Conversations with bots on Migram are not end-to-end encrypted: whatever you type to a bot reaches that bot's developer as plain text, because the bot has to read it to respond. A bot sees only what you hand it directly inside its own chat -- your messages, button taps, and inline queries -- and has no visibility into your other conversations, your contact list, or your online status.

Whoever built a given bot decides how it stores and uses what you send it. If that bot publishes its own privacy policy, that policy sits on top of this baseline one, not in place of it. You can cut a bot off at any time: delete the chat, or block it from its profile, and it loses the ability to message you afterward.

Bots built through @botmigi are barred from harvesting login credentials, impersonating real people, or reselling what they learn about users. Use the Report action on a bot's profile to flag one that breaks these rules -- Migram's moderators review every report that comes in.

Migram is an independent project, unaffiliated with and not endorsed by Telegram FZ-LLC or Telegram Messenger Inc.