Developer docs
English

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 GET and POST work.
  • Parameters can be sent in four ways — a query string (?chat_id=1&text=hi), application/x-www-form-urlencoded, application/json, and multipart/form-data when 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_codeWhenExample description
400A parameter is wrongBad 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
401The token is wrongUnauthorized
403No permission to sendForbidden: bot can't initiate conversation with a user, Forbidden: bot is not a member of the group chat
404Unknown method or fileNot Found: method not found
409getUpdates while a webhook is setConflict: can't use getUpdates method while webhook is active; use deleteWebhook to delete the webhook first
413The body is larger than 12 MBRequest Entity Too Large
500Internal server errorInternal 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.

ParameterTypeRequiredDescription
offsetIntegerOptionalOnly updates with this number or higher. Lower ones are removed as "received". Pass the last handled update_id + 1.
limitIntegerOptional1–100, default 100
timeoutIntegerOptionalSeconds to wait, 0–50, default 0. We recommend 25–30.
allowed_updatesArray of StringOptionalKinds 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.

ParameterTypeRequiredDescription
urlStringYesA public https:// address. An empty string removes the webhook
secret_tokenStringOptional1–256 characters, A-Z a-z 0-9 _ -. Sent in the X-Telegram-Bot-Api-Secret-Token request header
allowed_updatesArray of StringOptionalAny 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_updatesBooleanOptionalDiscards the waiting updates
max_connectionsIntegerOptional1–100, default 40. Only stored; delivery is one at a time per bot
certificateInputFileNot supported400 — use a certificate from a public authority
ip_addressStringIgnored

Returns: True

deleteWebhook

POST/bot<token>/deleteWebhook

Removes the webhook and goes back to getUpdates. Succeeds even if there's no webhook.

ParameterTypeRequiredDescription
drop_pending_updatesBooleanOptionalDiscards 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

MethodWhat it does
getMeInformation about the bot itself
sendMessageSend text (buttons included)
sendPhotoSend a photo
sendDocumentSend a file
sendLocationSend a location (live locations too)
sendVenueSend a place (a location with a name and an address)
sendContactSend a contact card
editMessageLiveLocation · stopMessageLiveLocationMove or stop a live location
editMessageTextEdit the text and buttons of a sent message
editMessageCaptionEdit a photo caption
editMessageReplyMarkupEdit only the buttons
deleteMessageDelete a message
answerCallbackQueryAnswer a button press
sendChatAction"Typing" indicator (accepted only)
getChatChat information
getFileDownload path of a received photo
setMyCommands · getMyCommands · deleteMyCommandsCommand menu
logOut · closeFor compatibility (they do nothing)
getUpdates · setWebhook · deleteWebhook · getWebhookInfoGetting updates (above)
setVoiceCallSettings · getVoiceCallSettings · endVoiceCall · sendVoiceCallContext · getVoiceCallUsage · startVoiceCall · answerVoiceCallToolQuery · setVoiceCallApiKey · setVoiceCallMcpServer …Voice bots (see Voice calls below)
What you can put in 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

ParameterTypeRequiredDescription
chat_idInteger or StringYesThe chat to send to (see the box above)
textStringYes1–4,096 characters. Sent as plain text
reply_markupInlineKeyboardMarkup · ReplyKeyboardMarkup · ReplyKeyboardRemove · ForceReplyOptionalButtons
disable_notificationBooleanOptionalSend silently
parse_mode, entities, reply_parameters, link_preview_options …IgnoredAccepted 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

ParameterTypeRequiredDescription
chat_idInteger or StringYesThe chat to send to
photoInputFile or StringYesA 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
captionStringOptional0–1,024 characters
reply_markupSame as sendMessageOptional
disable_notificationBooleanOptional

Returns: the sent Message (with a new file_id in photo)

sendDocument

POST/bot<token>/sendDocument

ParameterTypeRequiredDescription
chat_idInteger or StringYesThe chat to send to
documentInputFile or StringYesA 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
filenameStringOptionalThe name the recipient sees. Without it, the name of the uploaded file is used
captionStringOptional0–1,024 characters
reply_markupSame as sendMessageOptional
disable_notificationBooleanOptional
thumbnail, disable_content_type_detectionIgnoredAccepted 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

