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.
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 -sk -X POST \
"https://YOUR_SERVER_IP:5881/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 -sk -X POST \
"https://YOUR_SERVER_IP:5881/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 -sk -X POST \
"https://YOUR_SERVER_IP:5881/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 -sk -X POST \
"https://YOUR_SERVER_IP:5881/v1/bot/<token>/broadcast" \
-d '{"text":"EMERGENCY TEST","priority":"high"}'
# critical — full-screen repeating alarm until acknowledged
curl -sk -X POST \
"https://YOUR_SERVER_IP:5881/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 -sk "https://YOUR_SERVER_IP:5881/v1/bot/<token>/updates?offset=0&timeout_secs=25"
# Who am I, and who is subscribed?
curl -sk "https://YOUR_SERVER_IP:5881/v1/bot/<token>/getme"
curl -sk "https://YOUR_SERVER_IP:5881/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:5881 --tls-sni relay.example.com--http 127.0.0.1:5880— plain HTTP, loopback only. Never expose this.--https 0.0.0.0:5881— TLS with a self-signed certificate. That is why the examples above usecurl -sk.
-k skips verification entirely — fine for testing, not for production. Either pin the certificate in your client or tunnel the API over SSH or a real TLS proxy. The TLS-camouflage gateway cannot front this endpoint: its routing is protocol-specific and does not handle plain HTTP.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.
