Every operation¶
The 22 operations of PGP Chat Bot API 1.0.0, as the server’s OpenAPI document describes them. All paths are under {apiUrl}/bot/v1. Every call needs the header Authorization: Bearer <token>, and every answer is the envelope {"ok": true, "result": ...} or {"ok": false, "error": {...}}.
The SDK column links to the method that runs the operation. It has the same name on AsyncClient, Client, Bot and SyncBot.
GET /me¶
Who the bot is
operationId |
|
Tag |
Me |
SDK |
The bot’s own card as the server holds it, plus the limits this server enforces. This works while the bot is paused, so a programme can find out why nothing else does.
Answer |
Meaning |
|---|---|
|
The bot. |
|
|
|
|
GET /updates¶
Fetch updates (long polling)
operationId |
|
Tag |
Updates |
SDK |
Returns the updates waiting for the bot, oldest first, up to limit. With timeout the call hangs until something arrives or the clock runs out, and answers an empty list on the clock.
offset confirms: every update with an updateId below it is deleted and never shown again. Send the highest updateId you handled plus one. Without offset you get everything still in the queue, which is what a bot asks on its very first call.
Only one poll per bot at a time. A second call ends the first one at once with an empty answer, so two copies of the same programme polling one bot will starve each other: run one.
Not available while a webhook is set: delete the webhook first.
Parameter |
In |
Type |
Required |
Rules |
Description |
|---|---|---|---|---|---|
|
query |
integer |
no |
min |
Confirm everything below this id. Highest handled |
|
query |
integer |
no |
min |
At most this many, 1 to 100. Default 100. |
|
query |
integer |
no |
min |
Seconds to wait for something to arrive, 0 to 50. Default 0. |
Answer |
Meaning |
|---|---|
|
Updates, oldest first. Empty when nothing arrived before the timeout. |
|
|
|
|
|
|
|
A webhook is set. Delete it before polling. |
|
|
GET /webhook¶
The webhook, if any, and how it is doing
operationId |
|
Tag |
Updates |
SDK |
Works while paused.
Answer |
Meaning |
|---|---|
|
Webhook state. |
|
|
|
|
POST /webhook¶
Set a webhook
operationId |
|
Tag |
Updates |
SDK |
From now on every update is POSTed as JSON to url, one update per request, in order, with the header User-Agent: PGPChat-Bot-Api/1. If you give a secret, every delivery carries it in the header X-PGPChat-Bot-Api-Secret-Token: check it and refuse anything without it.
Answer with any 2xx within 10 seconds and the update is considered delivered. Anything else, a timeout or a redirect counts as a failure: the server backs off (one second, then doubling, up to an hour) and tries that same update again, so delivery is at least once and never out of order. After ten failures in a row the owner is told in the app. Updates still queue while the webhook is failing and are kept for 72 hours.
The URL must be https, carry no credentials or fragment, and resolve to a public address. Setting a webhook ends any poll in progress; GET /updates refuses while one is set.
Request body (JSON, required):
Field |
Type |
Required |
Rules |
Description |
|---|---|---|---|---|
|
string |
yes |
max length |
Absolute https URL. |
|
string |
no |
pattern |
Sent back on every delivery in |
Answer |
Meaning |
|---|---|
|
Set. |
|
The URL is not acceptable ( |
|
|
|
|
|
|
DELETE /webhook¶
Remove the webhook
operationId |
|
Tag |
Updates |
SDK |
Back to polling. Updates that queued while the webhook was set are still there. Works while paused.
Answer |
Meaning |
|---|---|
|
Removed. |
|
|
|
|
PUT /key¶
Publish the bot’s OpenPGP public key
operationId |
|
Tag |
Key |
SDK |
Turns the bot protected: from now on its private chats are end-to-end encrypted, people’s phones seal every message to this key, and the bot must send armored into those chats. Calling it again with a new key is a rotation; every phone learns the new fingerprint at once.
There is no way to remove the key from the API on purpose: downgrading a bot from protected to open is a decision for its owner, in the console.
Request body (JSON, required):
Field |
Type |
Required |
Rules |
Description |
|---|---|---|---|---|
|
string |
yes |
max length |
ASCII-armoured OpenPGP public key block. |
|
string |
yes |
pattern |
The key’s fingerprint, 40 (v4) or 64 (v6) hex characters. |
Answer |
Meaning |
|---|---|
|
Published. |
|
|
|
|
|
|
|
|
GET /commands¶
The command lists
operationId |
|
Tag |
Commands |
SDK |
Every scope with its commands, or one scope’s list when scope is given.
Parameter |
In |
Type |
Required |
Rules |
Description |
|---|---|---|---|---|---|
|
query |
string |
no |
pattern |
Answer |
Meaning |
|---|---|
|
All scopes (no |
|
|
|
|
|
|
|
|
PUT /commands¶
Replace the commands of one scope
operationId |
|
Tag |
Commands |
SDK |
The whole list of that scope, replaced. Up to 100 commands; a command is 1 to 32 lowercase letters, digits and underscores (a leading slash is dropped), its description 1 to 256 characters. Duplicates are folded to the first.
Scopes resolve the way Telegram’s do, narrowest first: in a private chat private then default; in a group chat:<id>, then groupAdmins for administrators, then groups, then default. An empty scope does not hide a wider one. The owner can edit the same lists in the console; whichever wrote last wins.
Request body (JSON, required):
Field |
Type |
Required |
Rules |
Description |
|---|---|---|---|---|
|
string |
no |
pattern |
|
|
array of object |
yes |
Answer |
Meaning |
|---|---|
|
Saved. |
|
|
|
|
|
|
|
A |
|
|
DELETE /commands¶
Empty one scope
operationId |
|
Tag |
Commands |
SDK |
Parameter |
In |
Type |
Required |
Rules |
Description |
|---|---|---|---|---|---|
|
query |
string |
no |
pattern |
Answer |
Meaning |
|---|---|
|
Emptied. |
|
|
|
|
|
|
|
|
POST /messages¶
Send a message
operationId |
|
Tag |
Messages |
SDK |
Into a chat the bot is in. Send text into a readable chat and armored into an encrypted one; the wrong kind is refused with wrong_body_kind and expected says which. A markup adds rows of buttons under the message. replyTo quotes a message of the same chat (a wrong id is silently dropped). silent only matters in a channel: the post goes out without a notification.
sendId makes the call safe to repeat: a UUID of your choosing, and a second call with the same one hands back the message already written instead of writing it twice. Use it whenever you retry after a timeout.
In an encrypted chat, seal the message to every key GET /chats/{id}/keys returns and to the bot’s own key, sign it with the bot’s key, and send the armoured result.
Request body (JSON, required):
Field |
Type |
Required |
Rules |
Description |
|---|---|---|---|---|
|
integer |
yes |
||
|
string |
no |
min length |
Readable chats. No control characters apart from newline and tab. |
|
string |
no |
max length |
Encrypted chats. An ASCII-armoured OpenPGP message. |
|
integer or null |
no |
||
|
object |
no |
Rows of buttons under a message. Up to 20 rows, 8 buttons a row, 100 in all, 8 KiB of JSON. The buttons are not encrypted: the server has to know which one was pressed. |
|
|
boolean |
no |
Channels only: no notification. |
|
|
string |
no |
format |
Your id for this send, so a retry writes nothing twice. |
Answer |
Meaning |
|---|---|
|
Sent. A repeated |
|
|
|
|
|
|
|
|
|
|
|
|
PATCH /messages/{messageId}¶
Edit one of the bot’s messages
operationId |
|
Tag |
Messages |
SDK |
New words, new buttons, or both. Only the bot’s own ordinary messages, at any age: a scoreboard that changes for a week is the normal use. Service notices the bot caused (“left the group”) are not its to edit. Leave markup out to keep the buttons, send markup: null to remove them. People in the chat see the edit live. Edits are counted against the same numbers as sends, in a window of their own: one a second in a private chat, twenty a minute in a group, and the thirty a second overall is shared.
Parameter |
In |
Type |
Required |
Rules |
Description |
|---|---|---|---|---|---|
|
path |
integer |
yes |
Request body (JSON, required):
Field |
Type |
Required |
Rules |
Description |
|---|---|---|---|---|
|
integer |
yes |
||
|
string |
no |
max length |
|
|
string |
no |
max length |
|
|
object or null |
no |
Omit to keep the buttons, |
Answer |
Meaning |
|---|---|
|
Edited. |
|
|
|
|
|
|
|
|
|
|
DELETE /messages/{messageId}¶
Delete a message
operationId |
|
Tag |
Messages |
SDK |
The bot’s own messages anywhere; anybody’s in a group or channel where the bot holds the delete messages right. Secret messages and service notices cannot be deleted by a bot. chatId goes in the JSON body or in the query.
Parameter |
In |
Type |
Required |
Rules |
Description |
|---|---|---|---|---|---|
|
path |
integer |
yes |
||
|
query |
integer |
no |
The chat, when not sent in the body. |
Request body (JSON):
Field |
Type |
Required |
Rules |
Description |
|---|---|---|---|---|
|
integer |
no |
Answer |
Meaning |
|---|---|
|
Deleted for everyone, for good. |
|
|
|
|
|
|
|
|
POST /callbacks/{callbackId}/answer¶
Answer a button press
operationId |
|
Tag |
Buttons |
SDK |
When somebody presses a button with callbackData, the bot gets a callback_query update and the button spins on their phone. Answer within 60 seconds: with text a short line flashes over the chat; with alert: true the text is shown in a box they have to dismiss; with url their phone shows the address and offers to open it. An empty answer just stops the spinner. The message itself changes only if you edit it (PATCH /messages/{id}).
Each press is answered once. A second press on the same message while the first is open is the same press.
Parameter |
In |
Type |
Required |
Rules |
Description |
|---|---|---|---|---|---|
|
path |
string |
yes |
Request body (JSON):
Field |
Type |
Required |
Rules |
Description |
|---|---|---|---|---|
|
string |
no |
max length |
|
|
boolean |
no |
default |
|
|
string |
no |
max length |
Absolute http(s) URL without credentials. |
Answer |
Meaning |
|---|---|
|
Delivered to the phone that pressed. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
GET /chats/{chatId}¶
A chat the bot is in
operationId |
|
Tag |
Chats |
SDK |
Its kind, its mode (which body to send), and for a group the bot’s own seat. A private chat carries the other person and whether they blocked the bot.
Parameter |
In |
Type |
Required |
Rules |
Description |
|---|---|---|---|---|---|
|
path |
integer |
yes |
A chat the bot is in. Any other id answers |
Answer |
Meaning |
|---|---|
|
The chat. |
|
|
|
|
|
|
|
|
GET /chats/{chatId}/keys¶
The keys to seal a message to
operationId |
|
Tag |
Chats |
SDK |
For an encrypted chat: the public keys of everybody in it, ids and keys only, no names. Seal your message to all of them and to the bot’s own key. Other bots that only receive what is addressed to them are left out, as they are for the app. A readable chat has no keys to give and answers chat_is_open.
Parameter |
In |
Type |
Required |
Rules |
Description |
|---|---|---|---|---|---|
|
path |
integer |
yes |
A chat the bot is in. Any other id answers |
Answer |
Meaning |
|---|---|
|
Keys. |
|
|
|
|
|
|
|
|
|
|
GET /chats/{chatId}/members¶
The roster of a group or channel
operationId |
|
Tag |
Chats |
SDK |
Who is in it and with which seat, as a member in the bot’s seat would see it: where the roster is hidden a bot that is not an admin sees the admins only, and a channel that keeps its admins to itself leaves them out. Names and handles only, never phone numbers. memberCount counts the list as returned.
Parameter |
In |
Type |
Required |
Rules |
Description |
|---|---|---|---|---|---|
|
path |
integer |
yes |
A chat the bot is in. Any other id answers |
Answer |
Meaning |
|---|---|
|
Members. |
|
A private chat has no members. |
|
|
|
|
|
|
|
|
GET /chats/{chatId}/members/{userId}¶
One member’s seat
operationId |
|
Tag |
Chats |
SDK |
Parameter |
In |
Type |
Required |
Rules |
Description |
|---|---|---|---|---|---|
|
path |
integer |
yes |
A chat the bot is in. Any other id answers |
|
|
path |
integer |
yes |
Answer |
Meaning |
|---|---|
|
The member. |
|
A private chat has no members. |
|
|
|
|
|
|
|
|
POST /chats/{chatId}/leave¶
Leave a group or channel
operationId |
|
Tag |
Chats |
SDK |
The room is told, the way it would be about a person. A bot cannot leave a private chat; the person can block it.
Parameter |
In |
Type |
Required |
Rules |
Description |
|---|---|---|---|---|---|
|
path |
integer |
yes |
A chat the bot is in. Any other id answers |
Answer |
Meaning |
|---|---|
|
Left. |
|
A private chat cannot be left. |
|
|
|
|
|
|
|
|
POST /chats/{chatId}/typing¶
Show “typing” for a few seconds
operationId |
|
Tag |
Chats |
SDK |
The same indicator a person’s phone sends. It clears on its own after a few seconds or when a message lands; send typing: false to clear it early. Only where the bot may write: a restricted bot, or a channel subscriber, gets 403 forbidden. One goes out per chat every 3 seconds at most; calls in between answer sent: false and show nothing.
Parameter |
In |
Type |
Required |
Rules |
Description |
|---|---|---|---|---|---|
|
path |
integer |
yes |
A chat the bot is in. Any other id answers |
Request body (JSON):
Field |
Type |
Required |
Rules |
Description |
|---|---|---|---|---|
|
boolean |
no |
default |
Answer |
Meaning |
|---|---|
|
|
|
|
|
|
|
|
|
|
GET /resolve/{username}¶
A public handle, turned into an id
operationId |
|
Tag |
Names |
SDK |
For the handle a person types at your bot - “paga @anna”, “segui @notizie” - when no message has handed you that id. People, groups and channels share one pool of names, so a handle is held by exactly one of the three and kind says which.
The answer carries id at the top, and the same thing again under user or chat with what little goes with it. username is the name that account or chat actually goes by, which is not always the one you asked with: a person can answer to several handles.
It finds anybody with a public handle, which is no more than the short link already tells a stranger with a browser (pgpchat.me/anna). The id on its own grants nothing: sending still needs the bot to be in the chat, and a key still needs a chat in common. Out of reach: a chat with no public name (a private group is not in the pool at all), an account that has been closed, and the server’s own service account.
Harder limit than the rest of the API: 60 a minute, and a handle nobody holds counts as five, so about a dozen blind guesses spend the minute. Over it you get 429 flood_wait with retryAfter.
Parameter |
In |
Type |
Required |
Rules |
Description |
|---|---|---|---|---|---|
|
path |
string |
yes |
pattern |
A public handle, without the |
Answer |
Meaning |
|---|---|
|
What the handle turned out to be. |
|
|
|
|
|
|
|
|
|
|
GET /users/{userId}/key¶
One person’s public key
operationId |
|
Tag |
Chats |
SDK |
Only for somebody the bot shares a chat with; anybody else is user_not_found, whether or not they exist.
Parameter |
In |
Type |
Required |
Rules |
Description |
|---|---|---|---|---|---|
|
path |
integer |
yes |
Answer |
Meaning |
|---|---|
|
The key. |
|
|
|
|
|
|
|
|
POST /token/revoke¶
Burn this token
operationId |
|
Tag |
Token |
SDK |
For a programme that notices its token leaked. Nothing works again, this call included, until the owner issues a new token in the console; the owner is told at once. Works while paused.
Answer |
Meaning |
|---|---|
|
Revoked. |
|
|
|
|
Webhook: update¶
An update, delivered to your webhook
One update per request, in order. Answer any 2xx within 10 seconds. The header X-PGPChat-Bot-Api-Secret-Token carries the secret you set, if any; User-Agent is PGPChat-Bot-Api/1. Deliveries are at least once: a slow 2xx can be retried, so treat updateId as the thing that makes a delivery new.
The body is one Update. In the SDK: create_webhook_app().
Answer |
Meaning |
|---|---|
|
Any 2xx: delivered. Anything else, a timeout or a redirect: retried later. |