07-api-reference

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}.


/blocks

PATCH /blocks/{block_id}

Редактировать один блок in-place (заголовок и/или content). Меняет три места одновременно в одной транзакции (RLS): blocks.updated_at + block_content.content + соответствующий блок в published_courses.course JSONB. Версия курса не инкрементируется. См. ADR-0019.

Тело:

{"title": "Новый заголовок", "content": {"markdown": "..."}}

Хотя бы одно из полей обязательно. content валидируется по Pydantic-схеме типа блока (textTextBlockContent, role_playRolePlayBlockContent, …).

Опциональный заголовок If-Match: <iso8601 updated_at> для optimistic concurrency. Несовпадение → 412.

Доступ: platform_admin / school_admin / teacher. Студент → 403.

Ошибки:

POST /blocks/{block_id}/regenerate

Перегенерировать content блока через зарегистрированный генератор (тот же, которым Composer создаёт блок при сборке курса). Заголовок не меняется (для этого есть PATCH). Сохраняет результат в те же три места, что и PATCH. См. ADR-0020.

Тело (опциональное):

{"refine_prompt": "сделай короче и формальнее, ≤2000 символов"}

refine_prompt дописывается приоритетным префиксом ко каждому Gemini-вызову, который делает генератор (стиль аналогичен style_prefix в /media/generate-narration).

Доступ: platform_admin / school_admin / teacher. Студент → 403.

Ошибки:


/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 — заблокировано.

GET /media/tts/languages

Публичный справочник BCP-47 языков, поддерживаемых Gemini TTS (источник — таблица tts_languages, миграция 0019_tts_languages). Используется редактором audio / role_play блоков для выпадающего списка языков. Без авторизации, без RLS. Возвращает массив {code, display_name, native_name}, отсортированный по sort_order. Пример:

[
  {"code": "en-US", "display_name": "English (US)", "native_name": "English (US)"},
  {"code": "nb-NO", "display_name": "Norwegian Bokmål", "native_name": "Norsk bokmål"}
]

Все медиа кладутся в MinIO бакет media-assets и регистрируются в media_assetscost_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"}

Поведение:

Ответ:

{
 "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 — источник правды. Это руководство может отставать на пару версий.