API для школ
Заводите классы и учеников из своей системы, забирайте логины, пароли и статистику занятий к себе — в журнал, в отчёт, в родительский чат. Всё то же, что преподаватель делает руками в кабинете, только из программы.
Что это и зачем
CodeWar — бесплатный тренажёр слепой печати. У преподавателя есть кабинет: классы, ученики с логинами и паролями, задания и журнал. API даёт то же самое программе — чтобы не переписывать тридцать логинов руками и не открывать сайт каждый раз, когда завуч просит отчёт.
Завести класс за секунду
Отправляете список имён — получаете логины и пароли. Почта ученикам не нужна, регистрация тоже.
Пароли под рукой на уроке
Пароль ученика виден до тех пор, пока он не сменит его сам. Забрать список можно в любой момент.
Статистика в свой журнал
Скорость, точность, пройденные уроки, дни занятий — по ученику, по классу, за любой период.
Три шага до первого запроса
- Получите роль преподавателя Если её ещё нет — заявка в кабинете, раздел «Работа с классом». Обычно отвечаем в тот же день.
- Попросите доступ к API Кабинет → Класс → API для школы → опишите, что хотите автоматизировать. Доступ открывается вручную: так мы понимаем, какая нагрузка нас ждёт.
- Создайте ключ и проверьте его Ключ показывается один раз — сразу сохраните его туда, откуда его возьмёт ваша программа. Первый запрос:
curl -H "Authorization: Bearer cw_live_ВАШ_КЛЮЧ" \
https://codewar.ru/api/v1/me
<?php
$ch = curl_init('https://codewar.ru/api/v1/me');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('CODEWAR_KEY')],
]);
$response = json_decode(curl_exec($ch), true);
print_r($response['data']);
import os, requests
r = requests.get(
"https://codewar.ru/api/v1/me",
headers={"Authorization": "Bearer " + os.environ["CODEWAR_KEY"]},
timeout=30,
)
print(r.json()["data"])
В ответ придёт вот это:
{
"data": {
"teacher": { "id": 1204, "name": "Анна Сергеевна" },
"key": {
"name": "Школьный сервер",
"prefix": "cw_live_7fa2c1d3",
"scope": "write",
"can_write": true,
"created_at": "2026-09-13T10:04:11+03:00"
},
"limits": {
"requests_per_minute": 120,
"heavy_requests_per_minute": 10,
"max_classes": 10,
"max_students_per_class": 50,
"classes_used": 2,
"students_total": 47
}
}
}
401 unauthorized — ключ не тот или
отозван. Если 403 api_disabled — доступ ещё не
открыт или его выключили. Разбор всех ошибок —
ниже.
Ключ и права
-
Ключ передаётся заголовком
Authorization: Bearer cw_live_…. В адресной строке его передавать нельзя, и мы его там не читаем: в адресе он осел бы в логах вашего сервера, наших и любого прокси между ними. - Ключ показывается один раз, при создании. У нас хранится только его хеш, поэтому «покажите ещё раз» невозможно технически. Потеряли — отзовите и создайте новый.
- Действующих ключей может быть два. Второй нужен, чтобы поменять ключ без простоя: создали новый, переключили программу, отозвали старый.
- Права бывают двух уровней. Чтение — только забирать данные. Чтение и запись — ещё и заводить учеников, создавать классы, выдавать задания, сбрасывать пароли.
- Отзыв ключа действует сразу. Выключение доступа администратором отзывает все ключи разом.
- Каждый запрос попадает в журнал, который преподаватель видит у себя в разделе API: метод, адрес, ответ, время. По нему удобно отлаживать интеграцию, не заглядывая в свои логи.
Границы и пределы
Что видит ключ
-
Классы, где преподаватель — владелец. Чужие классы не
видны и по прямой ссылке: ответ
404, тот же самый, что на несуществующий класс. - Учеников этих классов. Прогресс, попытки набора, дни занятий — только их.
-
Временный пароль — только у своих. Ученик, которого
завёл этот преподаватель и который ещё не менял пароль, —
пароль виден. Ученик, пришедший по ссылке-приглашению со
своим аккаунтом, — пароля нет никогда, даже если он в базе
есть. Такой ученик помечен
"managed": false. -
Пароль пропадает, как только ученик задаст свой в личном
кабинете: у нас остаётся только хеш, и показывать больше
нечего. В ответе поле тогда равно
null. - Ничего, кроме этого. Ни других преподавателей, ни чужих учеников, ни общих рейтингов, ни почты — даже своих учеников: если ученик указал почту сам, она его, а не класса.
Сколько чего можно
учеников в классе. Место преподавателя в классе тоже занято — оно одно из этих 50.
действующих классов у преподавателя. Архивные не считаются, но вернуть класс из архива сверх потолка нельзя.
запросов в минуту на ключ. Отчёт и выгрузка считаются тяжёлыми — их 10 в минуту.
Свободные места в классе видны в поле free_seats.
Когда мест не хватает, лишние ученики не заводятся, а попадают
в meta.skipped с причиной — молча их никто не
потеряет.
Превысили частоту — придёт 429 и заголовок
Retry-After с числом секунд. Подождите столько и
повторите; готовый PHP-класс делает это сам.
Формат ответа и ошибки
Адрес API — https://codewar.ru/api/v1. Версия в пути не
меняется: внутри v1 мы можем добавлять поля и
ручки, но не убирать и не переименовывать. Ваш разбор ответа
не сломается от нашей правки.
Успех — всегда data, при необходимости meta:
{
"data": [ { "id": 31, "name": "7Б информатика" } ],
"meta": { "page": 1, "per_page": 50, "total": 24, "pages": 1 }
}
Ошибка — всегда error с машинным кодом:
{
"error": {
"code": "validation_failed",
"message": "Проверьте параметры запроса.",
"fields": { "names": ["Поле names обязательно."] }
}
}
Разбирайте ошибки по code, а не по тексту: текст
мы можем переписать, код — нет.
| Код | HTTP | Что случилось и что делать |
|---|---|---|
unauthorized | 401 | Ключа нет, он неверный или отозван. Проверьте заголовок Authorization. |
api_disabled | 403 | Доступ к API у этого преподавателя закрыт или роль снята. Напишите в поддержку. |
forbidden_scope | 403 | Ключу не хватает прав на запись. Попросите права write. |
not_found | 404 | Не ваш класс, не ваш ученик или такого id вообще нет. Разницы между этими случаями нет намеренно. |
conflict | 409 | Действие невозможно: кончились места, класс в архиве, ученик уже занимался и его нельзя удалить. Текст объясняет, что именно. |
validation_failed | 422 | Не те параметры. Разбор по полям — в fields. |
rate_limited | 429 | Слишком часто. Подождите столько секунд, сколько сказано в Retry-After. |
server_error | 500 | Сломалось у нас. В ответе будет request_id — назовите его в поддержке, по нему мы найдём, что произошло. |
В каждом ответе есть заголовки X-RateLimit-Limit и
X-RateLimit-Remaining — сколько запросов осталось
в текущей минуте.
Классы
{
"data": [
{
"id": 31,
"name": "7Б информатика",
"students": 24,
"free_seats": 25,
"max_students": 50,
"archived": false,
"invite_enabled": true,
"invite_hash": "a1b2c3d4e5",
"target_cpm": 150,
"target_accuracy": 95,
"created_at": "2026-09-01T09:12:00+03:00"
}
],
"meta": { "total": 1 }
}
students — детей в классе; преподаватель туда не
входит. free_seats — сколько ещё влезет, и
считается с учётом места преподавателя. Не пугайтесь, что
students + free_seats на единицу меньше
max_students: это и есть его место.
| Параметр | Что это |
|---|---|
name обяз. |
Название, 2–100 знаков. |
target_cpm |
Цель по скорости, знаков в минуту: 20–1000. |
target_accuracy |
Цель по точности, проценты: 50–100. |
curl -X POST https://codewar.ru/api/v1/classes \
-H "Authorization: Bearer $CODEWAR_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "7Б информатика", "target_cpm": 150}'
<?php
$class = $cw->createClass('7Б информатика', 150, 95);
echo $class['id'], ' свободно мест: ', $class['free_seats'];
import os, requests
BASE = "https://codewar.ru/api/v1"
HEAD = {"Authorization": "Bearer " + os.environ["CODEWAR_KEY"]}
r = requests.post(
BASE + "/classes",
headers=HEAD,
json={"name": "7Б информатика", "target_cpm": 150},
timeout=30,
)
klass = r.json()["data"]
name, target_cpm,
target_accuracy,
invite_enabled (ссылка-приглашение),
archived (убрать в архив или вернуть).
Ученики
page, per_page (до 100).
{
"data": [
{
"id": 8801,
"name": "Иванов Пётр",
"login": "petr-ivanov-7b",
"managed": true,
"temp_password": "kesu-9142",
"level": 3,
"must_change_password": false,
"created_at": "2026-09-01T09:14:02+03:00",
"last_action_at": "2026-09-12T14:31:55+03:00"
},
{
"id": 8814,
"name": "Смирнова Лена",
"login": "lena_smirnova",
"managed": false,
"level": 7,
"must_change_password": false,
"created_at": "2025-11-04T18:02:10+03:00",
"last_action_at": "2026-09-12T19:40:00+03:00"
}
],
"meta": { "page": 1, "per_page": 50, "total": 24, "pages": 1 }
}
У второго ученика поля temp_password нет вовсе:
он пришёл по ссылке со своим аккаунтом
("managed": false). Такого ученика нельзя
переименовать, удалить или сбросить ему пароль — только
исключить из класса.
| Параметр | Что это |
|---|---|
names обяз. |
Массив имён. Имя — от 2 до 80 знаков, хотя бы одна буква. Годится и «7Б-03», если фамилии в чужую систему отдавать не хотите. |
curl -X POST https://codewar.ru/api/v1/classes/31/students \
-H "Authorization: Bearer $CODEWAR_KEY" \
-H "Content-Type: application/json" \
-d '{"names": ["Иванов Пётр", "Петрова Анна", "7Б-03"]}'
<?php
$result = $cw->addStudents(31, ['Иванов Пётр', 'Петрова Анна', '7Б-03']);
foreach ($result['data'] as $student) {
printf("%-22s %-18s %s\n", $student['name'], $student['login'], $student['password']);
}
// Смотреть обязательно: здесь те, кого не завели, и почему
foreach ($result['meta']['skipped'] as $skipped) {
echo 'пропущен ', $skipped['name'], ': ', $skipped['reason'], PHP_EOL;
}
r = requests.post(
BASE + "/classes/31/students",
headers=HEAD,
json={"names": ["Иванов Пётр", "Петрова Анна", "7Б-03"]},
timeout=30,
)
body = r.json()
for s in body["data"]:
print(s["name"], s["login"], s["password"])
for s in body["meta"]["skipped"]:
print("пропущен", s["name"], s["reason"])
{
"data": [
{ "id": 8801, "name": "Иванов Пётр", "login": "petr-ivanov-7b", "password": "kesu-9142" },
{ "id": 8802, "name": "Петрова Анна", "login": "anna-petrova-7b", "password": "voro-3318" }
],
"meta": {
"created": 2,
"skipped": [
{ "name": "7Б-03", "reason": "В классе кончились свободные места" }
],
"free_seats": 0
}
}
meta.skipped. «Создано 28 из 30»
без разбора причин — это двое детей, которые придут на урок
без пароля, а вы об этом узнаете от них.
login и
password. Только для своих
(managed: true).
name) или перевести в другой
свой класс (class_id). Перевод не сработает,
если в новом классе нет мест.
409: одним запросом стирать
историю занятий живого человека нельзя. Дальше его можно
лишь исключить из класса.
Статистика
Всё считается тем же кодом, что рисует страницы кабинета. Значит, цифры в вашем журнале и в нашем совпадают всегда — и спорить о том, чей отчёт правильный, не придётся.
{
"data": {
"student": { "id": 8801, "name": "Иванов Пётр", "login": "petr-ivanov-7b", "managed": true, "level": 3 },
"lessons_done": 14,
"cpm": 148,
"accuracy": 96.4,
"active_days": 9,
"minutes": 212,
"best_speed_wpm": 34,
"courses": [
{ "name": "Русская раскладка", "lessons_done": 14, "lessons_total": 60, "percent": 23, "cpm": 148 }
],
"last_day": "2026-09-12"
}
}
| Параметр | Что это |
|---|---|
from, to | Даты ГГГГ-ММ-ДД включительно. Без них — за всё время. |
page, per_page | Страница и размер, до 100. |
{
"id": 918233,
"lesson_id": 412,
"lesson": "Буквы О и Л",
"course": "Русская раскладка",
"cpm": 152,
"wpm": 30,
"accuracy": 97.2,
"errors": 4,
"seconds": 96,
"completed": true,
"at": "2026-09-12T14:31:55+03:00"
}
minutes,
lessons, trainings,
games, speed_tests,
arena. Параметры from и
to те же.
days дней (по
умолчанию 30): средние по классу, разбивка по неделям и
строка на каждого ученика с пометкой, растёт он или стоит.
{
"data": {
"class": { "id": 31, "name": "7Б информатика" },
"period_days": 30,
"from": "14.08.2026",
"summary": {
"students": 24, "active": 21, "grown": 15, "stalled": 4, "idle": 2, "never": 3,
"passed": 11, "cpm": 141, "delta": 23, "accuracy": 95.8, "lessons": 218, "minutes": 3140
},
"weeks": [ { "week": "14.08", "active": 18, "cpm": 118 } ],
"target": { "cpm": 150, "accuracy": 95 },
"students": [
{
"id": 8801, "name": "Иванов Пётр", "login": "petr-ivanov-7b",
"lessons": 14, "attempts": 41, "cpm": 148, "accuracy": 96.4,
"delta": 31, "days": 9, "minutes": 212,
"verdict": "ok", "state": "grown"
}
]
}
}
state: grown — скорость выросла,
stalled — занимается, но стоит на месте,
idle — перестал заниматься,
never — не начинал.
verdict сравнивает с целью класса:
above, ok, below.
header — заголовки колонок,
rows — строки. Разбирать CSV не нужно.
| Параметр | Что это |
|---|---|
what |
students (по умолчанию) — строка на ученика. tests — тесты скорости, по строке на попытку. |
columns |
Через запятую: who, course, typing, tests, class, visits. Без параметра — все. |
from, to |
Даты ГГГГ-ММ-ДД. |
Задания
Задание — это ссылка на урок или курс плюс срок и пороги. Отдельной механики у него нет: ученик проходит обычный урок, ничего не зная про задание, а «сдано» выводится из его результатов. Поэтому ручки «отметить сдано» нет и быть не может — отметка не хранится, она считается.
state, cpm,
accuracy, at.
| Параметр | Что это |
|---|---|
kind обяз. | lesson или course. |
target_id обяз. | Идентификатор урока или курса. Его видно в адресе урока в кабинете. |
due_at | Срок сдачи, дата или дата со временем. |
min_cpm | Порог скорости для зачёта. |
max_errors | Сколько ошибок допустимо. |
student_ids | Кому именно. Пусто — всему классу, и тогда задание получат и те, кого заведут позже. |
Готовый класс для PHP
Один файл, ни одной зависимости, PHP 7.4 и выше. Скачайте, положите к себе, подключите — и не думайте про заголовки, разбор ошибок и постраничные ответы.
В архиве CodeWarClient.php, рабочий
example.php и короткий README.txt.
<?php
require __DIR__ . '/CodeWarClient.php';
// Ключ — из переменной окружения, а не из кода: код попадёт в репозиторий
$cw = new CodeWarClient(getenv('CODEWAR_KEY'));
try {
$who = $cw->me();
echo "Ключ работает: ", $who['teacher']['name'], PHP_EOL;
$class = $cw->createClass('7Б информатика', 150, 95);
$result = $cw->addStudents($class['id'], ['Иванов Пётр', 'Петрова Анна']);
foreach ($result['data'] as $student) {
echo $student['login'], "\t", $student['password'], PHP_EOL;
}
} catch (CodeWarApiException $e) {
// Разбирать по коду, а не по тексту
if ($e->errorCode === 'forbidden_scope') {
exit("Ключу не хватает прав на запись.\n");
}
throw $e;
}
Что он делает за вас:
- подставляет заголовок
Authorization— ключ никогда не уходит в адрес; - бросает
CodeWarApiExceptionс полямиerrorCode,status,fields,requestId; - сам ждёт и повторяет запрос, упёршийся в
429; - листает страницы:
allStudents()иallSessions()возвращают всё сразу.
Рецепты
Первое сентября: завести всех разом
<?php
$cw = new CodeWarClient(getenv('CODEWAR_KEY'));
// Имена берутся из вашей системы. Фамилии отдавать не обязательно: сгодятся
// инициалы или номера по журналу — тренажёру фамилии ни к чему
$byClass = [
'7А информатика' => ['Иванов Пётр', 'Петрова Анна'],
'7Б информатика' => ['Сидоров Илья'],
];
$cards = fopen('passwords.csv', 'w');
fprintf($cards, "\xEF\xBB\xBF"); // BOM, иначе Excel испортит кириллицу
fputcsv($cards, ['Класс', 'Имя', 'Логин', 'Пароль'], ';');
foreach ($byClass as $name => $names) {
$class = $cw->createClass($name);
$result = $cw->addStudents($class['id'], $names);
foreach ($result['data'] as $student) {
fputcsv($cards, [$name, $student['name'], $student['login'], $student['password']], ';');
}
foreach ($result['meta']['skipped'] as $skipped) {
fwrite(STDERR, 'ПРОПУЩЕН ' . $skipped['name'] . ': ' . $skipped['reason'] . PHP_EOL);
}
}
fclose($cards);
Пароли к уроку: список для распечатки
Пароль лежит у нас открытым ровно до тех пор, пока ученик не задаст свой. Поэтому забрать список можно в любой момент — не только сразу после создания.
<?php
foreach ($cw->allStudents($classId) as $student) {
printf(
"%-24s %-18s %s\n",
$student['name'],
$student['login'],
$student['temp_password'] ?? '— сменил сам'
);
}
Еженедельная выгрузка в журнал
<?php
$from = date('Y-m-d', strtotime('-7 days'));
$to = date('Y-m-d');
foreach ($cw->classes() as $class) {
if ($class['archived']) {
continue;
}
$table = $cw->export($class['id'], 'students', $from, $to);
// $table['header'] — названия колонок, $table['rows'] — строки
foreach ($table['rows'] as $row) {
$journal->save($class['name'], array_combine($table['header'], $row));
}
}
Табель посещаемости
<?php
$days = $cw->activity($studentId, '2026-09-01', '2026-09-30');
foreach ($days as $day) {
echo $day['date'], ': ', $day['minutes'], ' мин, уроков ', $day['lessons'], PHP_EOL;
}
Частые вопросы
- Можно ли добавить наш сервер в исключения по IP?
- Не нужно: с ключом вы и так делаете всё, что вам нужно, и никакие проверки вам не мешают. Привязка к IP только усложнила бы вам жизнь — адрес школьного провайдера меняется чаще, чем кажется.
- Почему пароль ученика хранится открытым?
- Потому что иначе его нельзя отдать на уроке. Это временный пароль, который сгенерировали мы, и живёт он ровно до того момента, когда ученик задаст свой — тогда остаётся только хеш, и пароля не видит уже никто, включая нас. Пароли, которые люди придумали сами, не хранятся открытыми никогда.
- Можно ли получить пароли учеников, которых завёл не я?
-
Нет, и это не настройка. Ученик, пришедший в класс по
ссылке, владеет своим аккаунтом сам: в ответе у него
"managed": falseи поля с паролем нет вовсе. - А если школе нужно больше 50 учеников в классе?
- Заведите второй класс: их можно до 10. Если и этого мало — напишите в поддержку, потолки настраиваются, но мы хотим знать, кому и зачем.
- Ключ утёк. Что делать?
- Отозвать его в кабинете: раздел API, кнопка «Отозвать». Запросы с ним перестанут проходить сразу. Потом создайте новый и посмотрите журнал запросов — там видно, что с ним успели сделать.
- Можно ли звать API прямо из браузера, со страницы?
- Нет. Ключ в JavaScript виден любому, кто откроет страницу, — это то же самое, что раздать его. Зовите API со своего сервера.
- Что будет, если наш скрипт сойдёт с ума?
-
Упрётся в предел частоты и получит
429. Данные при этом целы: заводить учеников сверх мест в классе, а классы сверх потолка API не даёт — это те же ограничения, что и в кабинете. - Вы поменяете формат ответа?
-
Внутри
v1— нет. Поля и ручки могут добавляться, но существующие не убираются и не переименовываются. Если понадобится ломающее изменение, появитсяv2, аv1продолжит работать. - Как получить доступ?
- Кабинет → Класс → API для школы → заявка. Если аккаунта ещё нет — сначала регистрация, потом заявка на роль преподавателя.