Success is { ok: true, data, warnings }. Failure is
{ ok: false, error: { code, message, hint } }. Error codes are a closed
set: BAD_INPUT, UNAUTHORIZED, NOT_FOUND,
CONFLICT, NOT_CONNECTED, RATE_LIMITED,
PLAN_LIMIT, TELEGRAM_ERROR, INTERNAL.
Two kinds of key. rg_admin_… is the platform key: it creates tenants and
reaches everything. rg_live_… is tenant-scoped: it can only ever touch
its own tenant's data, and that is enforced on every route below.
Two kinds of key. rg_admin_… is the platform key and can reach every tenant.
rg_live_… is tenant-scoped and can only ever touch its own tenant. Send it
as Authorization: Bearer <key>.
curl http://localhost:8787/v1/tenants \
-H "Authorization: Bearer $RELAY_ADMIN_KEY"
| Method | Path | Who | What it does |
|---|---|---|---|
| POST | /v1/tenants | admin | Create a tenant, returns its tenant key |
| GET | /v1/tenants | admin | List tenants |
| GET | /v1/tenants/:id | admin, tenant | One tenant |
| POST | /v1/tenants/:id/connect-links | admin, tenant | Mint a connect link |
| POST | /v1/connect/:token/complete | public | Customer picks a bot name |
| POST | /v1/messages | tenant | Send a message |
| GET | /v1/messages | tenant | Message log |
| GET | /v1/tenants/:id/messages | admin, tenant | Message log for a tenant |
| POST | /v1/webhooks | tenant | Register an event webhook |
| GET | /v1/webhooks | tenant | List webhooks |
| DELETE | /v1/webhooks/:id | tenant | Remove a webhook |
| GET | /v1/usage | tenant | Messages used this period |
| POST | /v1/telegram/webhook | Telegram | Updates from Telegram |
| GET | /healthz | public | Liveness |
curl -X POST http://localhost:8787/v1/tenants \
-H "Authorization: Bearer $RELAY_ADMIN_KEY" \
-H "content-type: application/json" \
-d '{"name":"Sara'"'"'s Phone Shop"}'
The response contains the tenant's rg_live_… key. It is shown once and
stored only as a hash.
curl -X POST http://localhost:8787/v1/messages \
-H "Authorization: Bearer $TENANT_KEY" \
-H "content-type: application/json" \
-d '{
"chat_id": 15551234567,
"text": "Your order #412 ships tomorrow.",
"parse_mode": "HTML"
}'
chat_id is a numeric user id or a @username. Rate limits are
enforced for you: about 1 message/second per chat, 20/minute per group, 30/second overall.
Register a URL and we POST every event to it. The body is signed:
x-relay-signature: sha256=…, computed with the secret we returned at
registration. Verify it with an HMAC-SHA256 over the raw body.
curl -X POST http://localhost:8787/v1/webhooks \
-H "Authorization: Bearer $TENANT_KEY" \
-H "content-type: application/json" \
-d '{
"url": "https://example.com/hooks/telegram",
"events": ["message.sent", "message.received"]
}'
The same operations are exposed to AI agents over MCP. Start it with
node dist/mcp.js and point your client at it.
| Tool | What it does |
|---|---|
relay_list_tenants | List tenants and their connection state |
relay_create_tenant | Create a tenant and its key |
relay_create_connect_link | Mint a connect link for a tenant |
relay_send_message | Send a message as a tenant's bot |
relay_list_messages | Read a tenant's message log |
relay_register_webhook | Register an event webhook |
relay_get_usage | Messages used this period |
| Variable | Default | Meaning |
|---|---|---|
RELAY_DATA_DIR | ~/.relay-gateway | SQLite file and dev key |
RELAY_HOST | 127.0.0.1 | HTTP bind address |
RELAY_PORT | 8787 | HTTP port |
RELAY_PUBLIC_URL | http://host:port | Base URL for connect links |
RELAY_ADMIN_KEY | generated | The platform key |
RELAY_MANAGER_BOT_TOKEN | — | The manager bot that holds managed-bot tokens |
RELAY_MANAGER_BOT_USERNAME | — | Its @username, for the deep link |
RELAY_TELEGRAM_WEBHOOK_SECRET | generated | Header Telegram sends us |
RELAY_ENCRYPTION_KEY | derived | 32 bytes, hex or base64 |
RELAY_DEMO | 0 | Use the fake Telegram client |
RELAY_PLAN | selfhost | selfhost or hosted |