ParameterTypeRequiredDescription
chat_idInteger or StringYesThe chat to send to
latitude, longitudeFloatYesLatitude (−90 to 90) and longitude (−180 to 180). Out of range gives 400 Bad Request: GEO_POINT_INVALID
horizontal_accuracyFloatOptionalRadius of uncertainty in meters, 0–1,500. Rounded to a whole number when stored
live_periodIntegerOptionalMakes it a live location: 60–86,400 seconds, or 2147483647 (0x7FFFFFFF) until stopped. Anything else gives 400 Bad Request: wrong live_period
headingIntegerOptionalDirection of travel of a live location, 1–360 degrees
proximity_alert_radiusIntegerOptionalProximity alert radius of a live location, in meters. Stored and shown, but no alert is sent yet
reply_markupSame as sendMessageOptional
disable_notificationBooleanOptional

Returns: the sent Message (with a Location in location)

sendVenue

POST/bot<token>/sendVenue

ParameterTypeRequiredDescription
chat_idInteger or StringYesThe chat to send to
latitude, longitudeFloatYesWhere the place is (as in sendLocation)
titleStringYesName of the place
addressStringYesAddress
foursquare_id, foursquare_typeStringOptionalFoursquare's venue ID and type, passed on as given
google_place_id, google_place_typeStringOptionalGoogle place ID and type. Used only when there is no foursquare_id
reply_markupSame as sendMessageOptional
disable_notificationBooleanOptional

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

ParameterTypeRequiredDescription
chat_idInteger or StringYesThe chat to send to
phone_numberStringYesPhone number, passed on as written
first_nameStringYesFirst name
last_nameStringOptionalLast name
vcardStringOptionalAdditional data in vCard format, up to 64 KB
reply_markupSame as sendMessageOptional
disable_notificationBooleanOptional

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

ParameterTypeRequiredDescription
chat_idInteger or StringYesThe chat the message is in
message_idIntegerYesThe live location to move (one the bot sent)
latitude, longitudeFloatYesThe new position
horizontal_accuracyFloatOptionalRadius of uncertainty in meters, 0–1,500
headingIntegerOptionalDirection of travel, 1–360 degrees
proximity_alert_radiusIntegerOptionalProximity alert radius in meters. 0 turns it off
live_periodIntegerOptionalNew 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_markupInlineKeyboardMarkupOptionalNew buttons
inline_message_idStringNot supported400, 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

ParameterTypeRequiredDescription
chat_idInteger or StringYesThe chat the message is in
message_idIntegerYesThe message to edit (one the bot sent)
textStringYesThe new text, 1–4,096 characters
reply_markupInlineKeyboardMarkupOptionalNew buttons. If you leave it out, the buttons are removed (same as Telegram)
inline_message_idStringNot supported400, 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.

ParameterTypeRequiredDescription
callback_query_idStringYesThe update's callback_query.id
textStringOptionalText that pops up briefly at the top of the screen
show_alertBooleanOptionalIf true, shown as a dialog with an OK button
urlStringOptionalAn address for the app to open
cache_timeIntegerOptionalSeconds 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

ParameterTypeRequiredDescription
commandsArray of BotCommandYesUp to 100
language_codeStringOptionalStored as a per-language list. The app currently shows only the default list without a language code
scopeBotCommandScopeIgnored

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.

FieldTypeDescription
update_idIntegerA number that increases by 1 with each update. Used for getUpdates' offset
message OptionalMessageA new message to the bot (one-to-one, or every message in a group the bot is a member of)
edited_message OptionalMessageAn edited message
callback_query OptionalCallbackQueryAn inline button press
voice_call OptionalVoiceCallThe 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 OptionalVoiceCallToolQueryA bot tool the model called during a call (arrives only if asked for by name)
voice_call_transcript OptionalVoiceCallTranscriptOne turn of a call's transcript during the call (arrives only if asked for by name)

User

FieldTypeDescription
idIntegerUser ID (meaningful only within this server)
is_botBooleantrue for bots
first_nameStringFirst name. Deleted Account for deleted accounts
last_name OptionalStringLast name
username OptionalStringUsername (without @)

Chat

FieldTypeDescription
idIntegerFor a person, that person's ID (positive); for a basic group, negative
typeStringprivate or group
title OptionalStringGroup name (group)
first_name, last_name, username OptionalStringThe other person's name and username (private)

Message

