Docs
Bot API
Bots are ordinary Etoki identities hosted by the bot relay. Each one gets a token and a small HTTP API: send to a subscriber, broadcast to all, or long-poll for replies.
Which relay
Every bot lives on a bot relay. You can use the one we run, at https://bot.etoki.chat:8443, or run your own — the API is identical, only the host in the examples below changes.
Create a bot
Bots are created by chatting with the relay’s manager contact, the same way Telegram’s BotFather works:
1. In the app, add the bot manager contact: @bot
2. Send: /newbot
3. Reply with a display name, then a username ending in _bot
4. The manager replies with your API token — store it like a passwordEndpoints
| Method | Path | Description |
|---|---|---|
| GET | /v1/healthz | Liveness. No token required. |
| GET | /v1/bot/<token>/getme | Username, display name, whether a webhook is set. |
| GET | /v1/bot/<token>/conns | Connection IDs of every subscriber. |
| GET | /v1/bot/<token>/updates | Long-poll for inbound messages. Params: offset, timeout_secs. |
| POST | /v1/bot/<token>/send | Send to one or many subscribers. Max 500 per call. |
| POST | /v1/bot/<token>/broadcast | Send to every subscriber. No fan-out limit. |
Sending
Send to a single subscriber with conn_id:
curl -X POST \
"https://bot.example.com:8443/v1/bot/<token>/send" \
-d '{"conn_id":4,"text":"hello from the API"}'Or to several at once with conn_ids. Each recipient gets the message over its own end-to-end encrypted session, and the response reports success per connection:
curl -X POST \
"https://bot.example.com:8443/v1/bot/<token>/send" \
-d '{"conn_ids":[4,7,11],"text":"hello everyone"}'Use /broadcast to reach every subscriber without listing them. The recipient list is built server-side, so it is exempt from the 500-recipient cap that applies to /send:
curl -X POST \
"https://bot.example.com:8443/v1/bot/<token>/broadcast" \
-d '{"text":"announcement to all"}'Priority
Both /send and /broadcast accept an optional priority. It controls how loudly the message arrives on the recipient’s phone:
| Value | Alert level | On the device |
|---|---|---|
| normal | 0 | An ordinary message. Respects mute and Do Not Disturb. |
| high | 1 | Loud one-shot alert on the alarm channel. Pierces ringer mute and per-chat mute. |
| critical | 2 | Full-screen alert plus a repeating alarm until opened or dismissed (60s cap). |
# high — loud one-shot alert, pierces ringer mute
curl -X POST \
"https://bot.example.com:8443/v1/bot/<token>/broadcast" \
-d '{"text":"EMERGENCY TEST","priority":"high"}'
# critical — full-screen repeating alarm until acknowledged
curl -X POST \
"https://bot.example.com:8443/v1/bot/<token>/broadcast" \
-d '{"text":"site down","priority":"critical"}'Omitting priority means normal. Any other value is rejected with 400. Recipients keep control: alerts only pierce mute for conversations where the user has left the per-chat “allow alerts” toggle on.
Rate limits
There are two separate limits, and both return 429:
- General — 30 requests per second per bot, sustained and burst, across every endpoint. Exceeding it returns
{"error":"rate limited"}. Normal sends are not budgeted, but they are not unlimited either. - Emergency — 5 per hour per bot, counting only calls with
priorityofhighorcritical. Exceeding it returns{"error":"emergency rate limited"}.
Receiving
Long-poll /updates for inbound messages. Pass the last update ID you processed as offset to acknowledge everything before it. timeout_secs is capped at 25:
# Long-poll for inbound messages (timeout capped at 25s)
curl "https://bot.example.com:8443/v1/bot/<token>/updates?offset=0&timeout_secs=25"
# Who am I, and who is subscribed?
curl "https://bot.example.com:8443/v1/bot/<token>/getme"
curl "https://bot.example.com:8443/v1/bot/<token>/conns"Alternatively, register a webhook and the relay will POST each update to your HTTPS endpoint as JSON. Every delivery carries an x-etoki-signature header containing a keyed BLAKE3 hash of the exact request body, computed with a per-bot secret. Recompute it and compare before trusting the payload — otherwise anyone who learns your endpoint URL can forge updates.
Status codes
| Code | Meaning |
|---|---|
| 200 | Accepted. Per-connection results are in the body. |
| 400 | Bad JSON, missing text, no recipients, >500 recipients, or an invalid priority value. |
| 401 | Unknown or malformed token. |
| 404 | Unknown endpoint or bot. |
| 405 | Wrong HTTP method for that endpoint. |
| 429 | Rate limited — see below for which of the two limits fired. |
Running the relay
The bot relay is a separate binary. It registers its manager username in the directory and serves the API on two listeners:
etoki-botrelay \
--state /var/lib/etoki-botrelay \
--router etoki://<fp>@YOUR_SERVER_IP:5223 \
--directory etoki://<fp>@YOUR_SERVER_IP:5663 \
--http 127.0.0.1:5880 \
--https 0.0.0.0:8443 --tls-sni bot.example.com \
--tls-cert /etc/etoki/tls/bot.example.com/fullchain.pem \
--tls-key /etc/etoki/tls/bot.example.com/privkey.pem--http 127.0.0.1:5880— plain HTTP, loopback only. Never expose this.--https 0.0.0.0:8443with--tls-cert/--tls-key— the public listener, serving an ordinary Let’s Encrypt certificate. That is why none of the examples above need-k.
The installer issues that certificate with certbot and re-installs it on every renewal; see the VPS guide. Leave the flags out and the relay falls back to a self-signed certificate, which then forces every caller to disable verification — workable for a laptop test, not for anything else.
:443 is the TLS-camouflage gateway, which speaks a different protocol: an HTTP upgrade on a secret bridge path, and a decoy page for everything else. It has nothing to route a REST call to.Trust model
The relay hosts the bot side of every conversation, so it can read bot conversations — exactly like any bot platform. It never sees ordinary user-to-user traffic, which stays end-to-end encrypted between devices. Self-hosting the relay removes even that exposure.
