Hidesay Bot API (1.0.0)

Download OpenAPI specification:

The HTTP API a bot uses to talk to a Hidesay server.

This is the API a bot programme speaks. It lives under /bot/v1 on the same server the app uses.

A bot is an account that a programme controls. A person makes it from the @bot console inside the app, gets a token, and from then on the programme does everything with that token. A bot has no session, no wallet, no calls and no devices: it has this API and nothing else.

Envelope. Every answer is JSON. A success is {"ok": true, "result": ...}. A failure is {"ok": false, "error": {"status", "code", "message"}} with the same status on the HTTP line. The code is stable and is what your programme should branch on; the message is for a person reading a log.

Two kinds of body. A chat is either readable or end-to-end encrypted, and the server tells you which (mode on a chat, expected on an error). In a readable chat you send and receive text. In an encrypted chat you send and receive armored: an ASCII-armoured OpenPGP message. A bot that has published a public key (PUT /key) is protected: its private chats become encrypted and people's phones seal every message to it. A bot without a key is open: its private chats are readable, the server passes the words along, and the app tells people so before they write. Groups follow the group: a group with open history is readable, any other group is encrypted.

Updates. What happens in chats reaches the bot as updates, each with a growing updateId. Fetch them with GET /updates (long polling) or have them posted to a webhook (POST /webhook). Either way delivery is in order and at least once: keep the last updateId you handled and ignore anything you have already seen. Updates older than 72 hours are dropped, and so are the oldest when more than 10 000 pile up unread.

Limits. 600 API calls a minute per bot and 1 200 a minute per address. Sending is 30 messages a second overall, 20 a minute into any one group and 1 a second into any one private chat; past that you get 429 flood_wait with retryAfter in seconds and a Retry-After header. Honour it: the counter does not move while you are waiting. Resolving a handle has a window of its own, 60 a minute, where a name nobody holds counts as five.