FieldTypeDescription
message_idIntegerThe message's number within this chat
from OptionalUserThe sender
chatChatThe chat the message is in
dateIntegerWhen it was sent (Unix seconds)
edit_date OptionalIntegerWhen it was last edited
text OptionalStringThe text (always present unless it's a photo, file, location, place or contact message)
entities OptionalArray of MessageEntityCommands, mentions, URLs and hashtags in the text
photo OptionalArray of PhotoSizePhoto (Pabal has just the original)
document OptionalDocumentA file
location OptionalLocationA location or live location. A place message carries its location here too
venue OptionalVenueA place
contact OptionalContactA contact card
caption OptionalStringPhoto caption
caption_entities OptionalArray of MessageEntityCommands, mentions, URLs and hashtags in the caption
reply_markup OptionalInlineKeyboardMarkupInline buttons attached to the message

MessageEntity

FieldTypeDescription
typeStringbot_command, mention, url, hashtag
offsetIntegerStart position (in UTF-16 code units)
lengthIntegerLength (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

FieldTypeDescription
file_idStringThe ID used for downloading (getFile) and resending (sendPhoto)
file_unique_idStringThe same value for the same photo, even across bots. Can't be used for downloading
width, heightIntegerSize in pixels
file_sizeIntegerBytes

Document

FieldTypeDescription
file_idStringThe ID used for downloading (getFile) and resending (sendDocument)
file_unique_idStringThe same value for the same file, even across bots. Can't be used for downloading
file_nameStringThe name the sender gave it
mime_typeStringThe type the sender declared
file_sizeIntegerBytes

Location

FieldTypeDescription
latitude, longitudeFloatLatitude and longitude
horizontal_accuracy OptionalFloatRadius of uncertainty in meters
live_period OptionalIntegerSharing 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 OptionalIntegerDirection of travel, 1–360 degrees
proximity_alert_radius OptionalIntegerProximity alert radius in meters

A live location a person shares arrives again as edited_message each time the server applies a move.

Venue

FieldTypeDescription
locationLocationWhere the place is
titleStringName of the place
addressStringAddress
foursquare_id, foursquare_type OptionalStringFoursquare's venue ID and type
google_place_id, google_place_type OptionalStringGoogle place ID and type

Contact

FieldTypeDescription
phone_numberStringThe number, as the sender wrote it
first_nameStringFirst name
last_name OptionalStringLast name
user_id OptionalIntegerThe Pabal account with that number. Present only when the sender has that person saved in their contacts, or is that person
vcard OptionalStringAdditional data in vCard format

File

FieldTypeDescription
file_id, file_unique_idStringSame as PhotoSize
file_sizeIntegerBytes
file_pathStringOf the form photos/<file_id>.jpg or documents/<file_id>. Append it to /file/bot<token>/ to download

CallbackQuery

FieldTypeDescription
idStringThe ID to pass to answerCallbackQuery
fromUserThe person who pressed the button
message OptionalMessageThe message the button is attached to
chat_instanceStringA value identifying that chat
data OptionalStringThe 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

FieldTypeDescription
textStringThe button's label
callback_data One of the twoStringThe value sent to the bot when pressed, 1–64 bytes
url One of the twoStringThe address opened when pressed

Other kinds, such as switch_inline_query, web_app, login_url and pay, don't exist yet (400).

ReplyKeyboardMarkup

FieldTypeDescription
keyboardArray of Array of KeyboardButtonThe panel of buttons below the input field
resize_keyboard, one_time_keyboard, is_persistent, selective OptionalBooleanFit to size · hide after one use · always shown · only for specific people
input_field_placeholder OptionalStringPlaceholder 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

FieldTypeDescription
commandString1–32 characters of lowercase letters, digits and underscores (without /)
descriptionString1–256 characters

WebhookInfo

FieldTypeDescription
urlStringThe webhook address; an empty string if there is none
has_custom_certificateBooleanAlways false
pending_update_countIntegerThe number of updates waiting to be delivered
ip_address OptionalStringThe IP it last sent to
last_error_date OptionalIntegerWhen the last failure happened (Unix seconds)
last_error_message OptionalStringThe reason for the last failure (list)
max_connections OptionalIntegerThe value given to setWebhook
allowed_updates OptionalArray of StringThe 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).

ParameterTypeRequiredDescription
enabledBooleanOptionalCan be turned on only after agreeing to the voice-bot rules in @BotFather (otherwise 403 VOICE_RULES_NOT_ACCEPTED). Turning off always works
instructionsStringOptionalThe model's instructions, up to 8,192 bytes. "" clears them (not for a managed bot that is on)
greetingStringOptionalThe first greeting, up to 512 bytes. With "" the model decides
voiceStringOptionalVoice id, default kore (list)
languageStringOptionalko-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_durationIntegerOptionalSeconds, 60 to limits.max_call_duration
max_concurrent_callsIntegerOptional1 to limits.max_concurrent_calls
toolsArray of VoiceCallToolOptionalThe bot's tools that the model calls during a call: {"name", "description", "parameters"?, "timeout_ms"?}. Rules in Defining tools. A JSON string in forms
transcriptBooleanOptionalThe transcript switch. Can be turned on only after agreeing to the bot owner terms
modeStringOptionalmanaged (Pabal's AI model) or stream (stream mode)
stream_url, stream_secretStringOptionalTogether 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

ParameterTypeRequiredDescription
voice_call_idStringYesvoice_call.id
farewellBooleanOptionalDefault 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

ParameterTypeRequiredDescription
voice_call_idStringYesA call that is on
textStringYesThe bot owner's note to the model. Up to 2,000 characters and 6,144 bytes, checked against the content rules
respondBooleanOptionalDefault 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

ParameterTypeRequiredDescription
periodStringOptionaltoday (default) or month
monthStringOptionalYYYY-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.

ParameterTypeRequiredDescription
user_idIntegerYesThe person to call
purposeStringYesOne the bot chose of reminder, wake_up, delivery, appointment
contextStringYesWhat the model alone is told, up to 1,000 characters and 3,072 bytes, checked against the content rules
max_durationIntegerOptionalThe longest the call may last once answered, seconds, 30–300 (up to the bot's own maximum). 180 by default
ring_timeoutIntegerOptionalSeconds 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

ParameterTypeRequiredDescription
tool_query_idStringYesThe id of the VoiceCallToolQuery
resultString · Object · ArrayOne of the twoThe tool's result, up to 16,384 bytes
errorStringOne of the twoThe 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

ParameterTypeRequiredDescription
providerStringYesgoogle (a Vertex AI service account JSON key) or openai (sk-…)
api_keyStringYesThe 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

MethodParametersReturns
setVoiceCallMcpServerlabel, url, auth (object), optional audience, allowed_tools, requested_level, timeout_ms, tools (classification overrides), declaration, auto_repinThe registration result (status, job_id). If the recipient changes, 400 MCP_CHANGE_NEEDS_CONFIRMATION, then confirm in @BotFather
refreshVoiceCallMcpServerlabelCheck again (refreshing: true, job_id)
deleteVoiceCallMcpServerlabelTrue — works even with MCP off
getVoiceCallMcpBindingNoneiss, sub (bot:<id>), issuer_document, jwks_uri, audiences
createVoiceCallMcpClientKeylabelThe 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

FieldTypeDescription
idStringCall id (an unsigned decimal number)
directionStringincoming (a call to the bot) or outgoing (a call the bot made). Always there
eventStringstarted, ended, missed, refused; for a call the bot made, started, ended, not_answered, failed
from OptionalUserCalls to the bot only — the caller: id, is_bot, first_name, last_name, language_code. No username or phone number
to OptionalObjectCalls the bot made only — {"id": …}, the callee's id alone
purpose OptionalStringCalls the bot made only — the request's purpose
dateIntegerWhen 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 OptionalIntegerThis call's maximum length (seconds) — started, ended
start_date, duration OptionalIntegerWhen it came on and how many seconds it was on (server clock) — ended
end_reason OptionalStringcaller (callee for a call the bot made), bot, max_length, failed, operator, suspended — ended, missed
refused_reason OptionalStringbusy, budget, unavailable — refused
transcript OptionalObject{"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

FieldTypeDescription
idStringThe id to give to answerVoiceCallToolQuery
voice_call_idStringCall id
fromUserThe caller (same fields as in VoiceCall)
nameStringTool name
argumentsStringThe arguments, as the text of a JSON object (checked by the server)
budget_msIntegerMilliseconds from receipt until the answer must reach the server

VoiceCallTranscript

FieldTypeDescription
voice_call_idStringCall id
seqIntegerTurn number
roleStringcaller or bot
textStringThe text written down by the AI. If it contains a card or resident registration number, the digits become *
is_finalBooleanAlways true
offset_ms, dateIntegerMilliseconds 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>"). host is the host of the URL you gave. Check the 300 seconds, each nonce once, byte-by-byte comparison, and that start.call_id equals call (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 after start, before anything else) · audio (after live) · 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_message and callback_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_mode and entities are 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 no user_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_connections is 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 …) returns 404 Not Found: method not found.