The fdyappa Bot API
Build a bot that lives inside fdyappa chats. The API is Telegram-shaped — if you've built a Telegram bot, you already know this one. Long-poll from a laptop with no public URL, or set a webhook.
Introduction
The whole design rests on one idea: a bot is a user. It gets a real account, so DMs, groups, search, avatars, receipts and push all work for it with no special cases. What makes it a bot is a token and an update queue.
Every call is a plain HTTPS request to https://fdyappa.in/bot<token>/<method>. Parameters go as a JSON body (or query string). Every response is { "ok": true, "result": … } on success, or { "ok": false, "error_code": N, "description": "…" } on failure.
Quick start
- 1. In the fdyappa app, open Settings → Bots → New bot. Give it a name and a username ending in
bot. You'll get a token — copy it now, it's shown only once. - 2. Run the loop below on any machine. That's it — your bot is live.
- 3. Find your bot by its
@usernamein the app, press Start, and message it.
# Long-poll for updates (held open up to 30s), then reply
curl "https://fdyappa.in/bot$TOKEN/getUpdates?timeout=30"
curl -X POST "https://fdyappa.in/bot$TOKEN/sendMessage" \
-H "Content-Type: application/json" \
-d '{"chat_id": 123, "text": "Hello from my bot 👋"}'
"run this script and you have a bot" is literally true.Tokens & base URL
A token looks like 42:AbC…xyz — the number before the colon is the bot's user id, the rest is a secret. Put the whole token straight into the path:
https://fdyappa.in/bot42:AbC…xyz/getMe
The token is stored only as a hash on the server, so it can't be recovered — if you lose it, revoke & reissue from the app (the old token stops working immediately).
What's a chat_id?
Every method that sends something takes a chat_id — the conversation you're writing to. There are two ways to give it, and one important thing to know:
- The conversation id (a number) — read it straight off an incoming update at
message.chat.id, and pass it back. This works for both DMs and groups;chat.typetells you which ("private"or"group"). - A
@username(a string) — for a 1:1 chat you can address the person by handle instead:"chat_id": "@alice"(the leading@is optional). Groups have no username, so they're always addressed by their numeric id.
chat_id is NOT the user's id. Unlike some other bot platforms, a private chat here has its own conversation id, separate from the user's account id. In an update you'll see chat.id (the conversation, e.g. 57) and from.id (the sender's user id, e.g. 5) — they're different numbers. Send to chat.id; use from.id only for identity. Not sure of the id? Call getChat with a @username and it returns the numeric id to use.Addressing by username never lets a bot skip consent: if you've never chatted with @alice (she hasn't opened your bot and pressed Start), the call returns a clear 404 rather than starting an unsolicited chat.
# these two are equivalent when 57 is your DM with @alice
curl -X POST "https://fdyappa.in/bot$TOKEN/sendMessage" -d '{"chat_id": 57, "text": "hi"}'
curl -X POST "https://fdyappa.in/bot$TOKEN/sendMessage" -d '{"chat_id": "@alice", "text": "hi"}'
The rules
These aren't optional — they're what keep bots from becoming a spam cannon:
- A bot can't message a stranger. Someone must press Start (which sends the bot a
/start) before the bot may DM them. - Privacy mode in groups. By default a bot in a group only receives messages meant for it: a
/command, an@mentionof it, or a reply to one of its messages. The owner can turn this off. - Rate limits. A bot is capped at ~30 sends per minute — enough for real use, a guardrail against runaway loops.
Receiving updates — getUpdates
/getUpdates
Returns an array of updates. Held open (long-poll) until something happens or timeout seconds pass, so an idle bot costs one parked request instead of one per second.
| Param | Type | Notes |
|---|---|---|
offset | int | The update_id to start from. Passing N acknowledges everything below N (they're deleted). Use last_update_id + 1. |
timeout | int | Seconds to hold the request open (max 40). 0 returns immediately. |
limit | int | Max updates to return (1–100, default 100). |
The update object
Each update carries either a message or a callback_query:
// an incoming message
{
"update_id": 1024,
"message": {
"message_id": 5567,
"date": 1752400000,
"chat": { "id": 123, "type": "private" },
"from": { "id": 5, "username": "alice", "first_name": "Alice", "is_bot": false },
"text": "/start",
"reply_to_message_id": 5560
}
}
// a button tap
{
"update_id": 1025,
"callback_query": {
"id": "a1b2c3",
"from": { "id": 5, "username": "alice" },
"message": { "message_id": 5567, "chat": { "id": 123 } },
"data": "like:42"
}
}
text with a prefix: [IMAGE]<url>, [VIDEO:secs]<url>, [VOICE:secs]<url>, or [FILE:name:bytes]<url>. Plain text has no prefix.Webhooks (optional)
If your bot has a public HTTPS endpoint, skip polling: call setWebhook with a url and we POST each update to it as it happens. deleteWebhook switches back to long polling.
curl -X POST "https://fdyappa.in/bot$TOKEN/setWebhook" \
-H "Content-Type: application/json" \
-d '{"url": "https://my-server.example/hook"}'
Sending — sendMessage
/sendMessage
| Param | Type | Notes |
|---|---|---|
chat_id | int / string | Required. The conversation id (from an update's chat.id), or a @username for a 1:1 chat. See What's a chat_id? |
text | string | Required. Up to 4096 chars. |
reply_to_message_id | int | Optional. Send as a threaded reply. |
reply_markup | object | Optional. An inline keyboard, a form, and/or a card. |
Returns { ok, result: { message_id, date, chat, text } } — keep message_id if you plan to edit or delete it later.
sendPhoto & sendVideo
/sendPhoto · /sendVideo
Same as sendMessage but pass a media URL instead of text: photo for /sendPhoto, video for /sendVideo (must be a public http(s) URL). chat_id is required; reply_markup is supported.
curl -X POST "https://fdyappa.in/bot$TOKEN/sendPhoto" \
-H "Content-Type: application/json" \
-d '{"chat_id": 123, "photo": "https://picsum.photos/800/600"}'
Looking up a chat — getChat
/getChat
Resolve a chat_id — numeric or @username — into the chat's details, including the numeric id you send to. Handy when you know someone's handle but not their conversation id.
| Param | Type | Notes |
|---|---|---|
chat_id | int / string | Required. A conversation id or a @username. |
For a 1:1 chat you get back the conversation id, the peer's user_id, username and first_name. For a group you get id, title and members_count.
curl -X POST "https://fdyappa.in/bot$TOKEN/getChat" \
-H "Content-Type: application/json" \
-d '{"chat_id": "@alice"}'
// → { "ok": true, "result": {
// "id": 57, "type": "private",
// "user_id": 5, "username": "alice", "first_name": "Alice" } }
// send to result.id (57), not to user_id (5).
Inline keyboards
Attach buttons under any message with reply_markup. Each button has text plus either a callback_data (you get a callback_query update when tapped) or a url (opens in the browser). Up to 8 rows × 4 buttons.
{
"chat_id": 123,
"text": "Pick one:",
"reply_markup": {
"inline_keyboard": [
[{ "text": "👍 Like", "callback_data": "like:42" },
{ "text": "👎 Nope", "callback_data": "nope:42" }],
[{ "text": "🌐 Open site", "url": "https://fdyappa.in" }]
]
}
}
Rich buttons v2
Buttons aren't just text. Give one a style (primary, secondary, danger, success) and an icon, and pick one of four actions:
callback_data— you get acallback_queryupdate.url— opens in the browser.copy— copies text straight to the user's clipboard. No round-trip.open— deep-links into the app itself: a profile, a post, a room, a live, a chat. This is the one a Telegram bot can't do — it lives outside the app, ours lives inside it.
{
"text": "Pick a pizza",
"reply_markup": {
"inline_keyboard": [
[{ "text": "Order", "style": "primary", "icon": "checkmark", "callback_data": "order" },
{ "text": "Cancel", "style": "danger", "callback_data": "cancel" }],
[{ "text": "📋 Copy promo code", "copy": "FDY-2026" }],
[{ "text": "👤 Open the chef", "open": { "type": "user", "id": 42 } }]
]
}
}
open.type is an allowlist — user, post, room, live, chat, addressed by numeric id. A bot cannot route someone to a settings screen.
Forms v2
The big one. On Telegram, collecting five fields means five messages and a state machine to remember where each user got to. Here you send one message and get back one form_submission with every answer.
Field types: text, number, select, toggle, rating. Up to 10 per form.
{
"chat_id": 123,
"text": "Tell us about yourself",
"reply_markup": {
"form": {
"id": "signup",
"submit": "Send it",
"fields": [
{ "key": "name", "label": "Your name", "type": "text", "required": true },
{ "key": "plan", "label": "Plan", "type": "select", "options": ["Free", "Pro"] },
{ "key": "stars", "label": "Rate us", "type": "rating", "max": 5 },
{ "key": "notify", "label": "Email me", "type": "toggle" }
]
}
}
}
When the user hits submit you receive:
{
"form_submission": {
"form_id": "signup",
"from": { "id": 1, "username": "farhad" },
"message": { "message_id": 333, "chat": { "id": 57 } },
"values": { "name": "Farhad", "plan": "Pro", "stars": 5, "notify": true }
}
}
Values are validated server-side against the form you actually sent. Undeclared fields are dropped, required is enforced, ratings are clamped, and a select only accepts one of its own options — so you can trust values without re-checking it.
Cards v2
A real layout instead of text: an image, a title, a subtitle, and key/value rows. A card can be sent on its own — no caption required — and can carry a keyboard.
{
"chat_id": 123,
"reply_markup": {
"card": {
"title": "Margherita",
"subtitle": "Ready in 20 min",
"image": "https://…/pizza.jpg",
"fields": [{ "label": "Price", "value": "₹249" }]
}
}
}
Inline search v2
/setInline · /answerInlineQuery
A user types @yourbot pizza in any chat — yours or not, group or DM. You get an inline_query update; whatever you answer with, they can tap to send.
Turn it on once with setInline (it's off by default — a bot that never answers would just make the composer wait):
curl -X POST "https://fdyappa.in/bot$TOKEN/setInline" \
-H "Content-Type: application/json" -d '{"enabled": true}'
Then answer each query. You have 4 seconds — the composer is waiting on you, so be quick:
curl -X POST "https://fdyappa.in/bot$TOKEN/answerInlineQuery" \
-H "Content-Type: application/json" \
-d '{
"inline_query_id": "33b3e435",
"results": [
{ "id": "1", "title": "Margherita", "subtitle": "Classic", "send": "🍕 Margherita" },
{ "id": "2", "title": "Pepperoni", "send": "🍕 Pepperoni" }
]
}'
send is what actually gets posted into the chat when the user taps the result. Up to 20 results.
Group moderation v2
A bot that is an admin of a group can moderate it. The server enforces the floor: a bot can only act in groups where it's an admin, and no one — not even an admin bot — can ban, mute, or kick another admin, or itself.
Your bot must still check the person who asked. The server knows the bot is an admin; it does not know whether the user who typed the command is allowed to. Call getChatMember on the sender first — otherwise any member could tell your bot to ban someone.
| Method | Does |
|---|---|
banChatMember | Remove + block rejoin. Takes chat_id, user_id (numeric or @username), optional reason. |
unbanChatMember | Lift a ban. |
muteChatMember | They can't send messages. Optional minutes (0/absent = indefinite, capped at 30 days). |
unmuteChatMember | Let them speak again. |
purgeChatHistory | Delete messages older than days (≥ 1; never touches system messages). |
getChatMember | A user's status (member/administrator/banned/left), is_admin, is_muted. |
getChatAdministrators | The group's admins. |
curl -X POST "https://fdyappa.in/bot$TOKEN/muteChatMember" \
-H "Content-Type: application/json" \
-d '{"chat_id": 57, "user_id": "@spammer", "minutes": 60, "reason": "flood"}'
Want this without writing a bot? @fdyguard is a ready-made one — add it to your group, make it an admin, and type /help.
Answering a button tap
/answerCallbackQuery
When a user taps a callback_data button you receive a callback_query update. Acknowledge it with a short toast (and, if you like, edit the message):
curl -X POST "https://fdyappa.in/bot$TOKEN/answerCallbackQuery" \
-H "Content-Type: application/json" \
-d '{"callback_query_id": "a1b2c3", "text": "Liked 👍", "show_alert": false}'
Editing & deleting
/editMessageText · /deleteMessage
A bot can change or remove its own messages. editMessageText takes message_id + new text (and optional reply_markup); deleteMessage takes message_id. Great for live counters, status that updates in place, or self-destructing messages.
Typing indicator — sendChatAction
/sendChatAction
Show a "typing…" indicator in the chat while you prepare a reply. Pass chat_id. It clears on its own or when your next message lands.
The command menu — setMyCommands
/setMyCommands
Register the slash-commands the app shows people when they open your bot. Pass commands: an array of { command, description } (up to 30).
curl -X POST "https://fdyappa.in/bot$TOKEN/setMyCommands" \
-H "Content-Type: application/json" \
-d '{"commands":[{"command":"start","description":"Say hi"},{"command":"help","description":"What I can do"}]}'
A complete bot in Node.js
An echo bot with a button — no dependencies, just node bot.js (Node 18+ has global fetch):
const API = `https://fdyappa.in/bot${process.env.TOKEN}`;
const call = (m, body) => fetch(`${API}/${m}`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
}).then(r => r.json());
let offset = 0;
async function loop() {
for (;;) {
const r = await fetch(`${API}/getUpdates?offset=${offset}&timeout=30`).then(r => r.json());
for (const u of r.result || []) {
offset = u.update_id + 1;
if (u.message) {
await call('sendMessage', {
chat_id: u.message.chat.id,
text: `You said: ${u.message.text}`,
reply_markup: { inline_keyboard: [[{ text: '👍', callback_data: 'ok' }]] }
});
} else if (u.callback_query) {
await call('answerCallbackQuery', { callback_query_id: u.callback_query.id, text: 'thanks!' });
}
}
}
}
loop();
All methods at a glance
| Method | What it does |
|---|---|
getMe | Info about the bot account. |
getChat | Resolve a chat_id / @username to chat details. |
getUpdates | Long-poll for new updates. |
setWebhook / deleteWebhook / getWebhookInfo | Switch between webhook and polling. |
setMyCommands / getMyCommands | The slash-command menu. |
sendMessage | Send text, a form, a card, and/or buttons. |
sendPhoto / sendVideo | Send media by URL. |
editMessageText | Rewrite one of the bot's messages. |
deleteMessage | Remove one of the bot's messages. |
answerCallbackQuery | Acknowledge a button tap with a toast. |
sendChatAction | Show a "typing…" indicator. |
setInline v2 | Opt in to inline search (@yourbot query). |
answerInlineQuery v2 | Answer an inline query with tappable results. |
banChatMember / unbanChatMember v2 | Admin bot: ban / unban a group member. |
muteChatMember / unmuteChatMember v2 | Admin bot: stop / allow a member sending. |
purgeChatHistory v2 | Admin bot: delete messages older than N days. |
getChatMember / getChatAdministrators v2 | A member's status; the group's admins. |
Update types
| Update | When you get it |
|---|---|
message | Someone sent your bot a message. |
callback_query | Someone tapped a callback_data button. |
form_submission v2 | Someone submitted a form — every field, one update. |
inline_query v2 | Someone typed @yourbot query. Answer within 4s. |