Skip to content

Your first bot

An echo bot: it receives a message and sends the same text back. About fifteen minutes, and everything larger is a variation on it.

You cannot run this yet

Step 1 is blocked. @gcore does not exist, no token can be issued, and the gateway returns 404 for every call — see implementation status.

The rest is written as a real walkthrough rather than a sketch, so the flow can be reviewed now and followed unchanged once the platform is up. Nothing below has been executed against a live server. Treat the code as reference, not as something you can paste and watch work.

1. Get a token

Bots are created by chatting with @gcore inside GROM. Send /newbot, answer two questions — a display name and a username ending in bot — and it replies with a token:

4210:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw

Put it in an environment variable now, before it ends up in a file you later push:

bash
export GROM_TOKEN='4210:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw'

Check it:

bash
curl "https://api.casinoplaneta.info/bot$GROM_TOKEN/getMe"
json
{
  "ok": true,
  "result": { "id": 4210, "is_bot": true, "first_name": "Echo", "username": "my_echo_bot" }
}

getMe touches no chat and takes no parameters, which makes it the right thing to call when you want to know whether a token is good.

2. Say something to your bot

Open GROM, find @my_echo_bot, send it hello.

Nothing visible happens — that is correct. Your bot has no program behind it yet. The message is queued server-side, waiting to be collected.

This step is not optional, and it is not just for testing. A bot cannot open a conversation. Until a person messages it or adds it to a group, the bot has no way to reach them and no id to reach them with. Every "how do I message a user by phone number" question ends here: you cannot, by design.

3. Collect the update

bash
curl "https://api.casinoplaneta.info/bot$GROM_TOKEN/getUpdates"
json
{
  "ok": true,
  "result": [
    {
      "update_id": 100001,
      "message": {
        "message_id": 7,
        "from": { "id": 1337, "is_bot": false, "first_name": "Aziz" },
        "chat": { "id": 1337, "type": "private" },
        "date": 1770000000,
        "text": "hello"
      }
    }
  ]
}

Two ids matter and they are different things. update_id is your bookmark in the event stream. chat.id is where to reply — here it equals the sender's id because this is a private chat, but in a group it does not, and confusing the two sends your reply into the wrong conversation.

4. Reply

bash
curl "https://api.casinoplaneta.info/bot$GROM_TOKEN/sendMessage" \
  -H 'Content-Type: application/json' \
  -d '{"chat_id": 1337, "text": "hello"}'

That is the whole API in one call: read chat.id, send text back.

5. Make it a loop

The polling loop is the part worth getting right, because one detail decides whether it works at all.

python
import requests

TOKEN = "..."
API = f"https://api.casinoplaneta.info/bot{TOKEN}"
offset = None

while True:
    r = requests.get(f"{API}/getUpdates",
                     params={"offset": offset, "timeout": 50},
                     timeout=60).json()

    if not r["ok"]:
        print("error:", r["description"])
        continue

    for update in r["result"]:
        # Acknowledge BEFORE handling: +1, always.
        offset = update["update_id"] + 1

        msg = update.get("message")
        if not msg or "text" not in msg:
            continue

        requests.post(f"{API}/sendMessage",
                      json={"chat_id": msg["chat"]["id"], "text": msg["text"]})
javascript
const TOKEN = '...'
const API = `https://api.casinoplaneta.info/bot${TOKEN}`
let offset

const call = async (method, params) =>
  (await fetch(`${API}/${method}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(params),
  })).json()

while (true) {
  const r = await call('getUpdates', { offset, timeout: 50 })
  if (!r.ok) { console.error(r.description); continue }

  for (const update of r.result) {
    offset = update.update_id + 1        // +1, always

    const msg = update.message
    if (!msg?.text) continue

    await call('sendMessage', { chat_id: msg.chat.id, text: msg.text })
  }
}

offset must be the last update_id plus one. Passing the id itself re-delivers that update forever, and the bot answers the same message on every pass — the single most common bug when writing a poller by hand.

timeout: 50 makes the request block until something arrives, so an idle bot costs one open connection instead of a hot loop. Set your HTTP client's own timeout higher than that, or your client will hang up on a perfectly healthy wait.

Run exactly one poller per token. A second one gets 409 Conflict, which usually means a deploy left the old process running.

6. When to switch to webhooks

Polling is fine — for development, and for plenty of production bots. Move to a webhook when you want the platform to push events to you instead:

bash
curl "https://api.casinoplaneta.info/bot$GROM_TOKEN/setWebhook" \
  -H 'Content-Type: application/json' \
  -d '{"url": "https://example.com/hook", "secret_token": "a-long-random-string"}'

Three things to know before you do:

  • Check the secret. Compare X-Grom-Bot-Api-Secret-Token on every request. Without that check, anyone who guesses your URL can feed your bot fake updates.
  • Delivery is at-least-once. You will occasionally see the same update_id twice. Make the handler idempotent — store handled ids, or make the effect naturally repeatable.
  • Answer fast. Retries key off your response time, so a slow handler turns one update into several deliveries. Acknowledge with 200, then do the work.

Setting a webhook disables polling. To go back, call deleteWebhook.

Where to go from here

Buttons and keyboardsInline buttons and callback_query
Commands/start, menus, per-language command lists
ErrorsWhich failures to retry and which are permanent
LimitsHow fast you may send
Mini AppsWhen a chat interface is not enough