07. HTTP API reference
Базовый URL в dev: http://127.0.0.1:55400.
Авторизация: Authorization: Bearer <access_token> (кроме /auth/login и webhook-эндпоинтов).
Токен получать через POST /auth/login. Живёт 15 минут; refresh автоматически на фронте.
/auth
POST /auth/login
Вход.
{"email": "school_admin@local", "password": "change_me_locally"}
Ответ:
{"access_token": "eyJ...", "refresh_token": "eyJ...",
"user": {"id": "...", "email": "...", "is_platform_admin": false,
"memberships": [{"school_id": "...", "role_code": "school_admin"}]}}
POST /auth/refresh
Обновить access-токен.
{"refresh_token": "eyJ..."}
POST /auth/logout
Завершить сессию. Единственный способ разлогиниться.
/demo (dev-sanity)
GET /demo/visible-schools
Все школы, которые видит текущий пользователь по RLS. Для platform_admin — все; для остальных — своя (одна).
/courses
GET /courses
Каталог курсов школы текущего пользователя: {drafts: [...], published: [...]}.
GET /courses/{id}
Курс + последняя опубликованная версия + список всех версий.
GET /courses/{id}/v/{version}
Конкретная историческая версия (read-only).
POST /courses/{id}/copy
Deep-copy курса в ту же школу. Platform_admin обязан передать X-School-Id.
{"new_course_id": "clone-001", "new_title": "Клон"}
Коды: 201 success / 400 X-School-Id required / 404 not found / 409 id taken / 422 invalid.
PUT /courses/{id}/draft
Autosave черновика (teacher / school_admin). Тело — полная JSON-структура draft'а.
/cost
GET /cost/report?days=30
LLM-затраты. RLS автоматически фильтрует по школе (кроме platform_admin — видит всё).
/code
POST /code/run
Выполнить код в Judge0.
{"language": "python", "source": "print(42)", "stdin": ""}
Ответ: {stdout, stderr, time_ms, status}.
/media
POST /media/image
Сгенерировать изображение через Nano Banana.
{"prompt": "A neural network diagram, minimalist, watercolor"}
В dev при LMS_TEST_MODE=1 — cap 3 вызова на тест.
POST /media/audio
Gemini TTS / Lyria. В test-mode — заблокировано.
POST /media/video
Veo. В test-mode — заблокировано.
Все медиа кладутся в MinIO бакет media-assets и регистрируются в media_assets (с cost_usd).
/billing
GET /billing/providers
Список сконфигурированных провайдеров школы (или всех для platform_admin).
POST /billing/checkout
Создать checkout-сессию.
{"provider": "stripe", "sku": "plan_basic"}
Возвращает {checkout_url, session_id}. В dev checkout_url ведёт на /billing/mock-checkout.
POST /billing/webhooks/{provider}
Принимает события от провайдера. Без авторизации (подпись по body). Идемпотентно: повторный event возвращает {status:"duplicate", ack:true}. Используется superuser-движок → обходит RLS.
/learner (Phase 5, ADR-0015)
POST /learner/events
Записать событие обучения.
{"course_id": "demo-course",
"course_version": 1,
"block_path": "modules[0].lectures[0].blocks[0]",
"block_id": null,
"event_type": "view",
"payload": {}}
event_type ∈ {view, attempt, pass, fail, complete, skip}.201 → {id, occurred_at}. Пишется через RLS-движок: student пишет только о себе, school_admin — о любом в своей школе.
GET /learner/analytics/{course_id}
Агрегация по курсу. Доступ: school_admin / teacher / platform_admin (student → 403 analytics_forbidden_for_students).
Ответ:
{
"course_id": "demo-course",
"events_total": 120,
"learners_total": 8,
"completes": 5,
"fails": 12,
"attempts": 40,
"completion_rate": 0.625,
"hardest_blocks": [
{"block_path": "modules[0].lectures[1].blocks[2]",
"attempts": 10, "fails": 7, "fail_rate": 0.7}
]
}
/adaptive (Phase 5, ADR-0015)
POST /adaptive/supplement
Попросить адаптивный блок-подсказку.
{"course_id": "demo-course",
"course_version": 1,
"source_block_path": "modules[0].lectures[0].blocks[0]",
"source_block_type": "quiz_single",
"source_block_content": { ... },
"recent_events": [
{"event_type": "fail", "occurred_at": "..."}
],
"language": "ru"}
Поведение:
- если кэш есть (
adaptive_blocksUNIQUE по user+src_block) — вернуть кэш; - иначе проверить cap 5/студент/день →
429 adaptive_daily_cap_reached; - иначе
AdaptiveSupplementer.run(...)→ Gemini FAST → валидация черезTextBlockContent / FlashcardDeckContent / QuizSingleContent→INSERT INTO adaptive_blocks→ вернуть.
Ответ:
{
"block_id": "...",
"block_type": "text",
"block_content": { ... },
"cached": false,
"model": "gemini-3.1-flash-preview",
"cost_usd": 0.00012
}
/health, /ready
GET /health
Liveness. 200 {"status":"ok"}.
GET /ready
Readiness (проверяет БД, MinIO, Temporal). В dev обычно 200; 503 если что-то лежит.
Стандартные ошибки
| Код | Когда |
|---|---|
| 400 | Ошибка валидации на уровне роутера (например, X-School-Id required) |
| 401 | Нет/плохой JWT |
| 403 | Прошла аутентификация, но нет прав (RLS не пустил / роль не та) |
| 404 | Ресурс не найден или невидим (RLS) |
| 409 | Конфликт (дубликат id курса) |
| 422 | Pydantic-валидация тела запроса |
| 429 | Rate limit / бюджет / adaptive cap |
| 500 | Серверная (в логах) |
| 503 | /ready говорит что сервис не готов |
Как посмотреть полный актуальный список эндпоинтов
# OpenAPI JSON
curl http://127.0.0.1:55400/openapi.json | .venv/bin/python -m json.tool | less
# Swagger UI
open http://127.0.0.1:55400/docs
OpenAPI — источник правды. Это руководство может отставать на пару версий.