Privacy, in one paragraph. A bot only ever receives what was addressed to it. In a private chat that is everything the person writes. In a group it is a command (/start, /start@yourbot), a mention (@yourbot, by any of the bot's handles) or a reply to one of the bot's messages, unless the owner switched privacy mode off, in which case the whole room is delivered and every member can see that in the app. In an encrypted group the sender's phone applies that rule and seals the message to the bot as well; the server cannot read the words and does not try.

What a bot cannot do, on purpose. Send files (it speaks in words and buttons; in a readable chat it does see pictures people sent), secret messages, payments, wallets, calls, devices, push or phone numbers (those are a person's), join a chat by itself (an administrator adds it), read history from before it arrived, inline mode, web apps, games, login buttons, reply keyboards, scheduled sends, or edit other people's messages. It cannot search for people either: a handle somebody typed resolves to an id, because that handle is already public, and there is no listing and nothing by phone number. Each of these is left out by decision: it would either give a program power over people it has no business having, or make the server read what it promises not to.

Me

The bot itself.

Who the bot is

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.

Authorizations:
botToken

Responses

Response samples

Content type
application/json
{
  • "ok": true,
  • "result": {
    }
}

Updates

Getting told what happens: long polling or a webhook.

Fetch updates (long polling)

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.

Authorizations:
botToken
query Parameters
offset
integer >= 0

Confirm everything below this id. Highest handled updateId + 1.

limit
integer [ 1 .. 100 ]

At most this many, 1 to 100. Default 100.

timeout
integer [ 0 .. 50 ]

Seconds to wait for something to arrive, 0 to 50. Default 0.

Responses

Response samples

Content type
application/json
{
  • "ok": true,
  • "result": [
    ]
}

The webhook, if any, and how it is doing

Works while paused.

Authorizations:
botToken

Responses

Response samples

Content type
application/json
{
  • "ok": true,
  • "result": {
    }
}

Set a webhook

From now on every update is POSTed as JSON to url, one update per request, in order, with the header User-Agent: Hidesay-Bot-Api/1. If you give a secret, every delivery carries it in the header X-Hidesay-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.

Authorizations:
botToken
Request Body schema: application/json
required
url
required
string <= 1024 characters

Absolute https URL.

secret
string^[A-Za-z0-9_-]{1,256}$

Sent back on every delivery in X-Hidesay-Bot-Api-Secret-Token.

Responses

Request samples

Content type
application/json
{}

Response samples

Content type
application/json
{
  • "ok": true,
  • "result": {
    }
}

Remove the webhook

Back to polling. Updates that queued while the webhook was set are still there. Works while paused.

Authorizations:
botToken

Responses

Response samples

Content type
application/json
{
  • "ok": true,
  • "result": {
    }
}

Messages

Sending, editing and deleting messages, with or without buttons.

Send a message

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.

Authorizations:
botToken
Request Body schema: application/json
required
chatId
required
integer
text
string [ 1 .. 4096 ] characters

Readable chats. No control characters apart from newline and tab.

armored
string <= 131072 characters

Encrypted chats. An ASCII-armoured OpenPGP message.

replyTo
integer or null
object (InlineKeyboard)

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.

silent
boolean

Channels only: no notification.

sendId
string <uuid>

Your id for this send, so a retry writes nothing twice.

Responses

Request samples

Content type
application/json
Example
{
  • "chatId": 7781,
  • "text": "Rome, next three days: sun, sun, rain. Want the hourly view?",
  • "markup": {
    },
  • "sendId": "7a1a0f1c-2a86-4b94-9c38-0f1e7c3a9a10"
}

Response samples

Content type
application/json
{
  • "ok": true,
  • "result": {
    }
}

Edit one of the bot's messages

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.

Authorizations:
botToken
path Parameters
messageId
required
integer
Request Body schema: application/json
required
chatId
required
integer
text
string <= 4096 characters
armored
string <= 131072 characters
InlineKeyboard (object) or null

Omit to keep the buttons, null to remove them.

Responses

Request samples

Content type
application/json
{
  • "chatId": 7781,
  • "text": "Rome: hourly view sent above.",
  • "markup": null
}

Response samples

Content type
application/json
{
  • "ok": true,
  • "result": {
    }
}

Delete a message

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.

Authorizations:
botToken
path Parameters
messageId
required
integer
query Parameters
chatId
integer

The chat, when not sent in the body.

Request Body schema: application/json
optional
chatId
integer

Responses

Request samples

Content type
application/json
{
  • "chatId": 0
}

Response samples

Content type
application/json
{
  • "ok": true,
  • "result": {
    }
}

Buttons

Answering a press on an inline keyboard.

Answer a button press

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.

Authorizations:
botToken
path Parameters
callbackId
required
string
Request Body schema: application/json
optional
text
string <= 200 characters
alert
boolean
Default: false
url
string <= 1024 characters

Absolute http(s) URL without credentials.

Responses

Request samples

Content type
application/json
{
  • "text": "Hourly view coming up",
  • "alert": false
}

Response samples

Content type
application/json
{
  • "ok": true,
  • "result": {
    }
}

Chats

The chats the bot is in, their members and their keys.

A chat the bot is in

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.

Authorizations:
botToken
path Parameters
chatId
required
integer

A chat the bot is in. Any other id answers chat_not_found.

Responses

Response samples

Content type
application/json
{
  • "ok": true,
  • "result": {
    }
}

The keys to seal a message to

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.

Authorizations:
botToken
path Parameters
chatId
required
integer

A chat the bot is in. Any other id answers chat_not_found.

Responses

Response samples

Content type
application/json
{
  • "ok": true,
  • "result": {
    }
}

The roster of a group or channel

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.

Authorizations:
botToken
path Parameters
chatId
required
integer

A chat the bot is in. Any other id answers chat_not_found.

Responses

Response samples

Content type
application/json
{
  • "ok": true,
  • "result": {
    }
}

One member's seat

Authorizations:
botToken
path Parameters
chatId
required
integer

A chat the bot is in. Any other id answers chat_not_found.

userId
required
integer

Responses

Response samples

Content type
application/json
{
  • "ok": true,
  • "result": {
    }
}

Leave a group or channel

The room is told, the way it would be about a person. A bot cannot leave a private chat; the person can block it.

Authorizations:
botToken
path Parameters
chatId
required
integer

A chat the bot is in. Any other id answers chat_not_found.

Responses

Response samples

Content type
application/json
{
  • "ok": true,
  • "result": {
    }
}

Show "typing" for a few seconds

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.

Authorizations:
botToken
path Parameters
chatId
required
integer

A chat the bot is in. Any other id answers chat_not_found.

Request Body schema: application/json
optional
typing
boolean
Default: true

Responses

Request samples

Content type
application/json
{
  • "typing": true
}

Response samples

Content type
application/json
{
  • "ok": true,
  • "result": {
    }
}

One person's public key

Only for somebody the bot shares a chat with; anybody else is user_not_found, whether or not they exist.

Authorizations:
botToken
path Parameters
userId
required
integer

Responses

Response samples

Content type
application/json
{
  • "ok": true,
  • "result": {
    }
}

Names

Turning a public handle into an id.

A public handle, turned into an id

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 (hidesay.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.

Authorizations:
botToken
path Parameters
username
required
string^[A-Za-z0-9_]{3,32}$

A public handle, without the @. 3-32 characters: letters, digits and underscore. Case does not matter.

Responses

Response samples

Content type
application/json
{
  • "ok": true,
  • "result": {
    }
}

Commands

The list the app shows when somebody types a slash.

The command lists

Every scope with its commands, or one scope's list when scope is given.

Authorizations:
botToken
query Parameters
scope
string (Scope) ^(default|private|groups|groupAdmins|chat:\d{...

default, private, groups, groupAdmins, or chat:<id> for one chat the bot is in. Missing means default.

Responses

Response samples

Content type
application/json
{
  • "ok": true,
  • "result": [
    ]
}

Replace the commands of one scope

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.

Authorizations:
botToken
Request Body schema: application/json
required
scope
string (Scope) ^(default|private|groups|groupAdmins|chat:\d{...

default, private, groups, groupAdmins, or chat:<id> for one chat the bot is in. Missing means default.

required
Array of objects (Command) <= 100 items

Responses

Request samples

Content type
application/json
{
  • "scope": "default",
  • "commands": [
    ]
}

Response samples

Content type
application/json
{
  • "ok": true,
  • "result": {
    }
}

Empty one scope

Authorizations:
botToken
query Parameters
scope
string (Scope) ^(default|private|groups|groupAdmins|chat:\d{...

default, private, groups, groupAdmins, or chat:<id> for one chat the bot is in. Missing means default.

Responses

Response samples

Content type
application/json
{
  • "ok": true,
  • "result": {
    }
}

Key

Publishing the bot's OpenPGP key, which makes it protected.

Publish the bot's OpenPGP public key

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.

Authorizations:
botToken
Request Body schema: application/json
required
publicKey
required
string <= 65536 characters

ASCII-armoured OpenPGP public key block.

fingerprint
required
string^[0-9A-Fa-f]{40}$|^[0-9A-Fa-f]{64}$

The key's fingerprint, 40 (v4) or 64 (v6) hex characters.

Responses

Request samples

Content type
application/json
{
  • "publicKey": "string",
  • "fingerprint": "string"
}

Response samples

Content type
application/json
{
  • "ok": true,
  • "result": {
    }
}

Token

Burning the token this bot is using.

Burn this token

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.

Authorizations:
botToken

Responses

Response samples

Content type
application/json
{
  • "ok": true,
  • "result": {
    }
}

An update, delivered to your webhook Webhook

One update per request, in order. Answer any 2xx within 10 seconds. The header X-Hidesay-Bot-Api-Secret-Token carries the secret you set, if any; User-Agent is Hidesay-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.

Authorizations:
botToken
Request Body schema: application/json
required
One of
updateId
required
integer

Grows by one per update, per bot.

kind
required
string
Enum: "message" "edited_message" "deleted_messages" "callback_query" "my_chat_member"
required
object (Message)

A message a person wrote, as a bot receives it. Either armored (encrypted chat: decrypt it with the bot's private key) or text with media (readable chat).

object (ChatRef)
messageIds
Array of integers

deleted_messages: what went.

object (CallbackQuery)
object (UserRef)

As much as a bot may know about a person: id, handle, name. Never a phone number.

date
integer
object (Seat)

A membership, the bot's own or somebody's. admin carries the rights it holds; member and restricted carry the bitmask of what it may do; left and kicked carry nothing.

object (Seat)

A membership, the bot's own or somebody's. admin carries the rights it holds; member and restricted carry the bitmask of what it may do; left and kicked carry nothing.

Responses

Request samples

Content type
application/json
Example
{
  • "updateId": 0,
  • "kind": "message",
  • "message": {
    },
  • "chat": {
    },
  • "messageIds": [
    ],
  • "callback": {
    },
  • "from": {
    },
  • "date": 0,
  • "old": {
    },
  • "new": {
    }
}