API reference
Who is this for
Section titled “Who is this for”A developer who integrates with, debugs or changes the HTTP surface of ai-sticker-pack: the kiosk page, the
landing page’s lead delivery, the admin panel or a monitoring probe.
- One FastAPI application
(
bot/api.py) serves everything: kiosk endpoints, the static kiosk page,/health,POST /leadsand the admin panel. Production base URL:https://stickers.eventtalks.uz. Locally:http://localhost:8000. - Kiosk endpoints have no authentication. Their only protection is the unguessable session token and the state machine: nothing can be chosen or uploaded before consent is given in the bot.
POST /leadsrequires the shared secret headerX-Stickers-Leads-Secret./adminand/admin/api/*require HTTP Basic; state-changing calls also require the headerX-Admin-Action: 1.- In maintenance,
POST /session,POST /session/{token}/templateandPOST /session/{token}/photoanswer503with{"detail": {"code": "maintenance", …}}. - All rate limiters live in process memory and reset on restart. The API must run as a single worker process.
Conventions
Section titled “Conventions”- Bodies are JSON unless stated otherwise. Errors raised by the framework have the shape
{"detail": …}; the leads endpoint uses its own shape{"ok": false, "error": …}. <token>is a session token: 16 URL-safe characters generated by the server.- Session statuses:
idle → linked → consented → template_chosen → photo_taken, plusexpired. See State machines. - Some error texts are in Russian because the kiosk shows or logs them. They are quoted verbatim below.
- FastAPI’s generated documentation routes (
/docs,/redoc,/openapi.json) are not disabled in code. Routes declared withinclude_in_schema=False(/,/robots.txt,/kiosk-theme.js,/kiosk/light,/admin,/leads) do not appear there.
Overview
Section titled “Overview”| Method | Path | Auth | Purpose |
|---|---|---|---|
POST |
/session |
none | Create a kiosk session and its deep link |
GET |
/session/{token} |
none | Poll session status |
GET |
/session/{token}/qr.svg |
none | QR code with the deep link |
POST |
/session/{token}/template |
none | Choose an outfit (after consent) |
POST |
/session/{token}/photo |
none | Upload the selfie, queue the job |
POST |
/session/{token}/expire |
none | Kiosk abandons the session |
GET |
/templates |
none | Outfit cards for the kiosk |
GET |
/templates/{id}.png |
none | Card image |
GET |
/branding |
none | Optional footer logos |
GET |
/kiosk-config |
none | Kiosk timers |
GET |
/kiosk-theme.js |
none | Theme bootstrap script |
GET |
/kiosk/light, /kiosk/light/ |
none | Kiosk page in the light theme |
GET |
/kiosk/… |
none | Static kiosk files |
GET |
/ |
none | Redirect to /kiosk/ |
GET |
/robots.txt |
none | Disallow everything |
GET |
/health |
none | Liveness and version |
POST |
/leads |
secret header | Store a request from the landing page |
GET |
/admin |
Basic | Admin panel page |
GET |
/admin/api/summary |
Basic | Spend, balance, queue, errors |
GET |
/admin/api/guests |
Basic | Guest and job table |
GET |
/admin/api/jobs/{job_id}/cost |
Basic | Cost of one job by stage |
GET |
/admin/api/jobs/{job_id}/anchor |
Basic | Anchor portrait of a job |
GET |
/admin/api/jobs/{job_id}/consistency |
Basic | Consistency summary of a job |
GET |
/admin/api/audit |
Basic | Last 100 admin actions |
POST |
/admin/api/balance |
Basic + action header | Set the real provider balance |
POST |
/admin/api/guests/{telegram_user_id}/grant |
Basic + action header | Allow one more pack |
POST |
/admin/api/guests/{telegram_user_id}/reset |
Basic + action header | Delete a guest’s records |
POST |
/admin/api/jobs/{job_id}/retry |
Basic + action header | Re-queue a failed job |
Kiosk and session
Section titled “Kiosk and session”POST /session
Section titled “POST /session”Called when a guest presses the start button. No body.
Success 200:
{ "session_token": "<token>", "deep_link": "https://t.me/<bot_username>?start=<token>" }| Status | Body | Meaning |
|---|---|---|
503 |
{"detail": {"code": "maintenance", "message": "Сейчас идёт техобслуживание, загляни чуть позже 🛠"}} |
Maintenance mode. No session is created. |
GET /session/{token}
Section titled “GET /session/{token}”The kiosk polls this once a second.
{ "status": "consented", "template_id": null, "notice": null }| Field | Values |
|---|---|
status |
idle, linked, consented, template_chosen, photo_taken, expired |
template_id |
outfit id or null |
notice |
null, "pack_ready" or "pack_in_progress" — the session was closed by the pack limit and the kiosk shows a friendly screen instead of an error |
| Status | Body | Meaning |
|---|---|---|
404 |
{"detail": "session not found"} |
Unknown token |
GET /session/{token}/qr.svg
Section titled “GET /session/{token}/qr.svg”Returns image/svg+xml: a QR code of the deep link, error correction level M, quiet zone of 4 modules, generated
on the server so the kiosk needs no external library. Header Cache-Control: no-store.
| Status | Meaning |
|---|---|
404 |
Unknown token |
POST /session/{token}/template
Section titled “POST /session/{token}/template”Request:
{ "template_id": "u1" }Success 200:
{ "status": "template_chosen", "template_id": "u1" }Allowed from consented and from template_chosen (the guest may change their mind until the photo is taken; the
last choice wins).
Checks run in this order:
| Status | Body | Meaning |
|---|---|---|
422 |
{"detail": "неизвестный шаблон: <id>"} |
The id is not in the catalogue. Also the framework’s own 422 for a malformed body. |
422 |
{"detail": "шаблон недоступен: <id>"} |
The id is a hidden placeholder. |
503 |
maintenance body | Maintenance mode |
404 |
{"detail": "session not found"} |
Unknown token |
409 |
{"detail": {"code": "pack_limit", "state": "ready" | "in_progress", "notice": "pack_ready" | "pack_in_progress"}} |
The guest already has a pack or one is being made. The session is closed with that notice, and the bot sends the guest the link to the existing pack or a “still in progress” message. |
409 |
{"detail": "выбор шаблона сейчас невозможен, статус: <status>"} |
Wrong state — in particular, no consent yet. |
POST /session/{token}/photo
Section titled “POST /session/{token}/photo”multipart/form-data with one field, file.
Success 200:
{ "job_id": 42, "status": "queued" }What the server does: reads at most 15 MB, decodes the image, applies the EXIF orientation, converts to RGB,
downsizes so the longer side is at most 2048 px, re-encodes as JPEG (quality 92). Re-encoding removes all EXIF
metadata, including location and device model. The file is stored under a random name, the session moves to
photo_taken, a job is queued, and the response is returned. The “generating…” message to the guest is sent
after the response, in a background task.
| Status | Body | Meaning |
|---|---|---|
503 |
maintenance body | Maintenance. The photo is not stored and no job is created. |
404 |
{"detail": "session not found"} |
Unknown token |
409 |
{"detail": "сессия не готова к фото, статус: <status>"} |
The session is not in template_chosen. Also returned to the loser of a double tap; its file is deleted immediately. |
413 |
{"detail": "фото слишком большое"} |
Upload larger than 15 MB |
422 |
{"detail": "не картинка: …"} |
Not a decodable image |
422 |
{"detail": "слишком маленькое фото: (w, h)"} |
Shorter side below 256 px |
409 |
{"detail": {"code": "pack_limit", …}} |
Pack limit hit at the atomic check. The uploaded file is deleted immediately. |
Limits: 15 MB per upload; minimum 256 px on the shorter side. There is no rate limit on kiosk endpoints.
POST /session/{token}/expire
Section titled “POST /session/{token}/expire”Query parameter reason, default kiosk_reset. Accepted values: idle_timeout, qr_timeout, cancel,
staff_reset, camera_error, kiosk_reset. Any other value is stored as kiosk_reset.
Always 200:
{ "expired": true }expired is false when nothing changed: unknown token, or the session was already in photo_taken or
expired. A session closed this way does not consume the guest’s one-pack allowance. This endpoint is not
blocked by maintenance.
Templates, branding, configuration
Section titled “Templates, branding, configuration”GET /templates
Section titled “GET /templates”Outfits visible to guests, in catalogue order.
[ { "id": "m1", "label": "Бирюзовый бомбер", "preview": "/templates/m1.png?v=3f2a9c0d1b7e" }]v is the first 12 hex characters of the file’s SHA-256. If the file is missing, preview has no v.
Placeholders appear only with SHOW_PLACEHOLDER_TEMPLATES=1.
GET /templates/{template_id}.png
Section titled “GET /templates/{template_id}.png”Returns the card image of a catalogue entry: its preview file if set, otherwise its file. The path always ends
in .png, but the content type follows the real file (image/webp for preview cards, image/png for body
templates).
| Request | Response headers |
|---|---|
?v= equals the current version |
Cache-Control: public, max-age=31536000, immutable, ETag: "<version>" |
no v, or an old one |
Cache-Control: public, no-cache, ETag: "<version>" — cache but revalidate |
If-None-Match matches (weak or strong, or *) |
304 with the same headers, no body |
| Status | Body | Meaning |
|---|---|---|
404 |
{"detail": "template not found"} |
The id is not in the catalogue, or the file is missing. Files outside the catalogue are never served. |
GET /branding
Section titled “GET /branding”{ "logos": [ { "src": "/kiosk/static/<file>.png", "alt": "<label>" } ] }Lists only the logo files that actually exist in bot/kiosk/static/ (two fixed slots), so the kiosk never requests
a missing image. An empty list is normal.
GET /kiosk-config
Section titled “GET /kiosk-config”{ "idle_sec": 90, "idle_prompt_sec": 15 }From KIOSK_IDLE_SEC and KIOSK_IDLE_PROMPT_SEC.
GET /kiosk-theme.js
Section titled “GET /kiosk-theme.js”A tiny synchronous script loaded in the page head. It sets data-theme on the root element before first paint:
?theme=light|dark in the page URL wins, then the /kiosk/light address, then KIOSK_THEME. Content type
text/javascript, Cache-Control: no-store.
Static kiosk
Section titled “Static kiosk”| Path | Behaviour |
|---|---|
GET / |
Redirect (307) to /kiosk/ |
GET /kiosk/… |
Static files from bot/kiosk/ (HTML mode: /kiosk/ serves index.html) |
GET /kiosk/light |
The same index.html, Cache-Control: no-cache; the theme script picks the light theme from the address |
GET /kiosk/light/ |
Redirect to /kiosk/light, query string preserved (relative asset paths would break with the trailing slash) |
GET /robots.txt |
User-agent: * / Disallow: / |
Health
Section titled “Health”GET /health
Section titled “GET /health”No authentication, no secrets in the answer.
curl -s https://stickers.eventtalks.uz/health200:
{ "ok": true, "db": "ok", "active_jobs": 0, "maintenance": null, "version": "b715418", "render_mode": "figure", "app_env": "prod"}| Field | Meaning |
|---|---|
db |
The database really answered a query |
active_jobs |
Jobs in queued, processing or delivering |
maintenance |
null, or the source of the pause: "env" (MAINTENANCE_MODE=1), "auto" (after an “out of credit” answer), "admin" (/pause) |
version |
Content of the VERSION file written at deploy (short commit hash); locally the output of git rev-parse --short HEAD; otherwise "unknown" |
render_mode |
STICKER_RENDER_MODE as this process sees it |
app_env |
prod or dev |
| Status | Body | Meaning |
|---|---|---|
503 |
{"ok": false, "db": "error", "version": …, "render_mode": …, "app_env": …} |
The database does not answer |
/health stays 200 during maintenance. It says nothing about the bot process; see
Monitoring & alerts.
POST /leads
Section titled “POST /leads”Source: bot/leads_api.py. Called
by the landing page’s server action, never by a browser. The endpoint only validates and stores; the bot publishes
the card to the requests group later, so a lead is not lost when the bot is down.
Auth: header X-Stickers-Leads-Secret: <LEADS_SECRET>, compared in constant time.
curl -i -X POST https://stickers.eventtalks.uz/leads \ -H "Content-Type: application/json" \ -H "X-Stickers-Leads-Secret: <LEADS_SECRET>" \ -d '{ "request_id": "3f0c1a52-6e1b-4a8e-9d1f-0b6c2a7e4d10", "name": "Guest Name", "telegram": "@example_user", "phone": "+<digits>", "date": "mid December", "guests": "about 200", "comment": "ТЕСТ", "locale": "en", "utm": "utm_source=docs utm_campaign=example", "submitted_at": "2026-11-13T10:00:00Z", "client_ip": "" }'Put the marker ТЕСТ (or test) into the name or comment of a test lead: only such leads can be deleted
completely afterwards (see Bot commands).
Request fields
Section titled “Request fields”Exact rules from
bot/services/leads.py::validate.
String fields are trimmed first. A field that is not a string counts as empty.
| Field | Required | Rule | Error code |
|---|---|---|---|
request_id |
yes | Must parse as a UUID; stored in canonical lower-case form. The landing page creates it when the form opens, so a retry carries the same id. | requestIdInvalid |
name |
yes | 2 to 80 characters after trimming | nameRequired (shorter than 2), tooLong (longer than 80) |
telegram |
yes | Normalised first: a leading https://, http://, www., t.me/ or telegram.me/ prefix is removed, then a trailing /, then a leading @; the result is prefixed with @. Must match ^@[A-Za-z][A-Za-z0-9_]{3,30}[A-Za-z0-9]$: 5–32 characters after @, starts with a letter, does not end with _. |
telegramInvalid |
phone |
yes | Spaces, parentheses and hyphens are removed. Must match ^\+\d{7,15}$. |
phoneInvalid |
date |
no | Free text, at most 60 characters. Stored as event_date. |
tooLong |
guests |
no | Free text, at most 40 characters | tooLong |
comment |
no | At most 1200 characters | tooLong |
locale |
no | ru, uz or en; anything else becomes en. Never an error. |
— |
utm |
no | String, cut to 300 characters. Parsed as space-separated key=value pairs; utm_source, utm_medium, utm_campaign are extracted (each cut to 100 characters). |
— |
submitted_at |
no | Cut to 40 characters, stored as is | — |
client_ip |
no | The visitor’s address as seen by the landing page. A valid IPv4 or IPv6 address is hashed (HMAC-SHA-256 with LEADS_IP_SALT, first 32 hex characters) and only the hash is stored. Anything else is ignored, and the per-visitor limit then does not apply to this lead. |
— |
Empty optional fields are stored as NULL. Lengths are counted in Unicode code points, which is never stricter
than the landing page’s UTF-16 count. The rules and codes mirror the landing schema
(src/lib/requestSchema.ts); a change must be made on both sides.
Responses
Section titled “Responses”| Status | Body | Meaning |
|---|---|---|
201 |
{"ok": true, "lead": 42, "duplicate": false} |
Stored as a new lead |
200 |
{"ok": true, "lead": 42, "duplicate": true} |
The same request_id was already stored: same number, nothing new. Checked before rate limits, so a retry never gets 429. |
200 |
{"ok": true, "lead": 42, "duplicate": true, "repeat": true} |
Repeat request: the same Telegram handle (case-insensitive) or phone within 24 hours of an existing lead that is not spam and not anonymised. No new lead; the bot updates the existing card and pings the group. |
401 |
{"ok": false, "error": "unauthorized"} |
Wrong or missing secret |
404 |
{"detail": "Not Found"} |
The module is off: LEADS_ENABLED=0 or LEADS_SECRET empty. Indistinguishable from a missing route on purpose. |
413 |
{"ok": false, "error": "too_large"} |
Body larger than 16 KB |
422 |
{"ok": false, "error": "invalid", "errors": {"_": "badJson"}} |
Body is not a JSON object |
422 |
{"ok": false, "error": "invalid", "errors": {"phone": "phoneInvalid", …}} |
Field errors, one code per field |
429 |
{"ok": false, "error": "rate_limited", "why": "secret" | "visitor" | "hourly"} + Retry-After |
See limits below |
How the landing page reacts to each status is described in Lead submission.
Limits
Section titled “Limits”| Limit | Value | Key | Response |
|---|---|---|---|
| Wrong secret | 5 failures in 600 s | caller IP (the landing server) | 429, why: "secret", until the window passes |
| Per visitor | LEADS_IP_LIMIT (default 5) per LEADS_IP_WINDOW_SEC (default 600 s) |
hash of client_ip |
429, why: "visitor" |
| Total | LEADS_HOURLY_LIMIT (default 60) per hour |
all leads | 429, why: "hourly", plus one alert to the admin chat per hour |
| Body size | 16 KB | — | 413 |
Counters are sliding windows in process memory. Logs contain the lead number, request_id, locale and
utm_source only — never the name, handle or phone.
Admin panel
Section titled “Admin panel”Source: bot/admin_panel.py.
Authentication and protection
Section titled “Authentication and protection”| Mechanism | Behaviour |
|---|---|
| HTTP Basic | ADMIN_PANEL_USER / ADMIN_PANEL_PASSWORD, both compared in constant time. |
| Password not set | Every admin route answers 503 (dev only; in prod the API does not start). |
No Authorization header |
401 with WWW-Authenticate: Basic realm="sticker-admin". Not counted as a failure — this is the browser’s first request. |
| Wrong credentials | 401 after a 0.5 s delay; counted per client IP. |
| Lockout | 5 failures within 10 minutes → that IP gets 429 with Retry-After for 10 minutes. Log markers ADMIN_LOGIN_FAILED, ADMIN_LOGIN_BLOCKED. |
| CSRF | Every POST requires the header X-Admin-Action: 1; without it 403. A browser sends Basic credentials automatically, but another site cannot add a custom header without CORS. |
| Audit | Every POST action is written to audit_log with the login, IP, action and details. |
Response headers on /admin and /admin/*, including 401 and 429 |
Cache-Control: no-store, X-Robots-Tag: noindex, nofollow, noarchive, Referrer-Policy: no-referrer, X-Frame-Options: DENY, and a Content-Security-Policy restricted to 'self' (inline script and style allowed, no external origins, no framing, no form submission). |
The panel never returns photos, file paths, the appearance card or debug sheets. Guest names are visible only after login.
GET /admin
Section titled “GET /admin”The panel itself: one self-contained HTML page (bot/admin/index.html) that calls the endpoints below.
GET /admin/api/summary
Section titled “GET /admin/api/summary”{ "maintenance": null, "image_rpd": { "used": 0, "limit": 1000, "day_pt": "2026-11-13", "warn": false, "warn_frac": 0.7 }, "job_max_cost_usd": 1.5, "spent_total_usd": 0.0, "spent_today_usd": 0.0, "balance": { "balance": null, "basis": null, "override": null, "spent_since": 0.0, "note": "…" }, "budget_usd": null, "prices_missing": [], "packs": { "ready": 0, "in_progress": 0, "failed": 0 }, "queue": { "queued": 0, "processing": 0, "delivering": 0 }, "avg_job_sec": null, "errors_24h": { "rate_limited": 0, "errors": 0 }, "render_mode": "figure", "version": "b715418", "utc_offset_hours": 5.0}| Field | Meaning |
|---|---|
maintenance |
null or {"source": "env" | "auto" | "admin", "reason": …, "since": …} |
image_rpd |
Image requests in the provider’s current day (Pacific time) against IMAGE_RPD_LIMIT |
spent_today_usd |
Estimated spend since local midnight (DISPLAY_UTC_OFFSET_HOURS) |
balance.basis |
"override" (a real balance was typed in; spend since then is subtracted), "budget" (BUDGET_USD minus all spend) or null |
prices_missing |
Names of price variables that are not set; a non-empty list means spend is underestimated |
The same data feeds the bot command /admin_stats.
GET /admin/api/guests
Section titled “GET /admin/api/guests”Query: q (search in name, username, Telegram user id, pack name; cut to 100 characters) and status (queued,
processing, ready, failed, started, or empty). At most 500 rows.
{ "rows": [ { "job_id": 42, "telegram_user_id": "<telegram_user_id>", "first_name": "Guest", "last_name": null, "username": "example_user", "extra_packs_allowed": 0, "status": "ready", "created_at": "…", "ready_at": "…", "duration_sec": 61, "attempts": 1, "render_mode": "figure", "error": null, "cost_usd": 0.4, "pack_name": "…", "pack_link": "https://t.me/addstickers/…", "has_anchor": true } ]}telegram_user_id is an integer in real responses. Statuses are in panel terms: done is shown as ready,
delivering as processing, and started marks a guest who began but has no job. error is cut to 200
characters. pack_link is built only from a pack name that passes a strict pattern.
| Status | Meaning |
|---|---|
422 |
Unknown status value |
GET /admin/api/jobs/{job_id}/cost
Section titled “GET /admin/api/jobs/{job_id}/cost”{ "job_id": 42, "stages": [ { "stage": "…", "model": "…", "resolution": "…", "calls": 1, "images": 1, "cost_usd": 0.07, "failed": 0 } ], "total_usd": 0.4}Estimates from the usage log, grouped by stage, model and resolution. An unknown job returns an empty list.
GET /admin/api/jobs/{job_id}/anchor
Section titled “GET /admin/api/jobs/{job_id}/anchor”image/png: the stylised anchor portrait of a job (a drawn character, not the photo). 404 when the job has no
anchor, the file was removed by the TTL, or ADMIN_SHOW_ANCHOR=0.
GET /admin/api/jobs/{job_id}/consistency
Section titled “GET /admin/api/jobs/{job_id}/consistency”{ "skin": [ { "sticker": 1, "de_before": 7.2, "de_after": 2.1, "applied": true, "regenerated": false } ], "qa": {}, "regen": {}, "skin_de_max": 6.0, "anchor_verify": { "mismatch": [], "retried": false }, "edge_kept": null}Colour distances per sticker, QA outliers and regenerations. The values above are illustrative; qa, regen
and edge_kept are passed through from the job’s report file as written by the worker. anchor_verify.mismatch lists only check names, not
the model’s remarks about a person’s appearance. 404 ({"detail": "нет данных"}) when there is no report or
ADMIN_SHOW_ANCHOR=0.
GET /admin/api/audit
Section titled “GET /admin/api/audit”{ "entries": [ { "id": 1, "ts": "…", "actor": "admin", "ip": "…", "action": "grant_extra_pack", "telegram_user_id": null, "job_id": null, "details": "{…}" } ] }The last 100 entries, newest first. action is one of grant_extra_pack, reset_user, retry_job,
set_balance_override. details is a JSON string.
POST /admin/api/balance
Section titled “POST /admin/api/balance”Body {"value": 12.5} — the real balance from the provider’s billing page, in USD. From then on the panel shows
this value minus the estimated spend after that moment.
| Status | Body | Meaning |
|---|---|---|
200 |
{"ok": true, "balance": { … }} |
Saved; balance has the shape shown in the summary |
422 |
{"detail": "остаток должен быть от 0 до 1 000 000 USD"} |
Value outside 0 ≤ value < 1 000 000 |
POST /admin/api/guests/{telegram_user_id}/grant
Section titled “POST /admin/api/guests/{telegram_user_id}/grant”Allows the guest one more pack (users.extra_packs_allowed + 1). 200: {"ok": true, "extra_packs_allowed": 1}.
POST /admin/api/guests/{telegram_user_id}/reset
Section titled “POST /admin/api/guests/{telegram_user_id}/reset”Deletes the guest’s users, sessions and jobs rows after backing up the database.
{ "ok": true, "backup": "app.db.bak-…", "jobs": 1, "sessions": 2, "users": 1, "old_packs": ["https://t.me/addstickers/…"] }Packs in Telegram are not deleted; their links are returned and kept in the audit entry.
POST /admin/api/jobs/{job_id}/retry
Section titled “POST /admin/api/jobs/{job_id}/retry”Re-queues a failed job. 200: {"ok": true}.
| Status | detail |
Meaning |
|---|---|---|
409 |
джоб не найден или не в статусе failed |
Unknown job, or not failed |
409 |
селфи уже удалено (TTL/после пака) — гостю нужно сфотографироваться заново |
The selfie is gone; the guest must be photographed again |
409 |
лимит паков гостя исчерпан (… из …) — сначала «Разрешить ещё раз» |
Pack limit; grant first |
Errors common to all admin POST calls
Section titled “Errors common to all admin POST calls”| Status | Meaning |
|---|---|
401 |
No or wrong credentials |
403 |
Header X-Admin-Action: 1 missing |
429 |
IP locked out after failed logins |
503 |
ADMIN_PANEL_PASSWORD not set |
Maintenance behaviour
Section titled “Maintenance behaviour”Maintenance is on when MAINTENANCE_MODE=1, or when a pause is stored in the database (automatically after an
“out of credit” answer from the provider, or by /pause).
| Endpoint | In maintenance |
|---|---|
POST /session |
503, code maintenance |
POST /session/{token}/template |
503 (after the 422 template checks) |
POST /session/{token}/photo |
503; nothing is stored |
GET /session/{token}, qr.svg, POST …/expire |
Work normally |
GET /templates, /branding, /kiosk-config, static kiosk |
Work normally |
GET /health |
200, maintenance names the source |
POST /leads |
Works normally — leads do not depend on generation |
/admin, /admin/api/* |
Work normally; the summary carries the maintenance state |
The 503 body is always:
{ "detail": { "code": "maintenance", "message": "Сейчас идёт техобслуживание, загляни чуть позже 🛠" } }Where to go next
Section titled “Where to go next”- Guest session flow — the endpoints above in sequence.
- Lead submission — how the landing page calls
POST /leadsand falls back. - Sticker API component — internals of this application.
- Security — the threat model behind the limits and headers.
- Bot commands — the Telegram side.
- Runbook: kiosk 502 — when the API does not answer.
Last verified: 2026-11-13 against ai-sticker-pack@b715418 and ai-stickers-landing@e350ff5.