Getting started
Overview
Min (MinChat) developer documentation: REST API, realtime events and the Telegram-compatible Bot API.
Min is a messenger with web, desktop and Android clients. Everything the official clients do goes through the public HTTP API described here: a JSON REST API under /api, a Pusher-compatible WebSocket for realtime events, and a separate Telegram-compatible Bot API.
| Item | Value |
|---|---|
| REST base URL | https://minchatapp.com/api |
| Bot API | https://minchatapp.com/bot<token>/<method> |
| Realtime | Reverb (Pusher protocol) — settings from GET /api/broadcasting/client-config |
| Format | JSON (application/json); uploads — multipart/form-data |
| Auth | Authorization: Bearer <token> (Laravel Sanctum) |
| OpenAPI | /docs/openapi.json |
Quick start#
Sign in, list your chats and send a message:
# 1. Sign in (identifier-first wizard)
TICKET=$(curl -s -X POST https://minchatapp.com/api/auth/start \
-H 'Content-Type: application/json' -H 'Accept: application/json' \
-d '{"identifier":"anna@example.com"}' | jq -r .ticket)
TOKEN=$(curl -s -X POST https://minchatapp.com/api/auth/password \
-H 'Content-Type: application/json' -H 'Accept: application/json' \
-d "{\"ticket\":\"$TICKET\",\"password\":\"Secret123\"}" | jq -r .token)
# 2. List conversations
curl -s https://minchatapp.com/api/conversations \
-H "Authorization: Bearer $TOKEN" -H 'Accept: application/json'
# 3. Send a message
curl -s -X POST https://minchatapp.com/api/conversations/12/messages \
-H "Authorization: Bearer $TOKEN" -H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-d '{"content":"Hello from the API 👋"}'
const API = 'https://minchatapp.com/api';
async function call(path, { token, method = 'GET', body } = {}) {
const res = await fetch(API + path, {
method,
headers: {
Accept: 'application/json',
...(body ? { 'Content-Type': 'application/json' } : {}),
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const data = await res.json();
if (!res.ok) throw Object.assign(new Error(data.message), { code: data.code, status: res.status });
return data;
}
const { ticket } = await call('/auth/start', { method: 'POST', body: { identifier: 'anna@example.com' } });
const { token } = await call('/auth/password', { method: 'POST', body: { ticket, password: 'Secret123' } });
const chats = await call('/conversations', { token });
await call(`/conversations/${chats[0].id}/messages`, { token, method: 'POST', body: { content: 'Hello 👋' } });
import requests
API = "https://minchatapp.com/api"
s = requests.Session()
s.headers["Accept"] = "application/json"
ticket = s.post(f"{API}/auth/start", json={"identifier": "anna@example.com"}).json()["ticket"]
token = s.post(f"{API}/auth/password", json={"ticket": ticket, "password": "Secret123"}).json()["token"]
s.headers["Authorization"] = f"Bearer {token}"
chats = s.get(f"{API}/conversations").json()
msg = s.post(f"{API}/conversations/{chats[0]['id']}/messages", json={"content": "Hello 👋"}).json()
print(msg["id"])
Authentication#
All protected endpoints require a Sanctum personal access token in Authorization: Bearer <token>. Tokens are issued by the sign-in endpoints (password, one-time code, QR login, Google OAuth) and never expire on their own; every token is a session that the user can see and terminate in Settings → Devices (GET /api/user/sessions). Banned accounts receive 403 with code: "banned".
Optional client headers that label the session:
| Header | Meaning |
|---|---|
X-Client-Platform | web, desktop, android, … |
X-Client-Version | App version, e.g. 1.0.45 |
X-Device-Name | Human-readable device name |
Accept-Language | Sets the account locale on registration (ru / en) |
See Authentication for every sign-in flow.
Errors#
Errors use one envelope. code is a stable machine-readable identifier (domain.snake_case); error and message contain the same English text. Validation errors (422) add errors (field → messages) and error_codes (field → codes). The full catalog is on the Errors page.
{
"success": false,
"code": "validation.failed",
"error": "The given data was invalid.",
"message": "The given data was invalid.",
"errors": { "send_at": ["The time is too far in the future."] },
"error_codes": { "send_at": ["validation.failed"] }
}
| Status | Typical codes |
|---|---|
400 | Operation not applicable (e.g. conversation.private_not_editable) |
401 | common.unauthenticated, auth.invalid_credentials, auth.oauth_expired |
403 | common.forbidden, banned, conversation.not_member, conversation.banned |
404 | common.not_found, conversation.channel_not_found, bot.not_found |
409 | Conflicts (e.g. channel owner leaving: conversation.owner_must_transfer) |
419 | common.csrf — cookie session without Bearer and expired CSRF |
422 | validation.failed, message.empty, … |
429 | common.throttled, auth.too_many_attempts |
500 / 503 | common.server_error; 503 when a dependency (e.g. calls/LiveKit) is not configured |
Rate limits#
Limits are per route and per user (or per IP for public routes). Each endpoint in this reference shows its limit, e.g. 60/min. When exceeded the API answers 429 with code: "common.throttled" and the standard headers Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining. Routes without an explicit limit are still protected server-side; do not poll faster than once per second.
| Area | Limit |
|---|---|
| Sending messages | 60/min |
| Editing/deleting, reactions, polls | 60/min |
| Search (messages, users) | 30/min |
| Typing status, drafts, mentions read | 120/min |
| Sign-in | 10–20/min; /register 5 per 10 min |
| Bot API (per route) | 120/min |
| Call heartbeat | 240/min |
Pagination#
There is no single pagination scheme; each list uses the cursor that fits its data:
| Endpoint | Scheme |
|---|---|
GET /conversations/{id} | before_id / after_id / around_id + limit (≤ 100), has_more |
GET /conversations/{id}/messages/search | before_id, 20 per page |
GET /messages/search | before_id → next_before_id, 30 per page |
GET /conversations/{id}/media | offset/limit in attachments → next_offset, has_more |
GET /conversations/{id}/info | after + after_role → members_has_more |
GET /conversations/{id}/bans | after (ban_id) + limit → bans_has_more |
GET /conversations | not paginated (all chats) |
Bot getUpdates | offset (confirm) + limit ≤ 100 |
Conventions#
- IDs are integers; attachment ids are UUID strings.
- Timestamps are ISO 8601 in UTC (e.g.
2026-10-22T09:06:13+00:00or…000000Z). Bot API uses Unix time (date). - Booleans in multipart forms may be sent as
1/0. - Send
Accept: application/jsonso errors come back as JSON. - Send
client_id(UUID) with messages to make retries idempotent. - Attachment URLs (
/storage/media/…) require the same Bearer token — send it only to the Min host.
Versioning#
The API is not versioned in the URL. It evolves additively: new fields and endpoints may appear at any time, so ignore unknown fields. Breaking changes are avoided; legacy endpoints (e.g. /api/login, /api/files/…) stay available for older clients. Native clients are steered through GET /api/client-updates (update: soft|force).
Public web URLs#
| URL | Page |
|---|---|
/c/{username} | Public channel page |
/c/{username}/{message} | Public post |
/channels | Channel catalog |
/join/{code} | Group/channel invite |
/u/{token}, /u/{username} | Contact link |
/e/{slug} | Emoji set |
/invite/{code} | Registration invite |
/web | Web app |