Экзамены (ЕНТ)
Многосекционные экзамены: куратор собирает экзамен из секций (квизов), ученик проходит его по секциям с единым таймером. Авторство — manage_exams, прохождение — любой участник.
- GET
/api/v1/t/examsСписок экзаменов - GET
/api/v1/t/exams/:idЭкзамен с секциями - POST
/api/v1/t/examsСоздать экзамен - PATCH
/api/v1/t/exams/:idОбновить экзамен - DELETE
/api/v1/t/exams/:idУдалить экзамен - POST
/api/v1/t/exams/:id/publishОпубликовать экзамен - POST
/api/v1/t/exams/:id/unpublishСнять с публикации - POST
/api/v1/t/exams/:id/sectionsДобавить секцию - PATCH
/api/v1/t/exams/:id/sections/:section_idОбновить секцию - DELETE
/api/v1/t/exams/:id/sections/:section_idУдалить секцию - POST
/api/v1/t/exams/:id/sections/reorderИзменить порядок секций - GET
/api/v1/t/exams/:id/analyticsАналитика по экзамену - POST
/api/v1/t/exam-attemptsНачать попытку экзамена - GET
/api/v1/t/exam-attempts/activeАктивная попытка - GET
/api/v1/t/exam-attempts/:idПопытка экзамена - POST
/api/v1/t/exam-attempts/:id/goПерейти к секции - POST
/api/v1/t/exam-attempts/:id/sections/:n/answersСохранить ответы секции - POST
/api/v1/t/exam-attempts/:id/submitЗавершить попытку
/api/v1/t/examsСписок экзаменов
Возвращает экзамены школы. Ученикам всегда отдаются только опубликованные экзамены (черновики не утекают). Куратор/админ по умолчанию видят все статусы и могут сузить выборку параметром ?status=published|draft.
Фильтр status применяется только для кураторов/админов; для ученика он игнорируется (всегда published). Некорректное значение status — 400 invalid_status.
Query-параметры
| Параметр | Тип | Описание |
|---|---|---|
statusопц. | string | Только для куратора/админа: published | draft. У ученика игнорируется. |
Пример запроса
curl "https://api.edumentor.kz/api/v1/t/exams?status=published" \
-H "Authorization: Bearer <token>" \
-H "X-Tenant-Subdomain: acme"Пример ответа
{
"success": true,
"data": [
{ "id": "e1a2b3c4-...", "title": "Пробное ЕНТ — Математика", "status": "published" }
]
}/api/v1/t/exams/:idЭкзамен с секциями
Полное представление экзамена вместе со списком секций. Ответ зависит от роли: куратор/админ получают кураторское представление, ученик — студенческое с информацией о доступе и его попытках. Каждая секция содержит question_count (число вопросов в тесте секции).
Роль-зависимый ответ. Ученик видит только опубликованные экзамены — для черновика возвращается 404 exam_not_found. В студенческом представлении к базовым полям добавлены: result — лучшая/последняя завершённая попытка согласно retake_policy ({ attempt_id, score, max_score, score_pct, passed, submitted_at, sections }) либо null; result.sections — разбивка по секциям ({ section_id, title, weight, score, max_score, score_pct } для каждой секции), чтобы показать, сколько баллов получено за каждую часть; in_progress_attempt — незавершённая попытка для возобновления ({ attempt_id, started_at, deadline_at }) либо null; attempts_count — число завершённых попыток; auth_has_access (boolean) и has_access_untill (сегодня всегда null). Кураторское представление дополнительно к каждой секции содержит question_count.
Параметры пути
| Параметр | Тип | Описание |
|---|---|---|
idобяз. | uuid | ID экзамена. |
Пример запроса
curl https://api.edumentor.kz/api/v1/t/exams/e1a2b3c4-... \
-H "Authorization: Bearer <token>" \
-H "X-Tenant-Subdomain: acme"Пример ответа
{
"success": true,
"data": {
"id": "e1a2b3c4-...",
"title": "Пробное ЕНТ — Математика",
"description": "",
"time_limit_seconds": 14400,
"passing_score": 50,
"max_attempts": 3,
"retake_policy": "best",
"status": "published",
"sections": [
{ "id": "5ec70001-...", "quiz_id": "9d1f...", "title": "Часть A", "position": 1, "weight": 40, "question_count": 25 }
],
"result": {
"attempt_id": "a77e0001-...",
"score": "72.00",
"max_score": "100.00",
"score_pct": 72,
"passed": true,
"submitted_at": "2026-05-21T11:30:00Z",
"sections": [
{
"section_id": "5ec70001-...",
"title": "Часть A",
"weight": 40,
"score": "18.00",
"max_score": "25.00",
"score_pct": 72
}
]
},
"in_progress_attempt": {
"attempt_id": "a77e0002-...",
"started_at": "2026-05-22T09:00:00Z",
"deadline_at": "2026-05-22T13:00:00Z"
},
"attempts_count": 1,
"auth_has_access": true,
"has_access_untill": null,
"created_at": "2026-05-20T10:00:00Z",
"updated_at": "2026-05-20T10:00:00Z"
}
}Ошибки
| Код | error | Когда |
|---|---|---|
| 404 | exam_not_found | Экзамен не найден в этой школе (или это черновик, недоступный ученику). |
/api/v1/t/examsСоздать экзамен
Создаёт черновик экзамена. Секции добавляются отдельными запросами.
Тело запроса
| Параметр | Тип | Описание |
|---|---|---|
titleобяз. | string | Название (1–255). |
descriptionопц. | string | Описание. |
time_limit_secondsобяз. | integer | Общий лимит времени в секундах (> 0).Пример: 14400 |
passing_scoreопц. | integer | Проходной балл в процентах (0–100). 0 — допустимо. |
max_attemptsопц. | integer | Максимум попыток (> 0). Пусто — без ограничения. |
retake_policyопц. | string | Учитываемый результат при пересдаче: best или last. |
Пример запроса
curl -X POST https://api.edumentor.kz/api/v1/t/exams \
-H "Authorization: Bearer <token>" \
-H "X-Tenant-Subdomain: acme" \
-H "Content-Type: application/json" \
-d '{ "title": "Пробное ЕНТ — Математика", "time_limit_seconds": 14400, "passing_score": 50, "max_attempts": 3 }'Пример ответа
{
"success": true,
"data": {
"id": "e1a2b3c4-...",
"title": "Пробное ЕНТ — Математика",
"description": "",
"time_limit_seconds": 14400,
"passing_score": 50,
"max_attempts": 3,
"retake_policy": "best",
"status": "draft",
"sections": [
{ "id": "5ec70001-...", "quiz_id": "9d1f...", "title": "Часть A", "position": 1, "weight": 40, "question_count": 25 }
],
"created_at": "2026-05-20T10:00:00Z",
"updated_at": "2026-05-20T10:00:00Z"
}
}Ошибки
| Код | error | Когда |
|---|---|---|
| 400 | invalid_request_body | Некорректное тело запроса. |
| 403 | forbidden | Нет права manage_exams. |
/api/v1/t/exams/:idОбновить экзамен
Частичное обновление. Передавайте только изменяемые поля.
Структурные поля (time_limit_seconds, passing_score, max_attempts, retake_policy) запрещено менять у опубликованного экзамена — вернётся 409 exam_published_immutable. Флаги clear_passing_score / clear_max_attempts сбрасывают соответствующие значения в null.
Параметры пути
| Параметр | Тип | Описание |
|---|---|---|
idобяз. | uuid | ID экзамена. |
Тело запроса
| Параметр | Тип | Описание |
|---|---|---|
titleопц. | string | Название. |
descriptionопц. | string | Описание. |
time_limit_secondsопц. | integer | Лимит времени (> 0). |
passing_scoreопц. | integer | Проходной балл (0–100). |
clear_passing_scoreопц. | boolean | Сбросить проходной балл в null. |
max_attemptsопц. | integer | Максимум попыток (> 0). |
clear_max_attemptsопц. | boolean | Сбросить лимит попыток в null. |
retake_policyопц. | string | best или last. |
Пример запроса
curl -X PATCH https://api.edumentor.kz/api/v1/t/exams/e1a2b3c4-... \
-H "Authorization: Bearer <token>" \
-H "X-Tenant-Subdomain: acme" \
-H "Content-Type: application/json" \
-d '{ "title": "Пробное ЕНТ — Алгебра" }'Пример ответа
{
"success": true,
"data": {
"id": "e1a2b3c4-...",
"title": "Пробное ЕНТ — Математика",
"description": "",
"time_limit_seconds": 14400,
"passing_score": 50,
"max_attempts": 3,
"retake_policy": "best",
"status": "draft",
"sections": [
{ "id": "5ec70001-...", "quiz_id": "9d1f...", "title": "Часть A", "position": 1, "weight": 40, "question_count": 25 }
],
"created_at": "2026-05-20T10:00:00Z",
"updated_at": "2026-05-20T10:00:00Z"
}
}Ошибки
| Код | error | Когда |
|---|---|---|
| 404 | exam_not_found | Экзамен не найден. |
| 409 | exam_published_immutable | Структурные поля нельзя менять у опубликованного экзамена. |
/api/v1/t/exams/:idУдалить экзамен
Мягкое удаление экзамена.
Параметры пути
| Параметр | Тип | Описание |
|---|---|---|
idобяз. | uuid | ID экзамена. |
Пример запроса
curl -X DELETE https://api.edumentor.kz/api/v1/t/exams/e1a2b3c4-... \
-H "Authorization: Bearer <token>" \
-H "X-Tenant-Subdomain: acme"Пример ответа
{ "success": true, "data": { "deleted": true } }Ошибки
| Код | error | Когда |
|---|---|---|
| 404 | exam_not_found | Экзамен не найден. |
| 409 | exam_has_in_progress_attempts | Есть незавершённые попытки прохождения. |
/api/v1/t/exams/:id/publishОпубликовать экзамен
Делает экзамен доступным ученикам. Требуется хотя бы одна секция.
Параметры пути
| Параметр | Тип | Описание |
|---|---|---|
idобяз. | uuid | ID экзамена. |
Пример запроса
curl -X POST https://api.edumentor.kz/api/v1/t/exams/e1a2b3c4-.../publish \
-H "Authorization: Bearer <token>" \
-H "X-Tenant-Subdomain: acme"Пример ответа
{ "success": true, "data": { "published": true } }Ошибки
| Код | error | Когда |
|---|---|---|
| 400 | exam_section_required | В экзамене нет ни одной секции. |
| 409 | exam_already_published | Экзамен уже опубликован. |
/api/v1/t/exams/:id/unpublishСнять с публикации
Возвращает экзамен в черновик.
Параметры пути
| Параметр | Тип | Описание |
|---|---|---|
idобяз. | uuid | ID экзамена. |
Пример запроса
curl -X POST https://api.edumentor.kz/api/v1/t/exams/e1a2b3c4-.../unpublish \
-H "Authorization: Bearer <token>" \
-H "X-Tenant-Subdomain: acme"Пример ответа
{ "success": true, "data": { "unpublished": true } }Ошибки
| Код | error | Когда |
|---|---|---|
| 404 | exam_not_found | Экзамен не найден. |
/api/v1/t/exams/:id/sectionsДобавить секцию
Добавляет в экзамен секцию на основе существующего теста (квиза) с весом в общем балле.
Параметры пути
| Параметр | Тип | Описание |
|---|---|---|
idобяз. | uuid | ID экзамена. |
Тело запроса
| Параметр | Тип | Описание |
|---|---|---|
quiz_idобяз. | uuid | ID теста, который станет содержимым секции. |
titleобяз. | string | Название секции (1–255). |
weightобяз. | integer | Вес секции в общем балле (1–1000).Пример: 40 |
Пример запроса
curl -X POST https://api.edumentor.kz/api/v1/t/exams/e1a2b3c4-.../sections \
-H "Authorization: Bearer <token>" \
-H "X-Tenant-Subdomain: acme" \
-H "Content-Type: application/json" \
-d '{ "quiz_id": "9d1f...", "title": "Часть A", "weight": 40 }'Пример ответа
{
"success": true,
"data": { "id": "5ec70001-...", "quiz_id": "9d1f...", "title": "Часть A", "position": 1, "weight": 40 }
}Ошибки
| Код | error | Когда |
|---|---|---|
| 400 | invalid_weight | Вес вне диапазона 1–1000. |
| 404 | exam_not_found | Экзамен не найден. |
/api/v1/t/exams/:id/sections/:section_idОбновить секцию
Меняет тест, название или вес секции.
Параметры пути
| Параметр | Тип | Описание |
|---|---|---|
idобяз. | uuid | ID экзамена. |
section_idобяз. | uuid | ID секции. |
Тело запроса
| Параметр | Тип | Описание |
|---|---|---|
quiz_idопц. | uuid | Новый тест секции. |
titleопц. | string | Новое название (1–255). |
weightопц. | integer | Новый вес (1–1000). |
Пример запроса
curl -X PATCH https://api.edumentor.kz/api/v1/t/exams/e1a2b3c4-.../sections/5ec70001-... \
-H "Authorization: Bearer <token>" \
-H "X-Tenant-Subdomain: acme" \
-H "Content-Type: application/json" \
-d '{ "weight": 60 }'Пример ответа
{
"success": true,
"data": { "id": "5ec70001-...", "quiz_id": "9d1f...", "title": "Часть A", "position": 1, "weight": 60 }
}Ошибки
| Код | error | Когда |
|---|---|---|
| 404 | exam_section_not_found | Секция не найдена. |
/api/v1/t/exams/:id/sections/:section_idУдалить секцию
Удаляет секцию из экзамена.
Параметры пути
| Параметр | Тип | Описание |
|---|---|---|
idобяз. | uuid | ID экзамена. |
section_idобяз. | uuid | ID секции. |
Пример запроса
curl -X DELETE https://api.edumentor.kz/api/v1/t/exams/e1a2b3c4-.../sections/5ec70001-... \
-H "Authorization: Bearer <token>" \
-H "X-Tenant-Subdomain: acme"Пример ответа
{ "success": true, "data": { "deleted": true } }Ошибки
| Код | error | Когда |
|---|---|---|
| 404 | exam_section_not_found | Секция не найдена. |
/api/v1/t/exams/:id/sections/reorderИзменить порядок секций
Задаёт новый порядок секций. Список должен содержать все ID секций экзамена без повторов.
Параметры пути
| Параметр | Тип | Описание |
|---|---|---|
idобяз. | uuid | ID экзамена. |
Тело запроса
| Параметр | Тип | Описание |
|---|---|---|
section_idsобяз. | string[] | ID секций в нужном порядке (минимум 1). |
Пример запроса
curl -X POST https://api.edumentor.kz/api/v1/t/exams/e1a2b3c4-.../sections/reorder \
-H "Authorization: Bearer <token>" \
-H "X-Tenant-Subdomain: acme" \
-H "Content-Type: application/json" \
-d '{ "section_ids": ["5ec70002-...", "5ec70001-..."] }'Пример ответа
{ "success": true, "data": { "reordered": true } }Ошибки
| Код | error | Когда |
|---|---|---|
| 409 | reorder_duplicate | В списке есть повторяющиеся ID. |
| 409 | reorder_incomplete | Список не содержит все секции экзамена. |
/api/v1/t/exams/:id/analyticsАналитика по экзамену
Сводная статистика попыток: количество, средний балл, доля сдавших и пр.
Куратор без расширенных прав видит аналитику только по своим ученикам.
Параметры пути
| Параметр | Тип | Описание |
|---|---|---|
idобяз. | uuid | ID экзамена. |
Query-параметры
| Параметр | Тип | Описание |
|---|---|---|
include_admin_attemptsопц. | boolean | Включить попытки админов/кураторов в агрегаты. По умолчанию только ученики.Пример: true |
Пример запроса
curl "https://api.edumentor.kz/api/v1/t/exams/e1a2b3c4-.../analytics" \
-H "Authorization: Bearer <token>" \
-H "X-Tenant-Subdomain: acme"Пример ответа
{
"success": true,
"data": { "attempt_count": 42, "avg_score_pct": 64.5, "passed_count": 28 }
}Ошибки
| Код | error | Когда |
|---|---|---|
| 404 | exam_not_found | Экзамен не найден. |
/api/v1/t/exam-attemptsНачать попытку экзамена
Создаёт попытку прохождения опубликованного экзамена и запускает общий таймер.
Стартовать попытку может любой авторизованный участник; роль фиксируется в actor_role и влияет на фильтрацию аналитики.
Тело запроса
| Параметр | Тип | Описание |
|---|---|---|
exam_idобяз. | uuid | ID экзамена. |
Пример запроса
curl -X POST https://api.edumentor.kz/api/v1/t/exam-attempts \
-H "Authorization: Bearer <token>" \
-H "X-Tenant-Subdomain: acme" \
-H "Content-Type: application/json" \
-d '{ "exam_id": "e1a2b3c4-..." }'Пример ответа
{
"success": true,
"data": {
"id": "a77e0001-...",
"exam_id": "e1a2b3c4-...",
"user_id": "8f3b1c2a-...",
"status": "in_progress",
"actor_role": "student",
"started_at": "2026-05-21T09:00:00Z",
"deadline_at": "2026-05-21T13:00:00Z",
"section_attempts": []
}
}Ошибки
| Код | error | Когда |
|---|---|---|
| 404 | exam_not_found | Экзамен не найден. |
| 400 | exam_not_published | Экзамен ещё не опубликован. |
| 409 | max_exam_attempts_reached | Достигнут лимит попыток. |
/api/v1/t/exam-attempts/activeАктивная попытка
Возвращает незавершённую попытку текущего пользователя по экзамену (для возобновления).
Query-параметры
| Параметр | Тип | Описание |
|---|---|---|
exam_idобяз. | uuid | ID экзамена. |
Пример запроса
curl "https://api.edumentor.kz/api/v1/t/exam-attempts/active?exam_id=e1a2b3c4-..." \
-H "Authorization: Bearer <token>" \
-H "X-Tenant-Subdomain: acme"Пример ответа
{
"success": true,
"data": { "id": "a77e0001-...", "exam_id": "e1a2b3c4-...", "status": "in_progress" }
}Ошибки
| Код | error | Когда |
|---|---|---|
| 404 | exam_attempt_not_found | Активной попытки нет. |
/api/v1/t/exam-attempts/:idПопытка экзамена
Состояние попытки. Владелец читает свою попытку; куратор/админ — попытки своих учеников.
Не владельцу и не управляющему попытка отдаётся как 404 exam_attempt_not_found (без раскрытия существования).
Параметры пути
| Параметр | Тип | Описание |
|---|---|---|
idобяз. | uuid | ID попытки. |
Пример запроса
curl https://api.edumentor.kz/api/v1/t/exam-attempts/a77e0001-... \
-H "Authorization: Bearer <token>" \
-H "X-Tenant-Subdomain: acme"Пример ответа
{
"success": true,
"data": {
"id": "a77e0001-...",
"exam_id": "e1a2b3c4-...",
"status": "submitted",
"final_score": "72.00",
"final_max_score": "100.00",
"final_score_pct": "72.00",
"passed": true,
"section_attempts": [
{ "id": "5a000001-...", "exam_section_id": "5ec70001-...", "quiz_attempt_id": "qa00...", "position": 1 }
]
}
}Ошибки
| Код | error | Когда |
|---|---|---|
| 404 | exam_attempt_not_found | Попытка не найдена или недоступна. |
/api/v1/t/exam-attempts/:id/goПерейти к секции
Открывает указанную секцию и возвращает её содержимое для ученика. quiz — это студенческий вид теста (StudentQuizView): у вопросов есть points, но НЕТ правильных ответов (is_correct/correct_text убраны). Для вопросов matching правый столбец перемешан и токенизирован — у каждой опции match_rights[].id это одноразовый токен, а не реальный id (match_lefts[].id — реальные). Ответ matching отправляется как { pairs: [{ left_id, right_id }] }, где right_id — этот токен.
Раньше эндпоинт отдавал «сырой» QuizView с реальными id и ключом ответов — это и раскрывало верные ответы, и приводило к тому, что вопросы matching в экзамене всегда оценивались в 0 (грейдер ждёт токены). Теперь секция отдаёт тот же токенизированный студенческий вид, что и обычный тест, поэтому matching оценивается корректно.
Параметры пути
| Параметр | Тип | Описание |
|---|---|---|
idобяз. | uuid | ID попытки. |
Тело запроса
| Параметр | Тип | Описание |
|---|---|---|
section_indexобяз. | integer | Индекс секции, начиная с 0. |
Пример запроса
curl -X POST https://api.edumentor.kz/api/v1/t/exam-attempts/a77e0001-.../go \
-H "Authorization: Bearer <token>" \
-H "X-Tenant-Subdomain: acme" \
-H "Content-Type: application/json" \
-d '{ "section_index": 0 }'Пример ответа
{
"success": true,
"data": {
"section": { "id": "5ec70001-...", "title": "Часть A", "position": 1 },
"quiz": {
"id": "9d1f...",
"title": "Часть A",
"questions": [
{
"id": "q1...",
"type": "matching",
"prompt": "Сопоставьте страну и столицу",
"position": 0,
"points": "2.00",
"answers": [],
"match_lefts": [{ "id": "m1...", "text": "Франция" }],
"match_rights": [{ "id": "tok-9f3a...", "text": "Париж" }]
}
]
},
"deadline_at": "2026-05-21T13:00:00Z"
}
}Ошибки
| Код | error | Когда |
|---|---|---|
| 400 | invalid_section_index | Некорректный индекс секции. |
| 409 | exam_attempt_already_submitted | Попытка уже завершена. |
/api/v1/t/exam-attempts/:id/sections/:n/answersСохранить ответы секции
Сохраняет (мержит) ответы в указанной секции. Формат тела совпадает с прохождением обычного теста.
Параметры пути
| Параметр | Тип | Описание |
|---|---|---|
idобяз. | uuid | ID попытки. |
nобяз. | integer | Индекс секции, начиная с 0. |
Тело запроса
| Параметр | Тип | Описание |
|---|---|---|
answersобяз. | object[] | Массив ответов вида { question_id, payload }; payload зависит от типа вопроса. |
Пример запроса
curl -X POST https://api.edumentor.kz/api/v1/t/exam-attempts/a77e0001-.../sections/0/answers \
-H "Authorization: Bearer <token>" \
-H "X-Tenant-Subdomain: acme" \
-H "Content-Type: application/json" \
-d '{ "answers": [ { "question_id": "q1...", "payload": { "choice_id": "c2..." } } ] }'Пример ответа
{ "success": true, "data": { "id": "a77e0001-...", "status": "in_progress" } }Ошибки
| Код | error | Когда |
|---|---|---|
| 404 | exam_section_not_found | Секция не найдена. |
| 409 | exam_attempt_already_submitted | Попытка уже завершена. |
/api/v1/t/exam-attempts/:id/submitЗавершить попытку
Завершает попытку, автоматически оценивает все секции и фиксирует итоговый балл. Тело запроса игнорируется.
Параметры пути
| Параметр | Тип | Описание |
|---|---|---|
idобяз. | uuid | ID попытки. |
Пример запроса
curl -X POST https://api.edumentor.kz/api/v1/t/exam-attempts/a77e0001-.../submit \
-H "Authorization: Bearer <token>" \
-H "X-Tenant-Subdomain: acme"Пример ответа
{
"success": true,
"data": {
"id": "a77e0001-...",
"status": "submitted",
"final_score": "72.00",
"final_max_score": "100.00",
"final_score_pct": "72.00",
"passed": true,
"submitted_at": "2026-05-21T11:30:00Z"
}
}Ошибки
| Код | error | Когда |
|---|---|---|
| 404 | exam_attempt_not_found | Попытка не найдена. |
| 409 | exam_attempt_already_submitted | Попытка уже завершена. |