Bot development
Bot API reference
Every method Pabal's HTTP Bot API accepts, the objects it exchanges and its errors, in full. Compatible with the Telegram Bot API.
Pabal's HTTP Bot API uses the same address format, requests, responses and errors as the Telegram Bot API. This reference covers only what Pabal actually accepts. Methods not listed here return 404 Not Found: method not found. If you're new, start with the bot tutorial.
Making requests
https://pabal.me/bot<token>/<method>
- HTTP methods: both
GETandPOSTwork. - Parameters can be sent in four ways — a query string (
?chat_id=1&text=hi),application/x-www-form-urlencoded,application/json, andmultipart/form-datawhen uploading files. You can mix them. - Object parameters (
reply_markup,commands,allowed_updates) go in as objects and arrays in a JSON body, and as JSON strings in forms and query strings. - Method names are case-insensitive (
sendMessage=sendmessage). - Request bodies can be up to 12 MB (beyond that,
413). - File downloads use a separate address,
https://pabal.me/file/bot<token>/<file_path>(see getFile).
Responses and errors
Responses are always JSON. On success you get HTTP 200 and result; on failure, an error_code equal to the HTTP status and a human-readable description.
{"ok": true, "result": { … }}{"ok": false, "error_code": 400, "description": "Bad Request: chat not found"}| error_code | When | Example description |
|---|---|---|
400 | A parameter is wrong | Bad Request: chat not found, Bad Request: message text is empty, Bad Request: message is not modified: …, Bad Request: message to edit not found, Bad Request: wrong file identifier/HTTP URL specified, Bad Request: query is too old and response timeout expired or query ID is invalid |
401 | The token is wrong | Unauthorized |
403 | No permission to send | Forbidden: bot can't initiate conversation with a user, Forbidden: bot is not a member of the group chat |
404 | Unknown method or file | Not Found: method not found |
409 | getUpdates while a webhook is set | Conflict: can't use getUpdates method while webhook is active; use deleteWebhook to delete the webhook first |
413 | The body is larger than 12 MB | Request Entity Too Large |
500 | Internal server error | Internal Server Error |
Pabal doesn't rate-limit requests as a whole yet, but the voice methods have hourly limits and return 429 Too Many Requests: retry after N (Voice calls). Write your code to wait parameters.retry_after seconds and retry when it gets a 429.
Getting updates
Use one of these two approaches. You can't use both at the same time.
getUpdates
POST/bot<token>/getUpdates
Fetches waiting updates (long polling). Returns 409 if a webhook is set.
| Parameter | Type | Required | Description |
|---|---|---|---|
offset | Integer | Optional | Only updates with this number or higher. Lower ones are removed as "received". Pass the last handled update_id + 1. |
limit | Integer | Optional | 1–100, default 100 |
timeout | Integer | Optional | Seconds to wait, 0–50, default 0. We recommend 25–30. |
allowed_updates | Array of String | Optional | Kinds to receive (Update). Empty means all the default kinds (message, edited_message, callback_query). Voice kinds arrive only if listed by name. The list you give stays as the bot's setting and is used by later calls and by the webhook too (if you leave it out, the previous value is kept) |
Returns: Array of Update
setWebhook
POST/bot<token>/setWebhook
Receive updates at an HTTPS address. The details are in the Webhooks page.
| Parameter | Type | Required | Description |
|---|---|---|---|
url | String | Yes | A public https:// address. An empty string removes the webhook |
secret_token | String | Optional | 1–256 characters, A-Z a-z 0-9 _ -. Sent in the X-Telegram-Bot-Api-Secret-Token request header |
allowed_updates | Array of String | Optional | Any of message, edited_message, callback_query. Empty means all three. Voice kinds (voice_call, voice_call_tool_query, voice_call_transcript) arrive only if listed by name. If you leave it out, the previous value is kept |
drop_pending_updates | Boolean | Optional | Discards the waiting updates |
max_connections | Integer | Optional | 1–100, default 40. Only stored; delivery is one at a time per bot |
certificate | InputFile | Not supported | 400 — use a certificate from a public authority |
ip_address | String | Ignored |
Returns: True
deleteWebhook
POST/bot<token>/deleteWebhook
Removes the webhook and goes back to getUpdates. Succeeds even if there's no webhook.
| Parameter | Type | Required | Description |
|---|---|---|---|
drop_pending_updates | Boolean | Optional | Discards the waiting updates |
Returns: True
getWebhookInfo
GET/bot<token>/getWebhookInfo
The webhook's status. No parameters. If there's no webhook, url is an empty string.
Returns: WebhookInfo
Methods
| Method | What it does |
|---|---|
getMe | Information about the bot itself |
sendMessage | Send text (buttons included) |
sendPhoto | Send a photo |
sendDocument | Send a file |
sendLocation | Send a location (live locations too) |
sendVenue | Send a place (a location with a name and an address) |
sendContact | Send a contact card |
editMessageLiveLocation · stopMessageLiveLocation | Move or stop a live location |
editMessageText | Edit the text and buttons of a sent message |
editMessageCaption | Edit a photo caption |
editMessageReplyMarkup | Edit only the buttons |
deleteMessage | Delete a message |
answerCallbackQuery | Answer a button press |
sendChatAction | "Typing" indicator (accepted only) |
getChat | Chat information |
getFile | Download path of a received photo |
setMyCommands · getMyCommands · deleteMyCommands | Command menu |
logOut · close | For compatibility (they do nothing) |
getUpdates · setWebhook · deleteWebhook · getWebhookInfo | Getting updates (above) |
setVoiceCallSettings · getVoiceCallSettings · endVoiceCall · sendVoiceCallContext · getVoiceCallUsage · startVoiceCall · answerVoiceCallToolQuery · setVoiceCallApiKey · setVoiceCallMcpServer … | Voice bots (see Voice calls below) |
chat_id
A person's ID (positive) for a one-to-one chat, the group ID (negative) for a basic group, or a person's username ("@hana_lee"). You can find all of them in an update's chat.id. Channel and supergroup IDs (-100…) don't exist yet (400 chat not found).
getMe
GET/bot<token>/getMe
Use it to check that the token is right. No parameters.
Returns: User — for bots, it also includes can_join_groups (true), can_read_all_group_messages (true), supports_inline_queries (false), can_connect_to_business (false) and has_main_web_app (false).
sendMessage
POST/bot<token>/sendMessage
| Parameter | Type | Required | Description |
|---|---|---|---|
chat_id | Integer or String | Yes | The chat to send to (see the box above) |
text | String | Yes | 1–4,096 characters. Sent as plain text |
reply_markup | InlineKeyboardMarkup · ReplyKeyboardMarkup · ReplyKeyboardRemove · ForceReply | Optional | Buttons |
disable_notification | Boolean | Optional | Send silently |
parse_mode, entities, reply_parameters, link_preview_options … | Ignored | Accepted but not used. Formatting is not applied |
Returns: the sent Message
To send to a person, that person must have messaged the bot first (403 Forbidden: bot can't initiate conversation with a user). To send to a group, the bot must be a member of that group.
sendPhoto
POST/bot<token>/sendPhoto
| Parameter | Type | Required | Description |
|---|---|---|---|
chat_id | Integer or String | Yes | The chat to send to |
photo | InputFile or String | Yes | A file uploaded as multipart/form-data (up to 10 MB; JPEG, PNG or GIF), or the file_id of a photo received earlier. URLs don't work yet |
caption | String | Optional | 0–1,024 characters |
reply_markup | Same as sendMessage | Optional | |
disable_notification | Boolean | Optional |
Returns: the sent Message (with a new file_id in photo)
sendDocument
POST/bot<token>/sendDocument
| Parameter | Type | Required | Description |
|---|---|---|---|
chat_id | Integer or String | Yes | The chat to send to |
document | InputFile or String | Yes | A file uploaded as multipart/form-data (any type, up to 20 MB), or the file_id of a file received earlier. URLs don't work yet |
filename | String | Optional | The name the recipient sees. Without it, the name of the uploaded file is used |
caption | String | Optional | 0–1,024 characters |
reply_markup | Same as sendMessage | Optional | |
disable_notification | Boolean | Optional | |
thumbnail, disable_content_type_detection | Ignored | Accepted but not used |
Returns: the sent Message (with a new file_id in document)
The type (mime_type) is worked out from the file name's extension. Files sent from the apps can be up to 2 GB, but what a bot uploads or downloads over HTTP is capped at 20 MB, as on Telegram.
sendLocation
POST/bot<token>/sendLocation
| Parameter | Type | Required | Description |
|---|---|---|---|
chat_id | Integer or String | Yes | The chat to send to |
latitude, longitude | Float | Yes | Latitude (−90 to 90) and longitude (−180 to 180). Out of range gives 400 Bad Request: GEO_POINT_INVALID |
horizontal_accuracy | Float | Optional | Radius of uncertainty in meters, 0–1,500. Rounded to a whole number when stored |
live_period | Integer | Optional | Makes it a live location: 60–86,400 seconds, or 2147483647 (0x7FFFFFFF) until stopped. Anything else gives 400 Bad Request: wrong live_period |
heading | Integer | Optional | Direction of travel of a live location, 1–360 degrees |
proximity_alert_radius | Integer | Optional | Proximity alert radius of a live location, in meters. Stored and shown, but no alert is sent yet |
reply_markup | Same as sendMessage | Optional | |
disable_notification | Boolean | Optional |
Returns: the sent Message (with a Location in location)
sendVenue
POST/bot<token>/sendVenue
| Parameter | Type | Required | Description |
|---|---|---|---|
chat_id | Integer or String | Yes | The chat to send to |
latitude, longitude | Float | Yes | Where the place is (as in sendLocation) |
title | String | Yes | Name of the place |
address | String | Yes | Address |
foursquare_id, foursquare_type | String | Optional | Foursquare's venue ID and type, passed on as given |
google_place_id, google_place_type | String | Optional | Google place ID and type. Used only when there is no foursquare_id |
reply_markup | Same as sendMessage | Optional | |
disable_notification | Boolean | Optional |
Returns: the sent Message (with a Venue in venue, and the same point in location)
Without title or address you get 400 Bad Request: title and address are required.
sendContact
POST/bot<token>/sendContact
| Parameter | Type | Required | Description |
|---|---|---|---|
chat_id | Integer or String | Yes | The chat to send to |
phone_number | String | Yes | Phone number, passed on as written |
first_name | String | Yes | First name |
last_name | String | Optional | Last name |
vcard | String | Optional | Additional data in vCard format, up to 64 KB |
reply_markup | Same as sendMessage | Optional | |
disable_notification | Boolean | Optional |
Returns: the sent Message (with a Contact in contact)
A contact card sent by a bot never carries user_id, even when the number belongs to a Pabal account — a bot has no contacts, and filling it in would let a bot find out whose number it is by sending one card per number. Without phone_number or first_name you get 400 Bad Request: phone_number and first_name are required.
editMessageLiveLocation
POST/bot<token>/editMessageLiveLocation
| Parameter | Type | Required | Description |
|---|---|---|---|
chat_id | Integer or String | Yes | The chat the message is in |
message_id | Integer | Yes | The live location to move (one the bot sent) |
latitude, longitude | Float | Yes | The new position |
horizontal_accuracy | Float | Optional | Radius of uncertainty in meters, 0–1,500 |
heading | Integer | Optional | Direction of travel, 1–360 degrees |
proximity_alert_radius | Integer | Optional | Proximity alert radius in meters. 0 turns it off |
live_period | Integer | Optional | New sharing period, in seconds counted from when the message was sent. It can't be extended by more than a day beyond the current one; 2147483647 means until stopped |
reply_markup | InlineKeyboardMarkup | Optional | New buttons |
inline_message_id | String | Not supported | 400, since there's no inline mode |
Returns: the edited Message
A live location that has already ended — stopped, or its period run out — can neither be moved nor brought back by extending its period (400 Bad Request: message can't be edited). The same goes for a message that isn't a live location.
A move made less than 10 seconds after the last applied one, or of less than 10 m, is skipped and not stored. Stopping, and a change of period or alert radius, always apply. When a move is skipped, the returned Message shows the last applied position, so sending one every few seconds is fine.
stopMessageLiveLocation
POST/bot<token>/stopMessageLiveLocation
Parameters: chat_id, message_id, reply_markup (optional). Stops the live location where it last was. Its period shrinks to the time that actually passed, so every app draws it as finished. If it has already stopped or ended you get 400 Bad Request: message is not modified: …. Returns: the edited Message.
editMessageText
POST/bot<token>/editMessageText
| Parameter | Type | Required | Description |
|---|---|---|---|
chat_id | Integer or String | Yes | The chat the message is in |
message_id | Integer | Yes | The message to edit (one the bot sent) |
text | String | Yes | The new text, 1–4,096 characters |
reply_markup | InlineKeyboardMarkup | Optional | New buttons. If you leave it out, the buttons are removed (same as Telegram) |
inline_message_id | String | Not supported | 400, since there's no inline mode |
Returns: the edited Message. The change shows in the app right away, and messages edited by people reach the bot as edited_message.
If the text and buttons are exactly the same as before you get 400 Bad Request: message is not modified: …, and trying to edit someone else's message gives 400 Bad Request: message can't be edited.
editMessageCaption
POST/bot<token>/editMessageCaption
Parameters: chat_id, message_id, caption (0–1,024 characters; leave it out to remove the caption), reply_markup. Returns: the edited Message.
editMessageReplyMarkup
POST/bot<token>/editMessageReplyMarkup
Changes only the buttons and leaves the text as it is. Parameters: chat_id, message_id, reply_markup (leave it out to remove the buttons). Returns: the edited Message.
deleteMessage
POST/bot<token>/deleteMessage
Parameters: chat_id, message_id. Deletes the message for both sides of the chat. If the message doesn't exist: 400 Bad Request: message to delete not found. Returns: True.
answerCallbackQuery
POST/bot<token>/answerCallbackQuery
Answers a button someone pressed (CallbackQuery). You can answer within 10 seconds, and only once.
| Parameter | Type | Required | Description |
|---|---|---|---|
callback_query_id | String | Yes | The update's callback_query.id |
text | String | Optional | Text that pops up briefly at the top of the screen |
show_alert | Boolean | Optional | If true, shown as a dialog with an OK button |
url | String | Optional | An address for the app to open |
cache_time | Integer | Optional | Seconds for the app to remember this answer |
Returns: True. If it's too late or the query was already answered: 400 Bad Request: query is too old and response timeout expired or query ID is invalid.
sendChatAction
POST/bot<token>/sendChatAction
Accepted for compatibility and returns True, but the app doesn't show "typing…" yet.
getChat
GET/bot<token>/getChat?chat_id=…
Parameter: chat_id (a number). For a person, that person; for a group, only groups the bot is a member of. Returns: Chat. If it doesn't exist or can't be seen: 400 Bad Request: chat not found.
getFile
GET/bot<token>/getFile?file_id=…
Gets a download path from the file_id of a received photo. Returns: File. Then download it from this address.
https://pabal.me/file/bot<token>/<file_path>
A file_id is also the right to fetch that photo. If the value is wrong: 400 Bad Request: wrong file identifier/HTTP URL specified.
setMyCommands
POST/bot<token>/setMyCommands
| Parameter | Type | Required | Description |
|---|---|---|---|
commands | Array of BotCommand | Yes | Up to 100 |
language_code | String | Optional | Stored as a per-language list. The app currently shows only the default list without a language code |
scope | BotCommandScope | Ignored |
Returns: True. If the menu breaks the rules, nothing is kept: 400 Bad Request: BOT_COMMAND_INVALID (a command), BOT_COMMAND_DESCRIPTION_INVALID (an empty description or one over 256 characters), BOT_COMMANDS_TOO_MUCH (more than 100).
getMyCommands
GET/bot<token>/getMyCommands
Parameter: language_code (optional). Returns: Array of BotCommand.
deleteMyCommands
POST/bot<token>/deleteMyCommands
Parameter: language_code (optional). Empties that list. Returns: True.
logOut · close
On Telegram, these methods are used when moving to a local Bot API server. Pabal has nowhere to move to, so it just accepts them and returns True. To invalidate a token, use BotFather's /revoke.
Objects
Optional next to a field means it may be absent. Other Telegram fields not listed here are never sent by Pabal.
Update
One new update. Contains update_id and one of the fields below.
| Field | Type | Description |
|---|---|---|
update_id | Integer | A number that increases by 1 with each update. Used for getUpdates' offset |
message Optional | Message | A new message to the bot (one-to-one, or every message in a group the bot is a member of) |
edited_message Optional | Message | An edited message |
callback_query Optional | CallbackQuery | An inline button press |
voice_call Optional | VoiceCall | The start and end of a call to a voice bot or of a call it made (arrives only if asked for by name) |
voice_call_tool_query Optional | VoiceCallToolQuery | A bot tool the model called during a call (arrives only if asked for by name) |
voice_call_transcript Optional | VoiceCallTranscript | One turn of a call's transcript during the call (arrives only if asked for by name) |
User
| Field | Type | Description |
|---|---|---|
id | Integer | User ID (meaningful only within this server) |
is_bot | Boolean | true for bots |
first_name | String | First name. Deleted Account for deleted accounts |
last_name Optional | String | Last name |
username Optional | String | Username (without @) |
Chat
| Field | Type | Description |
|---|---|---|
id | Integer | For a person, that person's ID (positive); for a basic group, negative |
type | String | private or group |
title Optional | String | Group name (group) |
first_name, last_name, username Optional | String | The other person's name and username (private) |
Message
| Field | Type | Description |
|---|---|---|
message_id | Integer | The message's number within this chat |
from Optional | User | The sender |
chat | Chat | The chat the message is in |
date | Integer | When it was sent (Unix seconds) |
edit_date Optional | Integer | When it was last edited |
text Optional | String | The text (always present unless it's a photo, file, location, place or contact message) |
entities Optional | Array of MessageEntity | Commands, mentions, URLs and hashtags in the text |
photo Optional | Array of PhotoSize | Photo (Pabal has just the original) |
document Optional | Document | A file |
location Optional | Location | A location or live location. A place message carries its location here too |
venue Optional | Venue | A place |
contact Optional | Contact | A contact card |
caption Optional | String | Photo caption |
caption_entities Optional | Array of MessageEntity | Commands, mentions, URLs and hashtags in the caption |
reply_markup Optional | InlineKeyboardMarkup | Inline buttons attached to the message |
MessageEntity
| Field | Type | Description |
|---|---|---|
type | String | bot_command, mention, url, hashtag |
offset | Integer | Start position (in UTF-16 code units) |
length | Integer | Length (in UTF-16 code units) |
The server finds these in the text and adds them automatically. Formatting entities such as bold and italic don't exist yet.
PhotoSize
| Field | Type | Description |
|---|---|---|
file_id | String | The ID used for downloading (getFile) and resending (sendPhoto) |
file_unique_id | String | The same value for the same photo, even across bots. Can't be used for downloading |
width, height | Integer | Size in pixels |
file_size | Integer | Bytes |
Document
| Field | Type | Description |
|---|---|---|
file_id | String | The ID used for downloading (getFile) and resending (sendDocument) |
file_unique_id | String | The same value for the same file, even across bots. Can't be used for downloading |
file_name | String | The name the sender gave it |
mime_type | String | The type the sender declared |
file_size | Integer | Bytes |
Location
| Field | Type | Description |
|---|---|---|
latitude, longitude | Float | Latitude and longitude |
horizontal_accuracy Optional | Float | Radius of uncertainty in meters |
live_period Optional | Integer | Sharing period of a live location, in seconds counted from when the message was sent. Once stopped, it shrinks to the time that actually passed |
heading Optional | Integer | Direction of travel, 1–360 degrees |
proximity_alert_radius Optional | Integer | Proximity alert radius in meters |
A live location a person shares arrives again as edited_message each time the server applies a move.
Venue
| Field | Type | Description |
|---|---|---|
location | Location | Where the place is |
title | String | Name of the place |
address | String | Address |
foursquare_id, foursquare_type Optional | String | Foursquare's venue ID and type |
google_place_id, google_place_type Optional | String | Google place ID and type |
Contact
| Field | Type | Description |
|---|---|---|
phone_number | String | The number, as the sender wrote it |
first_name | String | First name |
last_name Optional | String | Last name |
user_id Optional | Integer | The Pabal account with that number. Present only when the sender has that person saved in their contacts, or is that person |
vcard Optional | String | Additional data in vCard format |
File
| Field | Type | Description |
|---|---|---|
file_id, file_unique_id | String | Same as PhotoSize |
file_size | Integer | Bytes |
file_path | String | Of the form photos/<file_id>.jpg or documents/<file_id>. Append it to /file/bot<token>/ to download |
CallbackQuery
| Field | Type | Description |
|---|---|---|
id | String | The ID to pass to answerCallbackQuery |
from | User | The person who pressed the button |
message Optional | Message | The message the button is attached to |
chat_instance | String | A value identifying that chat |
data Optional | String | The button's callback_data |
InlineKeyboardMarkup
inline_keyboard: Array of Array of InlineKeyboardButton — the outer array is the rows, and each inner array is the buttons in one row. Up to 100 rows and 100 buttons per message.
InlineKeyboardButton
| Field | Type | Description |
|---|---|---|
text | String | The button's label |
callback_data One of the two | String | The value sent to the bot when pressed, 1–64 bytes |
url One of the two | String | The address opened when pressed |
Other kinds, such as switch_inline_query, web_app, login_url and pay, don't exist yet (400).
ReplyKeyboardMarkup
| Field | Type | Description |
|---|---|---|
keyboard | Array of Array of KeyboardButton | The panel of buttons below the input field |
resize_keyboard, one_time_keyboard, is_persistent, selective Optional | Boolean | Fit to size · hide after one use · always shown · only for specific people |
input_field_placeholder Optional | String | Placeholder text in the input field |
KeyboardButton
A single string, or an object with text and the optional fields request_contact (send my contact) and request_location (send my location), both Boolean.
ReplyKeyboardRemove
{"remove_keyboard": true} — hides the panel of buttons. selective is optional.
ForceReply
{"force_reply": true} — the app opens the input field ready to reply to this message. selective and input_field_placeholder are optional.
BotCommand
| Field | Type | Description |
|---|---|---|
command | String | 1–32 characters of lowercase letters, digits and underscores (without /) |
description | String | 1–256 characters |
WebhookInfo
| Field | Type | Description |
|---|---|---|
url | String | The webhook address; an empty string if there is none |
has_custom_certificate | Boolean | Always false |
pending_update_count | Integer | The number of updates waiting to be delivered |
ip_address Optional | String | The IP it last sent to |
last_error_date Optional | Integer | When the last failure happened (Unix seconds) |
last_error_message Optional | String | The reason for the last failure (list) |
max_connections Optional | Integer | The value given to setWebhook |
allowed_updates Optional | Array of String | The value given to setWebhook |
Voice calls
These methods exist only in Pabal. A voice bot is a bot that people can call from the app and that an AI voice model answers; these methods configure that model and handle calls. What has to be turned on, how to turn it on and examples are in the Voice bots guide. The methods exist even on servers with voice off, so you can read the settings, but a bot on a server with voice off, or one the operator has not allowed yet, cannot change its settings except to turn things off and delete them (403 VOICE_NOT_AVAILABLE).
setVoiceCallSettings
POST/bot<token>/setVoiceCallSettings
Changes the bot's voice settings. Only what you send changes; everything is checked first and then written at once, or all of it is rejected. 30 times an hour per bot (together with @BotFather).
| Parameter | Type | Required | Description |
|---|---|---|---|
enabled | Boolean | Optional | Can be turned on only after agreeing to the voice-bot rules in @BotFather (otherwise 403 VOICE_RULES_NOT_ACCEPTED). Turning off always works |
instructions | String | Optional | The model's instructions, up to 8,192 bytes. "" clears them (not for a managed bot that is on) |
greeting | String | Optional | The first greeting, up to 512 bytes. With "" the model decides |
voice | String | Optional | Voice id, default kore (list) |
language | String | Optional | ko-KR (default) · en-US · ja-JP · cmn-CN · fr-FR · es-US · id-ID · vi-VN · ar-EG (list). Case-insensitive; zh-CN is stored as cmn-CN. A bot in a language other than Korean or English takes calls only after the operator has read its instructions and greeting and allowed it |
max_call_duration | Integer | Optional | Seconds, 60 to limits.max_call_duration |
max_concurrent_calls | Integer | Optional | 1 to limits.max_concurrent_calls |
tools | Array of VoiceCallTool | Optional | The bot's tools that the model calls during a call: {"name", "description", "parameters"?, "timeout_ms"?}. Rules in Defining tools. A JSON string in forms |
transcript | Boolean | Optional | The transcript switch. Can be turned on only after agreeing to the bot owner terms |
mode | String | Optional | managed (Pabal's AI model) or stream (stream mode) |
stream_url, stream_secret | String | Optional | Together with mode=stream. The secret only in the request body |
Returns: True
getVoiceCallSettings
GET/bot<token>/getVoiceCallSettings
No parameters. The current settings; whether the bot can take calls right now (available, unavailable_reason); the server's limits; the lists of voices and languages; where tool queries and transcripts go; and the state of the own key, stream mode and MCP servers. Keys and secrets are never shown, only fingerprints. The fields are listed in Voice bots — Current settings.
Returns: VoiceCallSettings (an object)
endVoiceCall
POST/bot<token>/endVoiceCall
| Parameter | Type | Required | Description |
|---|---|---|---|
voice_call_id | String | Yes | voice_call.id |
farewell | Boolean | Optional | Default true: the model says a short goodbye and hangs up (if it has not hung up within 10 seconds, the server ends the call). With false, it hangs up at once |
Returns: True. If it is not a live call of this bot, 400 VOICE_CALL_NOT_FOUND.
sendVoiceCallContext
POST/bot<token>/sendVoiceCallContext
| Parameter | Type | Required | Description |
|---|---|---|---|
voice_call_id | String | Yes | A call that is on |
text | String | Yes | The bot owner's note to the model. Up to 2,000 characters and 6,144 bytes, checked against the content rules |
respond | Boolean | Optional | Default false. With true the model reacts right away |
Returns: True. Errors: VOICE_CALL_NOT_ACTIVE before the call connects, VOICE_CONTEXT_NOT_SUPPORTED for a stream call, VOICE_CONTEXT_TOO_LONG if the text is too long, and 429 for more than 10 per minute per call.
getVoiceCallUsage
GET/bot<token>/getVoiceCallUsage
| Parameter | Type | Required | Description |
|---|---|---|---|
period | String | Optional | today (default) or month |
month | String | Optional | YYYY-MM, only with period=month |
Returns: period, from_date, to_date, calls, seconds, missed, refused (busy·budget·unavailable), platform_seconds, own_key_seconds, remaining_seconds_today (bot·owner), and outbound (today·limits) for a bot that has ever turned calling on — day boundaries follow the server's time zone
startVoiceCall
POST/bot<token>/startVoiceCall
The bot calls a person. Only a bot whose owner turned this on in @BotFather and whom the operator reviewed can call, and only people who allowed that bot to call them. The answers and how requests are judged are in Voice bots — Calling.
| Parameter | Type | Required | Description |
|---|---|---|---|
user_id | Integer | Yes | The person to call |
purpose | String | Yes | One the bot chose of reminder, wake_up, delivery, appointment |
context | String | Yes | What the model alone is told, up to 1,000 characters and 3,072 bytes, checked against the content rules |
max_duration | Integer | Optional | The longest the call may last once answered, seconds, 30–300 (up to the bot's own maximum). 180 by default |
ring_timeout | Integer | Optional | Seconds it rings, 30–45. 40 by default |
Returns: {"voice_call_id": "…"} — it only means the request was taken; whether it rang and whether it was answered come as voice_call updates. Refusals: 403 VOICE_OUTBOUND_OFF: <reason>, 403 USER_UNAVAILABLE (always the same body), 429, 503 VOICE_UNAVAILABLE. It cannot be called from a webhook answer.
answerVoiceCallToolQuery
POST/bot<token>/answerVoiceCallToolQuery
| Parameter | Type | Required | Description |
|---|---|---|---|
tool_query_id | String | Yes | The id of the VoiceCallToolQuery |
result | String · Object · Array | One of the two | The tool's result, up to 16,384 bytes |
error | String | One of the two | The bot's failure message, up to 512 bytes |
Returns: True. The first answer is final. Late answers get 400 TOOL_QUERY_EXPIRED: answered|timeout|cancelled|call_ended|unreachable, an unknown id TOOL_QUERY_ID_INVALID, and refused answers TOOL_RESULT_EMPTY·TOOL_RESULT_INVALID·TOOL_RESULT_TOO_LONG·TOOL_ERROR_TOO_LONG·TOOL_RESULT_REFUSED: <code>. A tool-query POST to a webhook can also be answered in the response body.
setVoiceCallApiKey · deleteVoiceCallApiKey
POST/bot<token>/setVoiceCallApiKey
| Parameter | Type | Required | Description |
|---|---|---|---|
provider | String | Yes | google (a Vertex AI service account JSON key) or openai (sk-…) |
api_key | String | Yes | The key. Only in the request body (in the query: 400), up to 8 KiB |
Returns: True — the key is kept only after passing a test session on the gateway. API_KEY_INVALID, API_KEY_NO_REALTIME_ACCESS, 503 API_KEY_CHECK_UNAVAILABLE (not kept), 403 VOICE_OWN_KEY_NOT_AVAILABLE (this server does not accept own keys). deleteVoiceCallApiKey takes no parameters and always returns True. Neither method can be called with bots.sendCustomRequest, and setVoiceCallApiKey cannot be called in a webhook response either. Details in Own model key.
MCP server methods
| Method | Parameters | Returns |
|---|---|---|
setVoiceCallMcpServer | label, url, auth (object), optional audience, allowed_tools, requested_level, timeout_ms, tools (classification overrides), declaration, auto_repin | The registration result (status, job_id). If the recipient changes, 400 MCP_CHANGE_NEEDS_CONFIRMATION, then confirm in @BotFather |
refreshVoiceCallMcpServer | label | Check again (refreshing: true, job_id) |
deleteVoiceCallMcpServer | label | True — works even with MCP off |
getVoiceCallMcpBinding | None | iss, sub (bot:<id>), issuer_document, jwks_uri, audiences |
createVoiceCallMcpClientKey | label | The public jwk and thumbprint (the private key never leaves Pabal) |
Send auth and secrets only in the request body. On a server with MCP off: 403 VOICE_NOT_AVAILABLE: MCP tools are not available on this server. Details in Voice bots — MCP servers.
Receiving voice updates
voice_call, voice_call_tool_query and voice_call_transcript arrive only if asked for by name — list them in allowed_updates of getUpdates or setWebhook. They do not arrive with an empty list, and what happened before you asked is not kept. Tool queries come as separate POSTs if there is a webhook, or at the front of the next getUpdates if getUpdates was called within the last 15 seconds.
VoiceCall
| Field | Type | Description |
|---|---|---|
id | String | Call id (an unsigned decimal number) |
direction | String | incoming (a call to the bot) or outgoing (a call the bot made). Always there |
event | String | started, ended, missed, refused; for a call the bot made, started, ended, not_answered, failed |
from Optional | User | Calls to the bot only — the caller: id, is_bot, first_name, last_name, language_code. No username or phone number |
to Optional | Object | Calls the bot made only — {"id": …}, the callee's id alone |
purpose Optional | String | Calls the bot made only — the request's purpose |
date | Integer | When it happened (Unix seconds). started is when the key exchange finished for an app call, and when the watch's audio connected for an Apple Watch call. On a server that waits for transcripts or has conversation keeping on, ended arrives up to 10 seconds late. For a call the bot made, an answered call's ended always arrives up to 10 seconds late, whatever the server's settings, while the server waits for the gateway's usage |
max_duration Optional | Integer | This call's maximum length (seconds) — started, ended |
start_date, duration Optional | Integer | When it came on and how many seconds it was on (server clock) — ended |
end_reason Optional | String | caller (callee for a call the bot made), bot, max_length, failed, operator, suspended — ended, missed |
refused_reason Optional | String | busy, budget, unavailable — refused |
transcript Optional | Object | {"turns": [{"role", "text", "offset_ms"}], "truncated"} — ended, when transcripts were on. Never on a call the bot made |
VoiceCall has no field that tells you whether the caller chose to keep the conversation. This is on purpose — that text belongs to the caller and does not go to the bot. That's why, on a server with conversation keeping on, ended waits the same amount for every call that isn't in stream mode, and the number of the message after a call record is at least 31 more than the record's in every call (the numbers are reserved in advance).
VoiceCallToolQuery
| Field | Type | Description |
|---|---|---|
id | String | The id to give to answerVoiceCallToolQuery |
voice_call_id | String | Call id |
from | User | The caller (same fields as in VoiceCall) |
name | String | Tool name |
arguments | String | The arguments, as the text of a JSON object (checked by the server) |
budget_ms | Integer | Milliseconds from receipt until the answer must reach the server |
VoiceCallTranscript
| Field | Type | Description |
|---|---|---|
voice_call_id | String | Call id |
seq | Integer | Turn number |
role | String | caller or bot |
text | String | The text written down by the AI. If it contains a card or resident registration number, the digits become * |
is_final | Boolean | Always true |
offset_ms, date | Integer | Milliseconds since the call's audio started, and the time |
Arrives only through the HTTP Bot API. The whole call is in the transcript of the ended voice_call.
Stream mode (mode=stream)
In stream mode, the call's audio goes to the bot owner's server instead of Pabal's AI model. Send mode=stream, stream_url=wss://… and stream_secret=… (32–128 printable ASCII characters, only in the request body) to setVoiceCallSettings, and Pabal's gateway connects to that address once (a probe); if that works, stream mode turns on. It needs the bot owner terms and an app build with the stream confirmation sheet. Setup, probe results and an example are in Voice bots — Stream mode.
- Connection: TLS 1.2 or later, a public certificate matching the name in the URL, the subprotocol
pabal.voice.v1, no compression, messages up to 64 KiB and text up to 4 KiB, no redirects. - Signature:
X-Pabal-Voice-Signature: t=…,nonce=…,call=…,v1=HMAC-SHA256(stream_secret, "pabal-voice-stream/1\n<t>\n<nonce>\n<call>\n<host>\n<path?query>").hostis the host of the URL you gave. Check the 300 seconds, each nonce once, byte-by-byte comparison, and thatstart.call_idequalscall(details). - Audio: text messages are JSON events; binary is PCM16 little-endian, mono, 24 kHz. The caller's audio comes in 20 ms (960-byte) chunks.
- Pabal → bot:
start(once) ·live(the AI notice has finished) · audio ·mark·cleared·buffer_full·stop(→ close 1000). - Bot → Pabal:
ready(within 3 seconds afterstart, before anything else) · audio (afterlive) ·clear·mark·hangup.
The shape of each message is in the table in Voice bots — Wire format.
Errors from the voice methods
The voice methods' error codes and their meanings are collected in Voice bots — Errors. 403 has the form Forbidden: <code>: <description> (VOICE_RULES_NOT_ACCEPTED, VOICE_NOT_AVAILABLE, VOICE_SUSPENDED, VOICE_OWNER_TERMS_NOT_ACCEPTED, VOICE_OWN_KEY_NOT_AVAILABLE, VOICE_STREAM_NOT_AVAILABLE, VOICE_OUTBOUND_OFF) — except USER_UNAVAILABLE, which is always exactly Forbidden: USER_UNAVAILABLE, with no description — and going over an hourly limit gives 429 with retry_after.
Differences from the Telegram Bot API
- Kinds of updates: only
message,edited_messageandcallback_query, plus the voice-bot kinds that exist only in Pabal (voice_call,voice_call_tool_query,voice_call_transcript). There are no channel posts, inline queries, payments, polls, membership changes (my_chat_member) and so on. - Formatting:
parse_modeandentitiesare ignored and text is sent as is. Only commands, mentions, URLs and hashtags are highlighted automatically. - Kinds of chats: one-to-one and basic groups only. No channels, supergroups or forum topics.
- Media: photos, files (
sendDocument), locations, live locations, places and contacts only. No messages that play as video or audio, no stickers, no albums (sendMediaGroup), no polls and no sending by URL. Uploads and downloads over HTTP are capped at 20 MB. - Locations: a live location's proximity alert (
proximity_alert_radius) is only stored, never sent. Moves within 10 seconds or 10 m are skipped, and a contact card sent by a bot carries nouser_id. - Bots in groups: there's no privacy mode, so bots receive every message in the group.
- Queue: unfetched updates are kept in memory, up to the most recent 1,000 per bot, and are lost when the server restarts (Telegram keeps them for 24 hours).
- Webhooks: updates are sent one at a time, in order (
max_connectionsis only stored), self-signed certificates aren't accepted, and there's no restriction on the port. - IDs: user, message and file IDs are meaningful only within this server. With no channels or supergroups, there are no IDs of the form
-100…either. - Missing methods: anything not in the list above (
forwardMessage,copyMessage,sendVideo,sendPoll,getChatMember,banChatMember,answerInlineQuery…) returns404 Not Found: method not found.