What is a Pico bot?
A bot is a small program that talks to people on Pico like a normal contact. You write it in any programming language, run it wherever you like, and it talks to Pico over a simple HTTPS API.
The Bot API follows the same shape as Telegram's, so if you have written a Telegram bot before, you already know most of it. 🎉
Quick start
- Create the bot. In Pico open
@botfatherand send/newbot. Choose a name and a username that ends inbot. - Copy the token. BotFather sends a token like
123456:AbCdEf-your-token. Tap it to copy. You can see it again whenever you want:/mybots→ your bot → 🔑 API token. - Say hello to your bot. Open it and press Start. Bots can never message someone who has not written to them first.
- Run a program. Use one of the examples below, put your token in, and write to your bot. 🎈
curl "https://picogram.ir/bot123456:AbCdEf-your-token/getMe"
curl -X POST "https://picogram.ir/bot123456:AbCdEf-your-token/sendMessage" \
-H "content-type: application/json" \
-d '{"chat_id": 42, "text": "Hello from my bot 👋"}'curlimport requests
TOKEN = "123456:AbCdEf-your-token"
API = f"https://picogram.ir/bot{TOKEN}"
offset = 0
while True:
r = requests.get(f"{API}/getUpdates", params={"offset": offset, "timeout": 30}, timeout=40).json()
for u in r["result"]:
offset = u["update_id"] + 1
m = u.get("message")
if m and m.get("text"):
requests.post(f"{API}/sendMessage", json={"chat_id": m["chat"]["id"], "text": f"You said: {m['text']}"})Pythonconst TOKEN = "123456:AbCdEf-your-token";
const API = `https://picogram.ir/bot${TOKEN}`;
let offset = 0;
for (;;) {
const r = await (await fetch(`${API}/getUpdates?offset=${offset}&timeout=30`)).json();
for (const u of r.result) {
offset = u.update_id + 1;
const m = u.message;
if (m?.text) {
await fetch(`${API}/sendMessage`, {
method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify({ chat_id: m.chat.id, text: `You said: ${m.text}` }),
});
}
}
}Node.js<?php
$api = "https://picogram.ir/bot" . getenv("PICO_TOKEN");
$offset = 0;
while (true) {
$res = json_decode(file_get_contents("$api/getUpdates?offset=$offset&timeout=30"), true);
foreach ($res["result"] as $u) {
$offset = $u["update_id"] + 1;
if (!empty($u["message"]["text"])) {
$m = $u["message"];
file_get_contents("$api/sendMessage?" . http_build_query([
"chat_id" => $m["chat"]["id"], "text" => "You said: " . $m["text"],
]));
}
}
}PHPpackage main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
)
const api = "https://picogram.ir/bot123456:AbCdEf-your-token"
func main() {
offset := 0
for {
resp, err := http.Get(fmt.Sprintf("%s/getUpdates?offset=%d&timeout=30", api, offset))
if err != nil { continue }
var out struct {
Result []struct {
UpdateID int `json:"update_id"`
Message *struct {
Text string `json:"text"`
Chat struct{ ID int `json:"id"` } `json:"chat"`
} `json:"message"`
} `json:"result"`
}
json.NewDecoder(resp.Body).Decode(&out)
resp.Body.Close()
for _, u := range out.Result {
offset = u.UpdateID + 1
if u.Message != nil && u.Message.Text != "" {
http.PostForm(api+"/sendMessage", url.Values{
"chat_id": {fmt.Sprint(u.Message.Chat.ID)}, "text": {"You said: " + strings.TrimSpace(u.Message.Text)}})
}
}
}
}GoHow to connect
Every request goes to:
https://picogram.ir/bot<TOKEN>/<METHOD>URL- 🔒 HTTPS only.
- 📨 Send parameters as a query string, as a JSON body (
content-type: application/json) or both.GETandPOSTboth work. - 📦 The answer is always JSON:
{"ok": true, "result": …}on success, or{"ok": false, "error_code": 400, "description": "…"}on failure. - 🐢 Rate limit: 600 requests per minute per IP address.
Telegram libraries
Most Telegram bot libraries let you change the API address. Point them at Pico and they work for every method listed on this page. Methods Pico does not have yet answer with an error.
# python-telegram-bot
Application.builder().token(TOKEN).base_url("https://picogram.ir/bot").build()
# aiogram 3
AiohttpSession(api=TelegramAPIServer.from_base("https://picogram.ir"))
# grammY
new Bot(TOKEN, { client: { apiRoot: "https://picogram.ir" } })
# Telegraf
new Telegraf(TOKEN, { telegram: { apiRoot: "https://picogram.ir" } })
# node-telegram-bot-api
new TelegramBot(TOKEN, { polling: true, baseApiUrl: "https://picogram.ir" })LibrariesLanguages and formats
Pico does not care what you write your bot in. If your language can make an HTTPS request and read JSON, it works: Python, JavaScript / TypeScript (Node, Deno, Bun), PHP, Go, Java, Kotlin, C#, Rust, Ruby, Dart, Swift, shell scripts… 🌍
| Accepted | |
|---|---|
| 📡 Transport | HTTPS (TLS 1.2+) |
| 📝 Request | query string, JSON body (application/json), or both |
| 📤 Response | JSON, UTF-8 |
| 🔤 Text | UTF-8, 1 to 4096 characters per message |
| 🔁 Getting messages | long polling with getUpdates or a webhook |
Methods
| Method | Parameters | What it does |
|---|---|---|
getMe | — | Your bot's id, name and username. |
getUpdates | offset, timeout (0–50 s), limit (1–100) | Long polling. Pass offset = last update_id + 1 to confirm what you already handled. |
sendMessage | chat_id, text, parse_mode, reply_to_message_id, reply_markup | Sends a text message. |
editMessageText | chat_id, message_id, text, parse_mode, reply_markup | Edits a message your bot sent. |
deleteMessage | chat_id, message_id | Deletes a message your bot sent. |
answerCallbackQuery | callback_query_id | Tells Pico you handled a button tap. |
sendChatAction | chat_id, action | Accepted so existing code keeps working. |
setMyCommands / getMyCommands | commands: JSON list of {command, description} (up to 100) | The /commands menu of your bot. Commands are lowercase letters, digits and underscores. |
getChat | chat_id | Basic info about a chat. |
setWebhook / deleteWebhook / getWebhookInfo | url, secret_token | Receive updates by webhook instead of polling. |
Photos, videos and files
Your bot can send everything people can send in Pico, and receive it too.
| Method | File parameter | Shown as |
|---|---|---|
sendPhoto | photo | a photo |
sendVideo | video | a video that plays inside the chat |
sendAnimation | animation | a looping video or a GIF |
sendAudio | audio | an audio file |
sendVoice | voice | a voice message with a waveform |
sendDocument | document | any other file |
sendLocation, sendContact, sendDice | — | a tappable map link, a contact card, a dice roll 🎲 |
Every one of them also takes chat_id, caption (up to 1024 characters), parse_mode, reply_to_message_id and reply_markup. For videos and voice you can add duration, width and height.
The file itself can be given in three ways:
# 1) upload a file from your computer (multipart/form-data)
curl -F chat_id=42 -F caption="Look 👀" -F photo=@cat.jpg "https://picogram.ir/bot123456:AbCdEf-your-token/sendPhoto"
# 2) send a file Pico already has (a file_id from an update or an earlier message)
curl "https://picogram.ir/bot123456:AbCdEf-your-token/sendDocument" -d chat_id=42 -d document=<FILE_ID>
# 3) let Pico download a public https link for you
curl "https://picogram.ir/bot123456:AbCdEf-your-token/sendVideo" -d chat_id=42 --data-urlencode "video=https://example.com/clip.mp4"
# receive: getFile gives a path, then download it
curl "https://picogram.ir/bot123456:AbCdEf-your-token/getFile?file_id=<FILE_ID>"
curl -O "https://picogram.ir/file/bot123456:AbCdEf-your-token/files/<FILE_ID>"curl- 📤 Upload with
multipart/form-data(one file per request). - 🆔 A
file_idthat Pico gave you, to send the same file again without uploading it. - 🔗 A public https link: Pico downloads it for you (up to 50 MB, public addresses only).
Receiving files
When someone sends your bot a photo, video, voice message or file, the update has the matching field (photo, video, voice, document), a file_id, and the caption.
{
"update_id": 120,
"message": {
"message_id": 940,
"from": { "id": 7, "is_bot": false, "first_name": "Sara" },
"chat": { "id": 42, "type": "private" },
"caption": "my cat",
"photo": [ { "file_id": "k2s9…", "width": 1280, "height": 960, "file_size": 183412 } ]
}
}JSONCall getFile to get a file_path, then download https://picogram.ir/file/bot<TOKEN>/<file_path>. The largest file is 100 MB (50 MB for uploads and links from a bot).
Forward, copy and edit
forwardMessage and copyMessage (chat_id, from_chat_id, message_id), editMessageCaption, editMessageReplyMarkup, pinChatMessage and unpinChatMessage.
Groups, channels and admin methods
Add your bot to a group or channel, make it an admin with the rights it needs, and it can manage the chat.
| Method | Needs the right | What it does |
|---|---|---|
getChatMember | — | Status of a person: creator, administrator, member, left or kicked, with their rights. |
getChatAdministrators | — | The owner and all admins. |
getChatMemberCount | — | How many members. |
banChatMember / unbanChatMember | ban | Remove and block someone from coming back, or let them return. |
setChatTitle / setChatDescription | info | Change the name or description. |
pinChatMessage / unpinChatMessage | pin | Pin or unpin a message. |
leaveChat | — | The bot leaves. |
Admin rights
Owners and admins give people (and bots) exactly the rights they need: change info, post messages (channels), edit and delete messages, ban people, invite people, pin messages and add new admins. The owner can also hand the chat over to someone else, and set what ordinary members of a group may do (send messages, send media, add people, pin, change info).
Your bot's profile
setMyName, getMyName, setMyDescription, getMyDescription. Change its picture in @botfather: /mybots → your bot → 📷 Edit picture.
Button answers
answerCallbackQuery with text shows a short message to the person who tapped the button.
Updates: what your bot receives
Every item from getUpdates (or every webhook call) has an update_id and one of two things:
💬 A message
{
"update_id": 118,
"message": {
"message_id": 931,
"from": { "id": 7, "is_bot": false, "first_name": "Sara", "username": "sara" },
"date": 1767225600,
"text": "hello",
"chat": { "id": 42, "type": "private" }
}
}JSONtext is the plain text. If the person used formatting, formatted_text also contains the original with Pico's formatting symbols.
👆 A button tap
{
"update_id": 119,
"callback_query": {
"id": "931.1767225700000",
"from": { "id": 7, "is_bot": false, "first_name": "Sara", "username": "sara" },
"data": "yes",
"message": { "message_id": 931, "chat": { "id": 42, "type": "private" }, "text": "Pick one" }
}
}JSONIn groups and channels a bot only sees commands (/start), messages that mention it (@yourbot) and replies to its own messages.
Formatting text
Add parse_mode to sendMessage and editMessageText: HTML, MarkdownV2 or Markdown, exactly like Telegram. Without it the text is sent as typed.
HTML
<b>bold</b> <i>italic</i> <u>underline</u> <s>strike</s> <tg-spoiler>spoiler</tg-spoiler>
<a href="https://picogram.ir">link</a> <code>inline code</code>
<pre><code class="language-python">print("hi")</code></pre>
<blockquote>quote</blockquote>HTMLMarkdownV2
*bold* _italic_ __underline__ ~strike~ ||spoiler||
[link](https://picogram.ir) `inline code`
```python
print("hi")
```
>quote (escape these with a backslash: _ * [ ] ( ) ~ ` > # + - = | { } . !)MarkdownV2Pico supports bold, italic, underline, strikethrough, spoiler, inline code, code blocks with a language, links, and block quotes (also collapsible ones with <blockquote expandable>). Mentions (@name), hashtags, links, e-mail addresses and /commands are recognised automatically. If the formatting is broken you get 400 can't parse entities.
Buttons (inline keyboards)
Add reply_markup to a message to show buttons under it. A button has either callback_data (your bot gets a callback_query when it is tapped) or a url (opens a link).
{
"chat_id": 42,
"text": "Pick one 👇",
"reply_markup": {
"inline_keyboard": [
[ { "text": "✅ Yes", "callback_data": "yes" }, { "text": "❌ No", "callback_data": "no" } ],
[ { "text": "🌐 Website", "url": "https://example.com" } ]
]
}
}JSON| Limit | Value |
|---|---|
| Rows | up to 8 |
| Buttons per row | up to 8 |
| Button text | 1–64 characters |
callback_data | up to 64 characters |
url | http(s) links only |
When someone taps a button, call answerCallbackQuery and then editMessageText to update the message, or send a new one.
Webhooks
Instead of polling, Pico can call your server whenever something happens.
curl "https://picogram.ir/bot123456:AbCdEf-your-token/setWebhook" \
--data-urlencode "url=https://bot.example.com/pico-hook" \
--data-urlencode "secret_token=my_secret_123"curl- 🔒 The address must be https on port 443 or 8443, and must resolve to a public address.
- 🔑 If you set
secret_token, every call carries the headerX-Pico-Bot-Api-Secret-Token. Check it. - 📮 Pico sends the update as JSON in a POST. Answer with 2xx within 10 seconds.
- 🔁 A failed delivery is tried 3 times (now, after 1.5 s, after 8 s).
getWebhookInfoshows the last error. - ⚠️ While a webhook is set,
getUpdatesanswers409 Conflict. CalldeleteWebhookto go back to polling.
Rules and limits
| Topic | Rule |
|---|---|
| Username | Ends in bot, 4–32 letters, digits or underscores, starts with a letter. Names containing pico are reserved for the Pico team. |
| Starting a chat | A bot can only write to people who wrote to it first. |
| Groups and channels | Bots see commands, mentions and replies to themselves. |
| Encryption | Chats with bots, groups and channels are not end-to-end encrypted. Only private chats between two people are. |
| Message size | 1–4096 characters. |
| Request rate | 600 requests per minute per IP. |
| Kept updates | 24 hours. |
🚧 Not available yet
Inline mode (answerInlineQuery), payments, games, polls, stickers, sendVideoNote, commands per language or scope, and Mini Apps. Tell us what you need through @support in Pico. 💙
Errors
| Code | Meaning | What to do |
|---|---|---|
401 | Unauthorized | The token is wrong or was revoked. Check it in BotFather. |
400 | Bad Request | A parameter is missing or invalid, or the formatting cannot be parsed. Read description. |
403 | Forbidden | The bot is not in that chat, or the person has not written to the bot first. |
404 | Not Found | The method does not exist (yet). |
409 | Conflict | A webhook is set. Use deleteWebhook or use the webhook. |
429/5xx | Slow down / server problem | Wait a moment and try again. |