API для разработчиков
Публичный REST-API Naminori — стройте свои клиенты повторений, плагины Anki/Obsidian, виджеты, скрипты майнинга. Все данные — только ваши (владельца токена); лукап и озвучка — общие сервисы.
Авторизация
Все запросы требуют персональный токен в заголовке. Токен создаётся после входа в аккаунт — в настройках → «API-токены». Он даёт доступ ТОЛЬКО к данным вашего аккаунта — храните его как пароль, при утечке отзовите в настройках.
Authorization: Bearer nmn_xxxxxxxxxxxxxxxxxxxxxxxx
Альтернативно — заголовок X-Naminori-Token. CORS открыт (авторизация по токену, не по cookie), так что API можно дёргать из браузерных расширений и локальных приложений.
Формат и лимиты
- Ответы — JSON (кроме
/api/v1/audioи/api/v1/dicts/…— бинарные файлы). - Ошибка — это JSON
{ "error": "код", "message"?: "…", "details"?: {…} }: вerrorкороткий машинный код, вmessage— готовая русская фраза для показа человеку (там, где она есть), вdetails— разбор по полям при невалидном теле. - Коды:
400кривой запрос/тело ·401нет/неверный токен ·404не найдено или чужое ·409конфликт (например, удаление непустой колоды) ·413слишком длинный текст/большой файл ·415неподдерживаемый тип файла ·429превышен лимит ·500/502/503сбой на нашей стороне (озвучка, сборка словаря) — запрос можно повторить позже. - Неизвестное значение фильтра — это ошибка, а не «фильтра нет»:
/cards,/queueи/dueотвечают400на чужойtype,state,suspendedили неверный форматdeckId. - Лимиты — на пользователя, окно 1 минута. Чтение (
/me,/due,/queue,/cards,/lookup,/dict,/vocab/decks) — 240 запросов/мин. Запись:/reviewи/vocab/cards— 240/мин,/review/batch— 60/мин (до 500 оценок в каждом),/vocab/addи/audio— 120/мин,/learn-new, создание и правка колод,/media/store— 60/мин. У/cardsсsort=createdилиsort=frequencyсвой потолок — 60/мин: эти сортировки на каждую страницу перебирают все ваши карточки. У остальных ручек порог назван в их описании; чтение и запись одной ручки считаются отдельно. - Коды отказов, которые стоит различать в клиенте:
confirm required(400 — действие надо подтвердить: повторите сconfirmпосле вопроса человеку),nothing to update(400 — в теле нечего менять; так же отвечает и старый сервер, не знающий новых полей),unsupported image(400 — картинка не JPG, PNG или WebP по содержимому),too large(413),storage unavailable(503 — файл не сохранился, повторите позже),entry not found(404 — ручной записи времени нет),slug taken,already friends,marks full,favorites full(409) иorder mismatch(409 — список колод изменился на другом устройстве: перечитайте его и повторите, см.PUT /api/v1/vocab/decks/order). У сброса прогресса колодыconfirm— неtrue, а число карточек изcount. Готовая фраза для человека — вmessage. - Удаляют данные только ручки редактора личных колод (
POST /api/v1/vocab/cardsиDELETE /api/v1/vocab/decks/{id}) и удаление своего, по одной вещи: ручной записи времени, аватара и шапки, дружбы, ролика из «Смотреть позже». Массовые записи — сброс оформления и смена целевого уровня — только с"confirm": true, сброс прогресса колоды — сconfirm, равным числу карточек (его можно отменить в течение 30 дней). Удалить аккаунт или выполнить админ-операцию через API нельзя.
Что нового
13 октября 2026. Новое поле — только по параметру; изменился порядок обычной выдачи очереди, смысл одного блока сводки и расписание новых пунктов грамматики.
GET /api/v1/queue— карточки в изучении и переучивании, чей срок наступил, идут первыми (обычная выдача, слова и грамматика).learning=1добавляет им полеlearningDue— в обеих выдачах; порядок и курсорorder=stableпрежние. Карточку в изучении или переучивании со сроком дальше 20 минут очередь не отдаёт, а счётчики (/due,/overview, колоды) не считают, пока срок не подойдёт: раньше она приходила в любой час того же дня.POST /api/v1/reviewиPOST /api/v1/review/batch— новый пункт грамматики с оценкой 3 при первой встрече сразу уходит в повторение (как новое слово с 6 октября): срок считает FSRS, около двух дней при усвоении 90 %. Раньше он возвращался через десять минут. Условия те же, что у слов: шаги обучения по умолчанию и желаемое усвоение от 85 %. Сроки изintervals=1говорят то же.GET /api/v1/overview— СМЫСЛ ИЗМЕНИЛСЯ:deck.vocab,deck.totalCards,newCards,learnedCardsиnextDue.vocabсчитают только слова и импорт Anki; карточки тренажёров (тональный акцент, ономатопеи, кана) в них больше не входят.
Октябрь 2026, обновление приложений («Погружение», оформление, профиль, колоды). Только добавления: прежние поля и ответы не изменились, кроме смысла двух ключей трекера времени и четырёх правил колод — они в пунктах «СМЫСЛ ИЗМЕНИЛСЯ».
- Настройки и оформление:
GETиPATCH /api/v1/settings— оформлениеappearance(тема, вид тёмной, акцент, шрифт карточек, шапка, скрытые блоки главного, дневная цель и цель блока прогресса), набор «Галереи», общие поля ленты и дня, цели трекера по навыкам;POST /api/v1/settings/appearance/reset,PUT /api/v1/me/target-level. Разделы «Галерея шапок» и «Шрифты карточек» — статика с годовым кэшем. GET /api/v1/me—appearance,vocabTypeReading, поля профиля иautoTimeTracking.GET /api/v1/overview— щит серии вstreak,today.newCards,goal.dailyиgoal.progress.- Профиль и друзья:
PATCH /api/v1/me/profile,/api/v1/me/avatar,/api/v1/me/cover(шапки главного и профиля, свой файл в «Мои»),GET /api/v1/profiles/{slug},/api/v1/friends; рейтинг —GET /api/v1/leaderboard. Поиск с главного —GET /api/v1/search. - Трекер времени:
GET /api/v1/time/feed(лента по дням),GET /api/v1/time/stats,PATCHиDELETE /api/v1/time/entries/{id},PUT /api/v1/time/feed/marks,PUT /api/v1/time/auto; уPOST /api/v1/time/entries— поля листа «Сохранить сессию» иoverlapMs/countedMs, уGET /api/v1/time/summary—bySkill,skillGoalsMin,autoTimeTracking, у пульса —titleв теле иcountedв ответе. Руками теперь можно записать и «Карточки» (reviews), и пробники (jlpt); секундомер поверх автоматического времени его не удваивает. - YouTube:
GET /api/v1/youtube/picks(подборка по сложности),/api/v1/youtube/favorites(«Смотреть позже») иGET /api/v1/youtube/captions(субтитры ролика, как у майнинга на сайте). - Договор «навык + режим → вид» у ручных записей: запись
{"activity": "reading", "contactMode": "flow"}хранится какreading-free(подробно — в разделе «Время занятий»). - СМЫСЛ ИЗМЕНИЛСЯ:
byActivity.reviewsв итогах времени больше не включает грамматику — она вbyActivity.grammar. Суммы те же; клиент, который показывает только итог, ничего не заметит. - СМЫСЛ ИЗМЕНИЛСЯ: время тренажёра питча теперь считается —
byActivity.pronunciation, со дня выпуска, без пересчёта прошлого. Личные итоги и сводка дня (timeMs,minutesиsecPerCardуGET /api/v1/stats/today) с этого дня больше на это время; рейтинг, достижения и чужой профиль — нет. - Колоды: у
GET /api/v1/vocab/decksи вdecksуGET /api/v1/overview— вид (icon,color,lookCustom), ручной порядок (position), подпись (subtitle), свои настройки показа (options,effective— в том числе «по частотности» и «Вводить чтение»),progress,reviewDueCountи цифры дня (today,todayDue,secPerCard,etaMin; у списка — соstats=1). Новые ручкиPUT /api/v1/vocab/decks/orderиGET /api/v1/vocab/decks/{id}/stats, сброс прогресса колоды с отменой (forgetDeck,unforgetDeckуPOST /api/v1/vocab/cards),deckInfo=1уGET /api/v1/queue, ввод чтения у слов (раздел «Колоды: вид, порядок, настройки, статистика»). Курсорorder=stable— до 1280 знаков. - СМЫСЛ ИЗМЕНИЛСЯ: у
PUT /api/v1/vocab/decks/{id}полеlimits, которого нет в теле, больше не становитсяnull, а не трогается;description: nullочищает подпись. Клиент, который шлёт все три лимита, ничего не заметит. - СМЫСЛ ИЗМЕНИЛСЯ: новые слова в «Все колоды» набираются по колодам — в порядке списка колод, каждая своим порядком новых (раньше колоды без своего лимита перемешивались по дате добавления). Повторения и выдача по
deckId— как раньше. - СМЫСЛ ИЗМЕНИЛСЯ: свой лимит новых у колоды-родителя накрывает всю её ветку, когда учат родителя или «Все колоды», — подколоды со своими лимитами тоже (родитель 10, подколода 30 → не больше 10 в день на обе). Выбранная подколода (
deckId) родителей выше не видит — у неё свой лимит, как и раньше. У кого вложенных лимитов нет, число новых в день не изменилось. - СМЫСЛ ИЗМЕНИЛСЯ: пока идёт «Отпуск», к повторению у колод — нули (
dueCount,reviewDueCount,todayDue,etaMinвGET /api/v1/vocab/decksиdecksGET /api/v1/overview): очередь слов в отпуске и так пуста, а «созревшие» числа звали в пустую сессию.
6 октября 2026. Только добавления: прежние поля и ответы без новых параметров не изменились. Что изменилось в самом расписании — в последних пунктах.
GET /api/v1/cards— у карточки появилисьcreatedAt(когда слово добавлено),jitenRank(частотность),source(откуда слово: вид, подпись, ссылка),deck(колода с путём),lastReview,scheduledDaysиanki— слово импортированной карточки.- Там же — отбор по дате добавления (
since,until), карточки по списку (ids), сортировка (sort=due|created|frequency,dir) и курсор (cursor→hasMore,nextCursor), как у очереди. GET /api/v1/stats— удержаниеretention(сегодня, 7 и 30 дней), окноdays— до 365 (было 90).GET /api/v1/stats/today—passedиretentionза сегодня.GET /api/v1/lookup— полеjitenRankотдавалось и раньше, теперь оно описано.GET /api/v1/queue?intervals=1— сроки для кнопок оценки: у элемента появляетсяintervalsс подписямиagain,hard,good,easy(«<10 мин», «11 д», «1,5 мес»), у ответа —intervalsAt. Подписи верны до конца суток UTC, в которые посчитаны; точный рубеж —intervalsUntil(конец учебного дня человека, если он раньше).POST /api/v1/queue/intervals— свежие сроки для карточек, которые у клиента уже на руках, без перекачки очереди.POST /api/v1/reviewиPOST /api/v1/review/batch— с"intervals": trueв теле ответ несётinterval: назначенный срок теми же словами, что на кнопке. Не совпал с показанным — настройки расписания сменили посреди сессии, попросите сроки заново.POST /api/v1/review— формат ответа прежний, но назначенный срок больше не зависит от мгновения ответа: одна и та же оценка той же карточки в пределах суток UTC даёт один и тот жеdue(от момента ответа), и он совпадает с подписью изintervals. Раньше разброс интервала засевался миллисекундой ответа, и срок гулял на несколько дней.POST /api/v1/reviewиPOST /api/v1/review/batch— новое слово (и новая карточка из импорта Anki), которому с первой встречи поставили оценку 3, сразу уходит в повторение: срок считает FSRS — около двух дней при усвоении 90 % (по умолчанию), 3–5 дней при 85 %, вGET /api/v1/cardsу негоstatereview. Раньше оно возвращалось через десять минут. Так — только при желаемом усвоении от 85 % (у колоды со своим усвоением — по нему): ниже первый срок выходит неделей и больше, и слово, как раньше, возвращается через десять минут. Оценка 1 по-прежнему ведёт по шагам обучения; у грамматики и у тех, кто задал свои шаги обучения, ничего не изменилось. Оценки 1–4 принимаются как раньше.- Кнопки «Сложно» и «Легко» после проверки ответа в грамматике убраны из приложений Naminori (сайт, телефон, ПК): остаётся одна «Дальше». Для API ничего не изменилось:
POST /api/v1/reviewиPOST /api/v1/review/batchпринимаютratingот 1 до 4, а очередь сintervals=1по-прежнему отдаёт срокиhardиeasy.
Частотность слов
Частотность — это поле jitenRank: место слова в частотном списке, 1 — самое частое. Его отдают GET /api/v1/cards (у каждой карточки со словом) и GET /api/v1/lookup (у найденного слова) — число одно и то же, и это то же число, что сайт показывает значком «Частотность (Jiten): №N».
- Ранг считается по паре «написание + чтение»: 北【きた】 «север» стоит на 3 516-м месте, а 北【ペー】 из маджонга в список не входит вовсе.
- У лукапа числа есть у КАЖДОЙ группы
groups[]— у своей пары, а у самого ответа — по-прежнему у главного слова. Рядом —jitenKanaRank: место того же слова, записанного каной (только у слова с кандзи): у 有る【ある】jitenRank: 12756, аjitenKanaRank: 18— его почти всегда пишут каной. Сайт показывает второе число в самой метке, только когда каной слово встречается чаще. Нет числа — нет и поля. - Список обрезан на 100 000-м месте. Слова в нём нет — у карточки
jitenRank: null, у лукапа поля нет вовсе. Это значит «реже или список слова не знает», а не «самое редкое». - ПОРЯДОК НОВЫХ «ПО ЧАСТОТНОСТИ» (
newOrder: frequencyвoptionsколоды, см.GET /api/v1/vocab/decks) идёт по ДРУГОМУ рангу, не поjitenRank: по нашему частотному списку корпуса (MeCab, около 45 тысяч словоформ) — у слова курса и намайненного слова; у карточки импорта Anki — число из поля частотности заметки на момент импорта («1336», «JPDBv2: 1336», у HTML-списка — первый словарь; повторный импорт того же файла его обновляет), а нет поля, в нём 0 или заглушка Yomitan 9999999 «частоты нет» — ранг сопоставленного слова. 1 — самое частое; без ранга — в конце колоды, ничьи — по порядку колоды; дневной лимит новых берёт самые частые. Сам ранг в ответах не отдаётся. Курсорorder=stableу такой колоды несёт ранг в себе — он по-прежнему непрозрачный. - Источник — Jiten, частотный список «Global» (jiten.moe), лицензия CC BY-SA 4.0. Показываете ранг у себя — назовите источник и сохраните ту же лицензию для данных, производных от списка; полная атрибуция — на странице благодарностей.
Шрифты карточек
Японский шрифт карточки, который человек выбирает в оформлении (настройка cardFont), можно взять с нашего сайта — тогда карточка в вашем клиенте выглядит так же, как у нас. Имя файла — это значение настройки: klee-one (Klee One), shippori-mincho (Shippori Mincho) и noto-sans-jp (Noto Sans JP, шрифт по умолчанию). Noto Sans JP годится и для всего японского текста клиента: системный японский на Android — одно начертание, а этот файл даёт веса 100–900 одним куском на диапазон (те же куски, что у шрифта сайта).
<link rel="stylesheet" href="https://naminori.fun/fonts/card/klee-one.css">
…
.card-jp { font-family: "Klee One", "Noto Sans JP", sans-serif; }- Подключать только ссылкой
<link rel="stylesheet">. Адреса кусков шрифта в CSS относительные: текст CSS, вставленный в<style>, будет искать их у вас, а не у нас. - Шрифт нарезан на куски по
unicode-range: браузер скачивает только куски со знаками, которые есть на экране, и только нужные начертания (у Klee One — 400 и 600, у Shippori Mincho — 400, 500 и 700; у Noto Sans JP все веса в одном вариативном куске). Обычная фраза — 5–6 небольших кусков. - Запрос разрешён с любого источника (
Access-Control-Allow-Origin: *), так что шрифт подключается из расширения, приложения и со своего сайта. Если у вашей страницы есть Content-Security-Policy, добавьте наш адрес вstyle-srcиfont-src. Скрипт расширения, который рисует на чужой странице, живёт по её CSP — там ссылка может не загрузиться. - Адрес CSS постоянный — зашивайте его, а не адреса кусков. CSS кэшируется на сутки, куски — на год: у изменённого куска меняется имя.
- Лицензия — SIL Open Font License 1.1, тексты рядом:
/fonts/card/OFL-klee-one.txt,/fonts/card/OFL-shippori-mincho.txtи/fonts/card/OFL-noto-sans-jp.txt.
Галерея шапок
Набор картинок для шапки главного экрана и профиля приходит в GET /api/v1/settings полем appearance.gallery — и только там (в /api/v1/me и в ответах PATCH его нет). Список меняется выкладкой сайта: не зашивайте его в клиент, берите отсюда и кэшируйте — шапку на старте (до сети) рисуют по закэшированному набору. Выбор человека — appearance.homeArt (и cover профиля), меняется через /api/v1/me/cover; незнакомый id (картинку убрали) читается как «без арта».
{ "id": "festival-night", "title": "Ночь мацури", "kind": "photo",
"url": "/covers/gallery/festival-night.phone-3x.v1.webp", "width": 1236, "height": 451,
"thumbUrl": "/covers/gallery/festival-night.thumb.v1.webp",
"focus": { "x": 0.5, "y": 0.33 }, "focusProfile": { "x": 0.5, "y": 0.28 }, "minDim": 25,
"strips": { "phone2x": { "url": "…", "width": 824, "height": 301 },
"phone3x": { "url": "…", "width": 1236, "height": 451 },
"profile1x": { "url": "…", "width": 1152, "height": 240 },
"profile2x": { "url": "…", "width": 1536, "height": 320 } },
"full": { "url": "/covers/gallery/festival-night.full.v1.webp", "width": 1536, "height": 1024 } }
// градиент: только SVG (тянется на любую полосу) и css
{ "id": "grad-coral", "title": "Коралл", "kind": "gradient", "url": "/covers/gallery/grad-coral.v1.svg",
"thumbUrl": "/covers/gallery/grad-coral.v1.svg", "width": 412, "height": 150,
"focus": { "x": 0.5, "y": 0.5 }, "minDim": 0, "css": "linear-gradient(180deg, …)" }url— ГОТОВАЯ шапка: полоса телефона 3x, уже вырезанная по фокусу;width/height— размер именно её. Клиент, который знает толькоurl, показывает её фоном сcover— и кадр верный.thumbUrl— миниатюра для выбора.kind—photo,scene(сцена палитры сайта) илиgradient. У градиента нетstrips,fullиfocusProfile, зато естьcss— для клиента, который не рисует SVG.strips— готовые полосы под плотности: телефон 2x и 3x, профиль 1x (1152×240) и 2x (1536×320, вsrcset—1536w). Размеры берите из манифеста, а не зашивайте.focusиfocusProfile— доли ПОЛНОГО кадраfull, а не картинкиurl: «Обрезать» открываетfullи ставит рамку с центром вfocus(для полосы профиля — вfocusProfile).- Своя рамка: если
homeArt.crop(илиcover.cropпрофиля) неnull,urlуказывает на полный кадр, и рамкуcrop(доли этого кадра) клиент ОБЯЗАН наложить. Показать такойurlфоном без рамки — значит показать кадр 3 : 2 по центру, и главное в нём срежется. У своей загруженной картинки правило то же. minDim(0…80) — порог читаемости надписей поверх картинки. Вуаль =max(homeArtDim, minDim)— считает клиент; ползунок затемнения показываетhomeArtDimкак есть, сервер его не поднимает.- Новые поля элемента сверх
id,title,url,thumbUrl,width,height,focus— необязательные: клиент, который их не знает, рисуетurlкак раньше. - Файлы
/covers/gallery/…— статика с годовым кэшем (immutable: новая нарезка — новое имя,.v2.) иAccess-Control-Allow-Origin: *— картинку можно мерить холстом из приложения.
Профиль и статистика
/api/media/animateСобрать АНИМИРОВАННЫЙ кадр сцены из последовательности картинок (multipart/form-data, поле frames повторяется; 2–24 кадра, до 400 КБ каждый). Возвращает такой же относительный URL, как /api/media/store. Зачем отдельно: браузер анимированный AVIF кодировать не умеет — canvas.toBlob отдаёт один кадр. Зачем вообще: две секунды движения весят около 12 КБ, то есть дешевле статичного скриншота, а сцена по ним узнаётся.
curl -X POST https://naminori.fun/api/media/animate -H "Authorization: Bearer nmn_…" -F "frames=@f001.webp" -F "frames=@f002.webp" -F "frames=@f003.webp"
→ { "ok": true, "url": "/api/media/9f1c….avif", "bytes": 12043 }/api/v1/meПрофиль: имя, почта (email — почта владельца ключа: её видит только сам человек, в приложениях и расширении ею подписана строка аккаунта; чужую почту не отдаёт ни одна ручка — ни профили, ни рейтинг, ни друзья), целевой уровень, стрик, сколько выучено грамматики и слов, плюс общие настройки аккаунта — палитра, автозвук, микшер громкости (audioMixer: общая и три канала — voice озвучка, media звук из видео и карточек, trainers тренажёры; проценты 0–100, меняется через PATCH /api/v1/settings { audioMixer } целиком) и режим «в предложении» (vocabContextSentenceMode), свёртка окна словаря (dictSensesAll — «Показывать все значения сразу», см. GET /api/v1/settings). Режим «в предложении» нужен клиенту, у которого нет своего тумблера: оно говорит, хочет ли человек видеть слово внутри живой фразы (см. context=1 у /api/v1/queue) или голым. Заводить рядом вторую, клиентскую, настройку не надо — это один и тот же выбор. Здесь же едет aiKey — есть ли у человека сохранённый ключ ИИ: по нему решают, рисовать ли кнопку «Разбор ИИ» (см. /api/v1/ai/analyze). Ключа нет — кнопки быть не должно вовсе, а не «нажал и получил отказ»: ключ вводится только в настройках на сайте. Признак едет ИМЕННО ЗДЕСЬ, потому что профиль клиент читает и кэширует и так, а попап словаря открывают десятки раз за занятие — отдельная справка на каждый попап стоила бы десятков запросов ради одного бита. Самого ключа наружу не отдаёт ни одна ручка: к поставщику по нему ходит сервер. Ещё два общих числа — дневные лимиты нового: vocabPerDay (слова) и newPerDay (грамматика), те же, что «Новых слов словаря в день» и «Новых пунктов грамматики в день» на сайте; по ним сервер режет каждую порцию /api/v1/learn-new. Менять их можно тем же PATCH /api/v1/settings ({ "vocabPerDay": 30 } — целое 0…500). vocabPerDay — «Новых в день (для колод без своего лимита)»: его по очереди делят колоды без своего лимита, а колода со своим лимитом живёт по нему (правило и пример — у PUT /api/v1/vocab/decks/{id}). encountersRecord и encountersSeen — выключатели на странице «Где встречались слова»: запоминать ли встречи вообще и увиденное (seen) в частности; encountersRecord: false — не копите очередь POST /api/v1/encounters вовсе, encountersSeen: false — шлите только lookup. С редизайна (октябрь 2026) здесь же: appearance — всё оформление аккаунта с подставленными умолчаниями (тема, вид тёмной темы, акцент, палитра, анимация фона, шрифт карточек, шапка главного, скрытые блоки главного, дневная цель, цель блока прогресса; разбор полей — у GET /api/v1/settings, только без списка palettes и без набора «Галереи» gallery: /me читают на каждом старте, и оформление нужно до сети); vocabTypeReading — общий «Вводить чтение» (false — выключено; колода его перекрывает); профиль владельца — avatarUrl, profileSlug (ник, null — не задан), profileVisibility и materialsVisibility — кто видит профиль и материалы: all «Всем», friends «Друзьям», none «Никому» (прежние profilePublic и materialsPublic остались для старых клиентов — true, когда «Всем»), about («О себе»); autoTimeTracking — включено ли «Считать автоматически» в трекере времени (менять — PUT /api/v1/time/auto). lowerUncovered — сколько опубликованных пунктов грамматики НИЖЕ целевого уровня у человека без карточки (снесены «Уборкой», удалением или сбросом прогресса): «Учить новое» начнёт с них; больше нуля — предложите «Отметить недостающие (N)», это PUT /api/v1/me/target-level с тем же уровнем. В свободном режиме и на N5 — 0; старый сервер поля не отдаёт — тогда кнопки нет. upperAutoMarked и upperAutoMarkedRange — то же для уровня и ВЫШЕ: автоматические отметки, оставшиеся после понижения цели (см. PUT /api/v1/me/target-level, returnAutoMarked). Поля theme верхнего уровня здесь нет и не будет: тема — appearance.theme (старые приложения, увидев theme сверху, перекрасили бы всех). hintsSeen — увиденные подсказки на месте действия (п.7 пачки 12), как в GET /api/v1/settings. uiLang — язык интерфейса (ru — русский, ja — японский, null — не выбран: показывайте по-русски) и uiFurigana — фуригана японских надписей (always — всегда, above — только над знаками выше целевого уровня, never — нет); те же поля — в GET /api/v1/settings. Курс (объяснения, словарь) от языка интерфейса не меняется.
{ "name": "...", "email": "you@example.com", "targetLevel": "N4", "lowerUncovered": 0, "upperAutoMarked": 0, "upperAutoMarkedRange": null, "streak": 16, "grammarLearned": 461, "vocabLearned": 2182,
"palette": "sakura", "audioAutoplay": true, "vocabContextSentenceMode": true, "dictSensesAll": false, "newPerDay": 25, "vocabPerDay": 25, "aiKey": true,
"audioMixer": { "master": 100, "voice": 100, "media": 100, "trainers": 100 },
"encountersRecord": true, "encountersSeen": true, "vocabTypeReading": false,
"avatarUrl": "/api/avatar/….webp", "profileSlug": "flasik",
"profileVisibility": "all", "materialsVisibility": "friends", "profilePublic": true,
"about": "Готовлюсь к N1", "materialsPublic": false, "autoTimeTracking": true,
"appearance": { "look": "palette", "theme": "dark", "darkStyle": "oled", "uiTheme": "oled",
"uiAccent": 1, "palette": "sakura", "bgMotion": true, "cardFont": "noto-sans-jp",
"homeArt": { "source": "gallery", "id": "fuji-dawn", "crop": null,
"url": "/covers/gallery/fuji-dawn.phone-3x.v1.webp", "own": null },
"homeArtDim": 25, "homeHidden": ["heatmap"], "dailyGoal": 60,
"homeGoal": { "type": "jlpt", "id": "N4" } } }/api/v1/overviewВесь кабинет ОДНИМ запросом: профиль, стрик (текущий + рекорд), сегодняшний прогресс (сделано / осталось по типам), состояние колоды (всего / новые / изучено), активность за 7 дней, время ближайшей карточки и список личных колод. Собрано для Telegram-бота, но подходит любому клиенту, которому нужен «экран дня» без пяти запросов подряд. «Сегодня» здесь — ДЕНЬ ЧЕЛОВЕКА: у него в настройках есть час, в который приходит новая порция («Новый день начинается», по часам его часового пояса, по умолчанию 00:00), и до этого часа день ещё вчерашний. Границы дня отдаёт поле day — считать их на клиенте не нужно (пояс дня может не совпадать с поясом устройства, а новый час или пояс действуют не сразу — со следующей смены дня): key — дата дня, ею же помечены строки daily и по ней идёт серия; startsAt — когда день начался; endsAt — когда начнётся следующий, то есть когда придут новые повторения и обнулятся дневные лимиты и «сделано сегодня»; rolloverHour — сам час, 0…23, по часам пояса timeZone; timeZone — пояс, по которому режется день (имя IANA; пустая строка — по Москве; поле есть с октября 2026). Рисуете «сегодня» на графике по daily — берите day.key, а не дату по часам устройства. registeredAt — когда заведён аккаунт. С редизайна (октябрь 2026), только добавлением: streak, кроме current и best, несёт щит заморозки — freezesLeftThisMonth (сколько заморозок серии осталось в месяце сегодняшнего дня; 1-го числа — уже бюджет нового месяца), protected (то же «есть ли запас», готовым признаком для пилюли «Защищён»), frozenDates (замороженные дни за последние 30, по возрастанию) и frozeYesterday (заморозка за вчера поставилась ИМЕННО ЭТИМ запросом — для тоста «серия спасена»). Да, это запись в GET: заморозку за пропущенный вчерашний день сервер ставит при первом заходе дня, как это делает страница /dashboard, — и тому, кто занимается только в приложении; повторный запрос её не ставит второй раз. today.newCards — сколько новых карточек человек впервые увидел сегодня. goal.daily = { target, done, date } — кольцо дневной цели: target — цель из настроек (appearance.dailyGoal, по умолчанию 60), done — то же, что today.doneTotal, date — day.key. goal.progress = { type, id, title, total, started, mastered, percent } — блок прогресса по цели appearance.homeGoal: type jlpt (id — уровень N5…N1, грамматика и слова уровня) или deck (id — колода, считается вся ветка с подколодами); started — карточки, вышедшие из новых; mastered — ОСВОЕННЫЕ: в повторении и с устойчивостью FSRS от 21 дня; percent — mastered от total с одной десятой; у type jlpt ещё grammar и vocab = { total, started, mastered } — из чего сложен уровень (грамматика и слова по отдельности; добавлено в октябре 2026). Колода цели удалена или чужая — считается целевой уровень. goal.progress бывает null — расчёт не удался, остальной ответ цел: спрячьте блок или держите прежнее значение. decks — личные колоды ТЕМ ЖЕ ОБЪЕКТОМ и в том же порядке, что GET /api/v1/vocab/decks?stats=1: все поля колоды (подпись, вид, ручной порядок, лимиты, свои настройки показа, прогресс) и всегда — цифры дня today, todayDue, secPerCard и etaMin; разбор полей — в записи GET /api/v1/vocab/decks. deck и nextDue считают только грамматику (grammar) и слова вместе с импортом Anki (vocab): карточки тренажёров — тонального акцента, ономатопей, каны — в них не входят (до 13 октября 2026 они попадали в vocab, и «слов» выходило больше, чем в GET /api/v1/me). Все «сегодня» этого ответа — одни сутки, day. quests — ЗАДАНИЯ ДНЯ (с октября 2026, только добавлением): набор на день человека, каждый день свой — { date, items, completed, total, bonus }. items — задания: kind (вид — ключ, которым его исключают в questOff у /api/v1/settings), label (готовый текст с числом), done и goal (сделано и цель; у заданий на время — минуты), complete и href (страница сайта, где задание делается). «Сделано» считает сервер из своих данных, где бы человек ни занимался, — сообщать о выполнении не нужно и нечем. bonus = { points, awarded, possible }: за ВСЕ задания набора — points баллов в общий рейтинг, один раз в день; awarded — уже начислены (начисляет этот же запрос и главная сайта, как только набор закрыт; вчерашний набор досчитывается при первом запросе нового дня); possible: false — набор баллов не даёт (человек исключил столько видов, что заданий меньше трёх или среди них нет ни карточек, ни времени занятий). items пуст — исключены все виды, блок не показывайте. quests бывает null — расчёт не удался, остальной ответ цел. learnable = { grammar, vocab } (с октября 2026, только добавлением) — СКОЛЬКО НОВОГО МОЖНО ВЗЯТЬ СЕГОДНЯ, то же «+N», что на главной сайта: grammar — остаток дневного лимита новой грамматики (newPerDay минус взятое сегодня), но не больше опубликованных пунктов, у которых у человека ещё нет карточки; vocab — остаток лимита новых слов (vocabPerDay минус разобранное и заведённое сегодня), но не больше слов, которые ещё может дать курс, и не больше места, что осталось в лимите новых основной колоды после уже ждущих там новых карточек (слова курса ложатся в неё и делят этот лимит с новыми карточками ваших колод). В свободном режиме оба — 0. why = { grammar, vocab } — почему ноль: off — курс выключен (свободный режим; у слов — и выключенные «слова курса»), limit — дневной лимит на сегодня взят (у слов — и когда его заняли уже ждущие новые карточки), empty — курс уже отдал всё; null — число больше нуля. Для подписи «Учить новое» берите learnable, а не deck.grammar.new: там карточки в состоянии «новая», а карточка грамматики заводится в миг «Учить» сразу изучаемой, и это число почти всегда ноль. learnable бывает null — расчёт не удался.
{ "name": "…", "targetLevel": "N4", "registeredAt": "2026-05-14T09:00:00.000Z",
"streak": { "current": 16, "best": 41, "freezesLeftThisMonth": 1, "protected": true,
"frozenDates": ["2026-06-20"], "frozeYesterday": false },
"day": { "key": "2026-07-02", "startsAt": "2026-07-02T02:00:00.000Z",
"endsAt": "2026-07-03T02:00:00.000Z", "rolloverHour": 5, "timeZone": "" },
"today": { "doneVocab": 40, "doneGrammar": 12, "doneTotal": 52,
"remainingVocab": 6, "remainingGrammar": 0, "remainingTotal": 6, "newCards": 9 },
"goal": { "daily": { "target": 60, "done": 52, "date": "2026-07-02" },
"progress": { "type": "jlpt", "id": "N4", "title": "N4", "total": 1690,
"started": 1004, "mastered": 612, "percent": 36.2,
"grammar": { "total": 120, "started": 98, "mastered": 71 },
"vocab": { "total": 1570, "started": 906, "mastered": 541 } } },
"deck": { "vocab": { "total": 2182, "new": 90, "learned": 2092 }, "grammar": { … },
"totalCards": 2643, "newCards": 114, "learnedCards": 2529 },
"daily": [ { "date": "2026-07-02", "reviews": 46 } ],
"nextDue": { "vocab": "2026-07-03T09:12:00.000Z", "grammar": null },
"quests": { "date": "2026-07-02", "completed": 1, "total": 3,
"items": [ { "kind": "reviews", "label": "Повторить 25 карточек", "done": 25, "goal": 25,
"complete": true, "href": "/reviews" }, … ],
"bonus": { "points": 20, "awarded": false, "possible": true } },
"decks": [ { "id": "…", "name": "Аниме", "isDefault": false, "cardCount": 312,
"newCount": 8, "dueCount": 14, "reviewDueCount": 6, "icon": "sword", "color": "violet", …,
"today": { "introduced": 17, "limit": 40, "own": true, "extra": 0 },
"todayDue": { "reviews": 6, "news": 8, "total": 14 }, "secPerCard": 7.4, "etaMin": 2 } ] }/api/v1/statsДневная статистика занятий — для графиков и heatmap (виджеты, Obsidian, дашборды). Окно — до года: days=365 отдаёт всё, что нужно тепловой карте (дни без занятий не приходят — рисуйте их пустыми). retention — УДЕРЖАНИЕ, те же три числа, что в профиле в блоке «Сегодня»: доля ответов без «Снова» за сегодня (today), за 7 дней (days7) и за 30 дней (days30). Это доли от 0 до 1; null значит, что оценённых ответов в окне не было, — это не ноль. «Трудно» считается «вспомнил», как в Anki. Считаются все виды карточек вместе; сутки — учебные сутки человека (его часовой пояс и час смены дня, как у /api/v1/stats/today); оба окна включают сегодняшний день. В окна 7 и 30 дней входит история ответов, перенесённая импортом из Anki (в today её нет — там только ответы на сайте и в приложениях); если это правило изменится на сайте, число здесь изменится вместе с ним — ручка считает той же функцией, что и профиль. От параметра days удержание не зависит: окна у него свои. Разбивки по словам и грамматике и удержания по «зрелым» карточкам нет ни здесь, ни на сайте. С октября 2026, только добавлением: у каждого дня daily есть cards и minutes — «карточек за день» и минуты ТЕМИ ЖЕ числами, что в календаре профиля и на главной сайта, но без истории, перенесённой из Anki: ответ числится днём, когда его ДАЛИ (по учебным суткам человека), а не днём, когда он доехал до сервера; ответы тренажёров, которые журнала не пишут (числа, ономатопея), входят в cards, как в профиле. Тепловую карту рисуйте по cards. reviewsDone остаётся счётчиком серии: он считает ответ днём приёма (офлайн-пачка ложится на день синхронизации) и с cards может расходиться. День, где cards есть, а reviewsDone нет, приходит строкой с нулями прежних полей.
Параметры: days=1..365 (30)
{ "days": 30, "streak": 16, "totals": { "reviewsDone": 812, "newGrammar": 24, "newVocab": 310 },
"daily": [ { "date": "2026-07-02", "reviewsDone": 46, "newGrammar": 2, "newVocab": 25 } ],
"retention": { "today": 0.913, "days7": 0.884, "days30": 0.871 } }
# удержание в процентах, как на сайте: Math.round(retention.days7 * 100) → 88 %
# тепловая карта за год:
curl "https://naminori.fun/api/v1/stats?days=365" -H "Authorization: Bearer nmn_…"/api/v1/gardenСад человека (с октября 2026; бывшая ферма) — картинка, которая растёт сама: каждый день с ответами сажает дерево, деревья взрослеют неделей, месяцем, четырьмя месяцами и четырьмя годами и от пропусков не гибнут; цветы, фонарь, лиса, тории, пруд и горы появляются по вехам (выученные слова и грамматика, серия, рекорд, уровень). Дни — те же, что у тепловой карты (cards в /api/v1/stats), без истории Anki. svg — готовый рисунок (viewBox 1200×630, режется по высоте под рамку: preserveAspectRatio xMidYMax slice; цвета внутри числами — вставляйте как есть), label — подпись для диктора («Сад: 42 дерева»), note — строка-приглашение для пустого сада (иначе null), streak — серия, trees — деревья от дней занятий, fruit — плодовые из урожая прежней фермы. Вид «Клетки | Сад» на главной — appearance.homeActivity в /api/v1/settings. Картинка для «Поделиться» — GET /api/v1/garden/image?format=wide (1200×630) или story (1080×1350): PNG, в кадре сад, серия и naminori.fun, без ника и почты. Порог — 60 в минуту (картинка — 20).
{ "svg": "<svg xmlns=…>…</svg>", "label": "Сад: 42 дерева", "note": null, "streak": 16, "trees": 42, "fruit": 0 }
curl "https://naminori.fun/api/v1/garden/image?format=story" -H "Authorization: Bearer nmn_…" -o garden.png/api/v1/stats/todayСводка дня в стиле Anki для экрана «сессия завершена»: сколько карточек, сколько времени, секунд на карточку и фраза под темп. cards — все повторения за день; minutes/secPerCard считаются по записям с замером времени (elapsedMs в POST /api/v1/review). «Сегодня» — учебный день человека: его часовой пояс и час смены дня из настроек, те же сутки, что у /api/v1/time/summary и на сайте (не полночь сервера). Время одного ответа — не дольше его elapsedMs, не дольше двух минут и не дольше, чем прошло с предыдущего ответа этого человека; поэтому у ответов с одним и тем же моментом время получает только первый (см. reviewedAt в POST /api/v1/review/batch). У ответов тренажёра питча elapsedMs не в счёт: их время — разрыв с предыдущим ответом (не больше двух минут), со дня выпуска редизайна, без пересчёта прошлого; см. /api/v1/time/summary. passed — сколько ответов без «Снова», retention — удержание за сегодня (passed / cards, доля от 0 до 1; null — ответов сегодня не было). За 7 и 30 дней — поле retention у GET /api/v1/stats. С 27.10.2026, только добавлением: cram — ответов зубрёжки (/cram на сайте) за тот же день; в cards они не входят (зубрёжка журнала повторений не пишет и расписание не трогает) — показывайте отдельной строкой «зубрёжка: N».
{ "cards": 52, "timeMs": 684000, "minutes": 11.4, "secPerCard": 13.2,
"phrase": "Ровный темп — так и держите.", "passed": 47, "retention": 0.9038, "cram": 0 }/api/v1/me/profileПоля профиля: name (2–60 знаков), slug — адрес публичного профиля /u/<slug> (3–30 знаков, латиница, цифры, _ и -, приводится к нижнему регистру), profileVisibility — кто видит профиль: all «Всем» (страница /u/<slug>, общий рейтинг, поиск в друзья по нику и имени), friends «Друзьям» (страница и вкладка «Друзья» рейтинга — только принятым друзьям, в общем рейтинге вас нет, в друзья находят только по точному нику), none «Никому»; about — «О себе» (до 280 знаков, текстом, без HTML; пустая строка или null — убрать) и materialsVisibility — кто видит материалы (что читали и смотрели, с заметками): те же all | friends | none, по умолчанию none, и не шире профиля. Прежние profilePublic и materialsPublic (boolean) ещё принимаются: true — all, false — none; пришли оба вида — главнее новый. Всё необязательно: присланное записывается, прочее не трогается. Адрес занят — 409 slug taken. Правило 60 часов: у профиля «Всем», набравшего за неделю рейтинга больше 60 ч (держит и следующую неделю), материалы видны всем, а закрыть профиль или материалы нельзя — 403 open rule с фразой для человека в message, не пишется ничего; пока правило действует, ответ (и GET /api/v1/profiles/{slug}) несёт openRule: true. Ответ — профиль, каким его увидят другие: user.cover — шапка профиля (source inherit — своей нет, показывается шапка главного экрана).
curl -X PATCH https://naminori.fun/api/v1/me/profile -H "Authorization: Bearer nmn_…" \
-H "Content-Type: application/json" -d '{"about":"Готовлюсь к N1","materialsVisibility":"friends"}'
→ { "user": { "id": "…", "name": "flasik", "slug": "flasik", "avatarUrl": "/api/avatar/….webp",
"about": "Готовлюсь к N1", "level": "N2",
"cover": { "source": "inherit", "id": null, "crop": null, "dim": 25, "url": null },
"since": "2026-05-14T09:00:00.000Z" },
"profileVisibility": "all", "materialsVisibility": "friends",
"profilePublic": true, "materialsPublic": false }/api/v1/me/avatarАватар. POST — multipart, поле file: JPG, PNG или WebP до 8 МБ; тип определяется по содержимому, а не по заявленному (не картинка — 400 unsupported image, больше 8 МБ — 413 too large). Сервер картинку не пережимает — уменьшите её до отправки. Прежний файл удаляется. DELETE — убрать аватар. Порог: 10 запросов в минуту и 20 загрузок в сутки.
curl -X POST https://naminori.fun/api/v1/me/avatar -H "Authorization: Bearer nmn_…" -F "file=@me.webp"
→ { "avatarUrl": "/api/avatar/0123456789abcdef-a1b2c3d4.webp" }/api/v1/me/coverШапки: slot home — шапка главного экрана, slot profile — шапка публичного профиля (пока своей нет — показывается шапка главного, source inherit). POST — multipart: slot, file (JPG, PNG или WebP до 8 МБ, по содержимому), crop — по желанию, JSON {x,y,w,h} долями 0…1 от картинки; новая загрузка заменяет своё фото слота — и выбранное, и лежащее в «Мои»: у слота не больше одного своего фото. PATCH — JSON {slot, source?: none | gallery | inherit | custom, id?, crop?, dim?}: картинка из набора (gallery + id — только id из набора «Галереи», иначе 400 invalid body с details.id), «без арта» (none), «как на главном» (inherit, только profile), снова своё фото слота из «Мои» без новой загрузки (custom + id из cover.own; crop не прислан — прежняя рамка этого фото; чужое, другого слота или уже удалённое — 400 invalid body с details.id, выбор не тронут), рамка и затемнение dim 0…80 % (null — по умолчанию, 25) без смены источника. gallery, none и inherit меняют только выбор: своё фото не удаляется, а остаётся в «Мои» (cover.own). Затемнение и источник шапки меняются только здесь, не через /api/v1/settings. DELETE ?slot=… — «Убрать арт»: главный → «без арта», профиль → «как на главном»; своё фото остаётся в «Мои». DELETE ?slot=…&photo=1 — «Удалить фото»: своё фото слота (выбранное или из «Мои») удаляется навсегда; было выбрано оно — выбор становится «без арта» / «как на главном», иначе выбор (набор, его рамка и затемнение) не меняется. photo — только 1 или 0 (0 — то же, что без него), иное — 400 invalid query, ничего не тронуто. Ответ всех трёх — {slot, cover: {source, id, crop, dim, url, own}}; own — своё фото слота {id, crop, url}, выбрано оно сейчас или лежит в «Мои», либо null — своего фото нет. У «как на главном» own — своё отложенное фото ПРОФИЛЯ: фото главного туда не попадает. Поле own необязательное (старые клиенты его не знают и ничего не теряют); в чужом профиле (/api/v1/profiles/{slug}) его нет. url у картинки набора — готовая полоса, а при своей рамке (crop не null) — полный кадр, на который рамку надо наложить (см. раздел «Галерея шапок»). Порог: 10 запросов в минуту и 20 загрузок в сутки; PATCH с одним dim (только затемнение, без source и crop) считается отдельно — 60 в минуту (с октября 2026: подбор затемнения ползунком не съедает лимит выбора картинки).
curl -X POST https://naminori.fun/api/v1/me/cover -H "Authorization: Bearer nmn_…" \
-F "slot=home" -F "file=@cover.webp" -F 'crop={"x":0,"y":0.2,"w":1,"h":0.45}'
→ { "slot": "home", "cover": { "source": "custom", "id": "0123456789abcdef-h-a1b2c3d4.webp",
"crop": { "x": 0, "y": 0.2, "w": 1, "h": 0.45 }, "dim": 25,
"url": "/api/cover/0123456789abcdef-h-a1b2c3d4.webp",
"own": { "id": "0123456789abcdef-h-a1b2c3d4.webp", "crop": { "x": 0, "y": 0.2, "w": 1, "h": 0.45 },
"url": "/api/cover/0123456789abcdef-h-a1b2c3d4.webp" } } }
# «Убрать арт» — фото остаётся в «Мои» (как после набора, «без арта» и «Сбросить оформление»):
curl -X DELETE "https://naminori.fun/api/v1/me/cover?slot=home" -H "Authorization: Bearer nmn_…"
# выбрать его снова, без новой загрузки:
curl -X PATCH https://naminori.fun/api/v1/me/cover -H "Authorization: Bearer nmn_…" \
-H "Content-Type: application/json" \
-d '{"slot":"home","source":"custom","id":"0123456789abcdef-h-a1b2c3d4.webp"}'
# «Удалить фото» — навсегда (спросите человека: это необратимо):
curl -X DELETE "https://naminori.fun/api/v1/me/cover?slot=home&photo=1" -H "Authorization: Bearer nmn_…"/api/v1/profiles/{slug}Профиль участника — лист из рейтинга и своя витрина. slug — ник или me (свой профиль — ровно то, что видят другие). period — today | week | month (всё прочее — week). Нет такого ника — 404 not found. Профиль, который вам не виден (и это не ваш), — тот же 404 not found, как у несуществующего ника: «Никому» не видит никто, «Друзьям» — только принятые друзья; поле hidden в ответе всегда false. Время — по правилам рейтинга: посчитанное автоматически и «Разговор», записанный вручную (сутки сервера, без дневного потолка, без «Произношения»); totals.ms — то же, что строка «Всё время» в GET /api/v1/leaderboard; прочие ручные записи чужим не видны. friendship: self | friends | outgoing | incoming | none. period — {key, label, ms, kinds}: kinds — время выбранного периода по видам reading, listening, speaking, cards, grammar, other (мс; сумма — period.ms с точностью до округления) — из него «По навыкам» за любой период; radarWeek — доли видов за текущую неделю (как shares строки рейтинга); last4Weeks — 28 дней с понедельника, у будущих ms: null. summary.cards — слова и прочие карточки за период {ms, answers, retention, secPerCard} БЕЗ грамматики; summary.grammar — то же по грамматике (с 21.10.2026: раньше грамматика входила в cards). summary.reading.pages — страницы из ручных записей, только при открытых материалах; иначе chars — знаки читалки. materials (до 20, по убыванию времени) — только если материалы видны вам (materialsPublic: true — «Всем», или «Друзьям», а вы друг; свои — когда их видит хоть кто-то), иначе null: название, навык, время (только автоматическое — у материала из одних ручных записей ms: 0), объём, комментарий, videoId у роликов, cards и secPerCard у колод (секунды — по ответам с замером, как summary.cards.secPerCard); видео с компьютера — «Видео с компьютера» без имени файла. openRule: true — действует правило 60 часов: у профиля «Всем», набравшего за неделю рейтинга больше 60 ч (держит и следующую неделю), materials видны всем, какой бы выбор ни стоял, вместе со скрытым из ленты; иначе false. Видимость читается на каждый запрос, тяжёлые числа — кэш 5 минут; скрытие из ленты, правка и удаление ручной записи и сброс прогресса сбрасывают его сразу. Порог — 20 в минуту.
Параметры: period=today|week|month (week)
{ "user": { "id": "…", "name": "flasik", "slug": "flasik", "avatarUrl": "/api/avatar/….webp",
"about": "Готовлюсь к N1", "level": "N2",
"cover": { "source": "inherit", "id": null, "crop": null, "dim": 25, "url": null },
"since": "2026-05-14T09:00:00.000Z" },
"hidden": false, "friendship": "none",
"totals": { "ms": 5270400000, "cards": 41008, "bestStreak": 214 },
"period": { "key": "week", "label": "На этой неделе", "ms": 123480000,
"kinds": { "reading": 64800000, "listening": 32400000, "speaking": 0, "cards": 18000000, "grammar": 7200000, "other": 1080000 } },
"radarWeek": { "reading": 0.52, "listening": 0.26, "speaking": 0, "cards": 0.15, "grammar": 0.06, "other": 0.01 },
"last4Weeks": { "from": "2026-09-14", "days": [ { "date": "2026-09-14", "ms": 3600000 } ],
"activeDays": 13, "elapsedDays": 24 },
"summary": { "cards": { "ms": 18000000, "answers": 3000, "retention": 0.8, "secPerCard": 6 },
"grammar": { "ms": 7200000, "answers": 240, "retention": 0.85, "secPerCard": 30 },
"reading": { "ms": 64800000, "pages": 324, "chars": null, "books": 3 },
"audio": { "ms": 32400000, "videos": 13, "avgMs": 2492308 } },
"materialsPublic": true,
"materials": [ { "title": "抜け出し", "skill": "reading", "ms": 18720000, "volume": 94, "volumeUnit": "pages",
"comment": "Читал в метро", "videoId": null, "cards": null, "secPerCard": null } ],
"openRule": false }/api/v1/friendsДрузья. GET /api/v1/friends → {friends: [{userId, name, slug, avatarUrl, level}], incoming: [{requestId, userId, name, slug, avatarUrl}], outgoing: […]}; slug и level — null, когда профиль вам не виден (у друзей — «Никому», у заявок — всё, кроме «Всем»). POST /api/v1/friends/requests {handle} — «В друзья» по нику (или точному имени): профилю «Всем» — по нику или имени, «Друзьям» — только по точному нику, «Никому» — никак; не найден — 404 not found (тёзки по имени — 404 с ambiguous: true, нужен ник); уже друзья — 409 already friends. Если этот человек уже звал вас, его заявка принимается — ответ {friendship: "friends"}, иначе {friendship: "outgoing"} (повтор не шлёт второе уведомление). POST /api/v1/friends/requests/{requestId}/accept — принять заявку вам → {friendship: "friends"}; чужая или отменённая — 404. DELETE /api/v1/friends/{userId} — снять дружбу или заявку в любую сторону → {friendship: "none", removed}. Порог: чтение 60, запись 20 в минуту.
curl -X POST https://naminori.fun/api/v1/friends/requests -H "Authorization: Bearer nmn_…" \
-H "Content-Type: application/json" -d '{"handle":"flasik"}'
→ { "friendship": "outgoing" }/api/v1/leaderboardРейтинг по времени занятий — «Сообщество» → «Рейтинг». Правила те же, что у топа на сайте: в места идёт АВТОМАТИЧЕСКОЕ время (повторения, читалка, видео, игры) и «Разговор», записанный вручную (записи activity speaking — руками, секундомером или таймером, без потолка; кроме импорта Refold; с октября 2026), — и только публичных профилей, без потолка за сутки (с октября 2026; ролики, видео и чтение на нескольких устройствах сразу идут один раз; дни до снятия потолков — по прежним правилам), сутки — сервера (полночь по Москве), а не человека; «Произношение» (тренажёр питча) в рейтинг не идёт. Прочих ручных записей в рейтинге нет — ни в местах, ни в цифрах. Параметры мягкие: незнакомое значение — значение по умолчанию, а не 400. period — week (по умолчанию), month или all (всё время: закрытые месяцы плюс текущий). skill — зачёт по навыку: all, reading, listening, speaking (с 21.10.2026), cards или grammar (у письма и произношения зачёта нет); незнакомый или навык без зачёта — срез all и skillIgnored: true, чтобы клиент не выдал общий зачёт за пустой зачёт «Письма» (сервер старше 21.10.2026 так же отвечает и на speaking). scope — all или friends (вы и принятые друзья; rank и total тогда — среди них, а topPercent — null; в friends есть и профили «Друзьям», в all — только «Всем»). В ответе: range {from, to} — границы периода; skills — навыки, по которым есть зачёт; participants — сколько людей с местом в срезе; top — до 100 строк (пьедестал и список прокруткой) {rank, userId, name, slug, avatarUrl, level, ms, shares, isFriend, isMe} (avatarUrl — только наш или Google-адрес, иначе null; level — целевой уровень, null — не задан; ms — время навыка, у all — всё; shares — доли видов reading, listening, speaking, cards, grammar, other во ВСЁМ времени человека за период, два знака); me — ваша строка {rank, total, topPercent, ms, level, shares, status, aheadMs, manualMs}, свежая, а не из кэша среза; manualMs — записанное вручную за период, что в места НЕ идёт (всё ручное, кроме «Разговора» из рейтинга), мс, — для подписи «ещё N вручную — не в рейтинге». topPercent = max(1, ⌈100 × rank / total⌉) — «Топ 20 %». aheadMs — время места выше (ближайшее большее в срезе, мс) — «до следующего места», когда вы за сотней строк; null — выше никого или места нет. status объясняет, почему места нет: ranked — место есть; private — профиль скрыт (в общем рейтинге только профили «Всем»; с «Друзьям» место есть в scope=friends); quarantine — карантин месяца за крупный перенос часов задним числом; auto-off — выключено «Считать автоматически» (PUT /api/v1/time/auto); no-data — за период и по навыку времени нет. У private, quarantine и auto-off в me.ms всё равно ваше время по правилам рейтинга — его есть что показать рядом с причиной. Порог — 60 в минуту.
Параметры: period=week|month|all (week) · skill=all|reading|listening|cards|grammar (all) · scope=all|friends (all)
curl "https://naminori.fun/api/v1/leaderboard?period=week&skill=reading" -H "Authorization: Bearer nmn_…"
→ { "ok": true, "period": "week", "skill": "reading", "skillIgnored": false, "scope": "all",
"range": { "from": "2026-10-05T00:00:00.000Z", "to": "2026-10-12T00:00:00.000Z" },
"skills": ["all", "reading", "listening", "cards", "grammar"], "participants": 214,
"top": [ { "rank": 1, "userId": "…", "name": "flasik", "slug": "flasik",
"avatarUrl": "/api/avatar/….webp", "level": "N2", "ms": 61200000,
"shares": { "reading": 0.62, "listening": 0.3, "cards": 0.06, "grammar": 0.02, "other": 0 },
"isFriend": false, "isMe": false } ],
"me": { "rank": 37, "total": 214, "topPercent": 18, "ms": 9000000, "level": "N4",
"shares": { "reading": 0.4, "listening": 0.2, "cards": 0.3, "grammar": 0.1, "other": 0 },
"status": "ranked", "aheadMs": 9420000 } }/api/v1/leaderboardТа же ручка — БАЛЛЫ И ПРОШЛЫЕ МЕСЯЦЫ (с октября 2026). metric — hours (по умолчанию у ручки — ради старых клиентов; наши приложения открывают рейтинг в баллах и шлют metric=points явно) или points: рейтинг по баллам за повторения (грамматика +2, слово +1, карточка — не больше раза в день, «Снова» не считается) — те же числа, что на сайте во вкладке «Баллы». ym — прошлый месяц ГГГГ-ММ из окна архива (11 месяцев; вне окна или мусор — живой период): итоги закрытого месяца, баллы — из снимка, по которому выданы медали. У баллов и у архива нет навыка, круга друзей и «всего времени»: skill и scope не действуют (skillIgnored, scopeIgnored: true), period=all отдаёт месяц, архив — всегда месяц. Строк top — до 100. Строки top и me тогда несут points (баллы) или ms (часы прошлого месяца) вместо shares и aheadMs; у me — gap {kind: next | lead, value}: сколько не хватает до места выше или отрыв от второго, и manualMs; winner: true — закрытый месяц по баллам (первая строка — победитель месяца). participants и me.total — сколько людей в зачёте, одно число и тому, у кого места нет; avatarUrl — как у часов: только наш или Google-адрес, иначе null. В КАЖДОМ ответе ручки (и у часов текущего периода): metric, ym (null — живой период) и months — прошлые месяцы для меню периода, новые сверху; по полю metric клиент узнаёт, что сервер умеет баллы.
Параметры: metric=hours|points (hours) · ym=ГГГГ-ММ (нет) · period=week|month (week; у архива — month)
curl "https://naminori.fun/api/v1/leaderboard?metric=points&period=week" -H "Authorization: Bearer nmn_…"
→ { "ok": true, "metric": "points", "period": "week", "ym": null, "months": ["2026-09", "2026-08", "…"],
"skill": "all", "skillIgnored": false, "scope": "all", "scopeIgnored": false,
"participants": 137, "winner": false,
"top": [ { "rank": 1, "userId": "…", "name": "flasik", "slug": "flasik", "avatarUrl": null,
"level": "N2", "isFriend": false, "isMe": false, "points": 2310 } ],
"me": { "rank": 12, "total": 137, "topPercent": 9, "level": "N4", "status": "ranked",
"gap": { "kind": "next", "value": 41 }, "manualMs": 0, "points": 640 } }Настройки и оформление
Общие настройки аккаунта — одни на сайт и все приложения: своих копий у клиента быть не должно (иначе выбор человека разъезжается по устройствам). Новое в ответах всегда приходит ДОБАВЛЕНИЕМ поля, прежние поля не меняются. Старый сервер на тело только из новых полей отвечает 400 nothing to update — проверяйте, что поле вернулось в ответе.
/api/v1/settingsНастройки, которые правятся из приложений. scheduling — расписание FSRS: learningSteps и relearningSteps (шаги обучения строкой, как печатает человек: «1m 10m», «30s 1m 10m»; единицы s, m, h, d, дробь сервер сохраняет целыми — «0.5m» станет «30s»), desiredRetentionPercent (желаемое усвоение; границы — retentionMinPercent…retentionMaxPercent), leechThreshold и leechAction (mark — пометить «приставучей», suspend — снять с повторений), tuned/tunedAt/tunedReviews — подобраны ли личные веса и по скольким ответам; autoAdvance — автопросмотр (questionSec, answerSec, action: skip | again, потолок maxSec); audioAutoplay, audioMixer, vocabContextSentenceMode, newPerDay, vocabPerDay — те же, что в /api/v1/me; reviewCount, leechCount, minReviewsToOptimize — хватает ли истории на подбор весов (reviewCount — без карточек слов и грамматики, на которые отвечали вводом: подбор их не берёт); с октября 2026 в нём и история ответов, перенесённая импортом из Anki, но она досчитывается только до порога minReviewsToOptimize — число говорит, хватает ли истории, а не сколько всего ответов и есть ли приставучие. С редизайна (октябрь 2026): appearance — оформление С УМОЛЧАНИЯМИ (в базе не выбранное лежит пустым, а клиенту приходит готовое значение): look (выводится: theme — палитра «Классика», palette — любая другая), theme (system | light | dark — общая ось с сайтом), darkStyle (вид тёмной темы: oled | graphite), uiTheme (выводится для удобства: light, system или вид тёмной), uiAccent (акцент 1…5), palette, bgMotion (анимация фона палитр: лепестки, снег, звёзды), cardFont (японский шрифт карточек, см. раздел «Шрифты карточек»), homeArt {source: none | gallery | custom, id, crop, url, own} и homeArtDim (шапка главного и её затемнение 0…80 %, по умолчанию 25; правятся только через /api/v1/me/cover), homeHidden (скрытые блоки главного), homeActivity («Активность» главной: cells — клетки, garden — сад, см. /api/v1/garden; по умолчанию cells), dailyGoal (дневная цель, карточек; по умолчанию 60), homeGoal ({type: jlpt, id: N5…N1} или {type: deck, id: колода}; по умолчанию — целевой уровень), palettes (все палитры) и gallery (набор «Галереи» шапок — см. раздел «Галерея шапок»; только здесь, ни в /me, ни в ответах PATCH и сброса его нет). Ключи homeHidden: counters (счётчики), ring (кольцо дневной цели), goal (блок прогресса JLPT или колоды), metrics (метрики 2×2), heatmap (теплокарта), badges (плашки колод с цифрами); новые блоки по умолчанию видны. Прежние имена из черновика — goalRing, goalProgress, deckCounts — сервер понимает и на входе, и в сохранённом, но отдаёт и пишет только нынешние: шлите нынешние. Ещё общие поля: vocabNewOrder (порядок новых слов: deck — как в колоде, random — вразброс: случайный отбор новых на день, в «Все колоды» — из всех колод «вразброс» сразу; frequency — только у колоды, options.newOrder в PUT /api/v1/vocab/decks/{id}) и vocabQueueOrder (reviews-first — сначала повторения, mixed — вперемешку с новыми), vocabTypeReading («Вводить чтение», false — выкл.), dayRolloverHour (час смены дня, 0…23, по часам пояса человека), timeZone и timeZoneAuto (пояс человека: по нему считаются день карточек, серия, дневные лимиты и сутки трекера времени; пустая строка — как на сервере, по Москве; timeZoneAuto: true — пояс определён по устройству, false — выбран руками или не задан; новый час или пояс действуют со следующей смены дня), furigana (always | hover | never | above), grammarAudioVoice (edge | voicevox), questPitch и questOff (задания дня: questOff — исключённые из подборки виды списком целиком, ключи — как kind в quests у /api/v1/overview; питч хранит своё поле questPitch, но в списке он тоже есть: pitch в questOff при записи выключает его, список без pitch — включает; явный questPitch в том же теле главнее), timeGoalHours (цель трекера, часов всего) и timeSkillGoals (дневные цели трекера по навыкам, минуты, умолчания подставлены; 0 — без цели). Курс (с октября 2026, только добавлением): vocabCourseNew — предлагать ли новые слова курса JLPT (строка «Слова курса JLPT» на сайте) и freeMode — «Свободный режим»: курс выключен целиком — новую грамматику и слова курса сервер не выдаёт (learn-new отвечает courseOff: true), грамматика ниже целевого уровня известной не отмечается, а всё добавленное руками работает как прежде. Словари перевода (с октября 2026, только добавлением) — тот же выбор, что в настройках сайта: dictSources — словари (jitendex_ru — «Колобок», kolobok_light — «Колобок Лайт», тот же словарь коротко: вместе с полным не хранится; warodai, jitendex; минимум один, без грамматики; jmdict с 13.10.2026 заменён Колобком Лайт — присланный, он ляжет Лайтом или выпадет рядом с Колобком) и dictGrammar — показывать ли в лукапе грамматику Naminori. Лукап без src= (нативный Android) берёт этот выбор сам; клиент, сводящий свои галочки, проверяет, что поле есть в ответе (старый сервер его не отдаёт). Окно словаря (с октября 2026, только добавлением): dictSensesAll — «Показывать все значения сразу»: false — окно наведения свёрнуто (у статьи первые три значения и «Ещё N значений», более короткие совпадения, части слова и кандзи — строкой «слово, чтение, первое значение ›», статья по нажатию), true — всё целиком; приходит готовым значением (не выбирали — умолчание сервера), меняется тем же PATCH ({ "dictSensesAll": true }), то же поле — в /api/v1/me. vacationUntil — отпуск: момент ISO, до которого очередь на паузе, или null, если отпуска нет (истёкший сервер закрывает сам и отдаёт null); включают и выключают — POST /api/v1/settings/vacation. hintsSeen — подсказки на месте действия, которые человек уже видел (п.7 пачки 12): ключи вроде «manga-edges», общие для всех устройств аккаунта (тур не повторяется на новом); то же поле — в /api/v1/me. uiLang и uiFurigana — язык интерфейса (ru | ja | null — не выбран) и фуригана японских надписей (always | above | never); то же — в /api/v1/me. Порог — 120 в минуту.
{ "scheduling": { "learningSteps": "1m 10m", "relearningSteps": "10m", "desiredRetentionPercent": 90,
"retentionMinPercent": 70, "retentionMaxPercent": 99, "leechThreshold": 8,
"leechAction": "mark", "tuned": false, "tunedAt": null, "tunedReviews": null },
"autoAdvance": { "questionSec": 0, "answerSec": 0, "action": "skip", "maxSec": 120 },
"audioAutoplay": true, "audioMixer": { "master": 100, "voice": 100, "media": 100, "trainers": 100 },
"vocabContextSentenceMode": true, "newPerDay": 25, "vocabPerDay": 25,
"appearance": { "look": "theme", "theme": "system", "darkStyle": "oled", "uiTheme": "system",
"uiAccent": 1, "palette": "classic", "bgMotion": true, "cardFont": "noto-sans-jp",
"homeArt": { "source": "none", "id": null, "crop": null, "url": null, "own": null },
"homeArtDim": 25, "homeHidden": [], "dailyGoal": 60, "homeGoal": { "type": "jlpt", "id": "N5" },
"palettes": ["classic", "sakura", "matcha", "winter", "sunset", "tanabata"],
"gallery": [ { "id": "fuji-dawn", "title": "Фудзи на рассвете", … } ] },
"vocabNewOrder": "deck", "vocabQueueOrder": "reviews-first", "vocabTypeReading": false,
"dayRolloverHour": 0, "timeZone": "", "timeZoneAuto": false, "furigana": "hover",
"grammarAudioVoice": "edge", "questPitch": true, "questOff": [], "timeGoalHours": 1000,
"timeSkillGoals": { "reading": 90, "listening": 45, "speaking": 15, "cards": 20,
"grammar": 0, "writing": 0, "pronunciation": 0 },
"dictSources": ["jitendex_ru"], "dictGrammar": true, "dictSensesAll": false,
"vacationUntil": null, "reviewCount": 41008, "leechCount": 3, "minReviewsToOptimize": 400 }/api/v1/settingsСохранить настройки: присылайте только то, что меняете, остальное не трогается. Разделы scheduling и autoAdvance — целиком (ошибка в шагах — 400 invalid scheduling с готовой фразой в message), audioMixer — все четыре ручки сразу, остальные поля — по одному. appearance — объектом, но пишется ПО ПОЛЯМ: нет поля — не трогать, null — вернуть умолчание; принимаются theme, darkStyle, uiAccent, palette, bgMotion, cardFont, homeHidden (до 20 строк по 40 знаков; незнакомые ключи и повторы молча отсеиваются, прежние имена переводятся в нынешние), homeActivity (cells | garden; null — умолчание), dailyGoal (1…2000), homeGoal (jlpt — только N5…N1, deck — только ваша колода, иначе 400 с details.appearance.homeGoal). Выводимые look и uiTheme, шапка и её затемнение (только /api/v1/me/cover) и целевой уровень (PUT /api/v1/me/target-level) здесь НЕ принимаются. timeSkillGoals — сливается ПО НАВЫКАМ: телефон правит «Чтение», компьютер в ту же секунду «Аудирование» — сохранятся обе цели; null в навыке — его умолчание, null целиком — все к умолчаниям. timeZone — имя IANA («Asia/Tokyo») или пустая строка; незнакомое — 400. Вместе с ним шлите timeZoneAuto, если пояс выбрал сам человек в настройках (false — город руками, true — «как на устройстве»): день переходит на этот пояс со следующей смены дня, а час, выставленный ещё по московским часам, пересчитывается в местный. Без timeZoneAuto пояс считается названным устройством (фоновая сверка): он запоминается с timeZoneAuto: false, но день человека с давним московским часом за ним сам не переходит. timeZoneAuto без timeZone — 400. mangaRtl — направление манги (true — справа налево, японский том; false — слева направо), та же настройка, что «Как читать → Направление» манги на сайте; пишется только оно, остальной вид читалки не трогается, читается из ответа тома (rtl у GET /api/v1/reading/books/{id}). Типы строгие: «30» строкой вместо числа — 400, а не тихое приведение. Вся проверка идёт ДО записи: отказ значит «ничего не сохранено». details отказа — по полям верхнего уровня, у appearance и timeSkillGoals — по вложенному полю (details.appearance.homeHidden, details.timeSkillGoals.reading). Пустое тело или пустой вложенный объект ({appearance:{}}) — 400 nothing to update. vocabCourseNew и freeMode — булевы. freeMode: true ставит на паузу нетронутые карточки грамматики, заведённые выбором уровня (по которым человек уже отвечал, и добавленные руками — не трогает); freeMode: false снимает их с паузы и заново применяет целевой уровень — грамматика ниже него отмечается известной, поэтому клиент обязан сначала переспросить человека. Значение, равное текущему, — пустое действие: карточки не трогаются. Ответ — новое состояние целиком (как у GET, но без appearance.gallery) плюс ok и message — что сохранено, по-русски. Порог записи — 120 в минуту, отдельно от чтения. hintsSeen — дописать увиденные подсказки (массив ключей, к уже увиденным добавляются, не заменяют); null — показать все подсказки заново. uiLang — язык интерфейса (ru | ja; null — «не выбран») и uiFurigana — фуригана японских надписей (always | above | never; null — always).
curl -X PATCH https://naminori.fun/api/v1/settings -H "Authorization: Bearer nmn_…" \
-H "Content-Type: application/json" \
-d '{"appearance":{"darkStyle":"graphite","homeHidden":["heatmap"]},"timeSkillGoals":{"reading":60}}'
→ { "ok": true, "message": "Оформление сохранено. Цели по навыкам сохранены", "scheduling": { … },
"appearance": { "darkStyle": "graphite", "homeHidden": ["heatmap"], … }, … }
· 400 { "error": "invalid body", "message": "…", "details": { "appearance": { "homeGoal": ["колода не найдена"] } } }/api/v1/settings/vacationОтпуск — то же, что «Повторения и FSRS → Отпуск» на сайте: пока он идёт, очереди слов и грамматики пусты (/api/v1/queue, /api/v1/due), а при возврате сроки всех карточек сдвигаются на время паузы, чтобы долг не упал на голову. Тело — одно из двух: {"until": "<ISO>"} — включить до этого момента (сайт и приложение шлют полдень по UTC выбранного дня; дата — в будущем и не дальше 6 месяцев, иначе 400 invalid until с готовой фразой в message), или {"action": "end"} — «Вернуться сейчас». Новый отпуск поверх идущего сперва закрывает прежний тем же правилом. Ответ — как у GET /api/v1/settings, с полем vacationUntil (null — отпуска нет) и message. Оценки, данные во время отпуска (телефон без сети), сервер принимает как обычно. Порог — общий с PATCH, 120 в минуту.
curl -X POST https://naminori.fun/api/v1/settings/vacation -H "Authorization: Bearer nmn_…" \
-H "Content-Type: application/json" -d '{"until":"2026-11-01T12:00:00.000Z"}'
→ { "ok": true, "message": "Отпуск включён до 01.11.2026", "vacationUntil": "2026-11-01T12:00:00.000Z", "scheduling": { … }, … }
· {"action":"end"} → { "ok": true, "message": "Отпуск закончен — очередь снова открыта", "vacationUntil": null, … }
· 400 { "error": "invalid until", "message": "Слишком долго — максимум 6 месяцев" }/api/v1/settings/appearance/reset«Сбросить оформление». Тело {"confirm": true}; без него (пустое тело, false, не то) — 400 confirm required: сброс трогает много настроек сразу, и клиент обязан переспросить человека. Возвращает к умолчаниям всё с вкладки «Главный экран»: тему, палитру, вид тёмной темы, акцент, шрифт карточек, анимацию фона, скрытые блоки, затемнение шапки — и ВЫБОР шапки главного (к стандартной, «без арта»). Свой загруженный файл шапки при этом НЕ удаляется: он остаётся в «Мои» (appearance.homeArt.own) и выбирается снова одним PATCH /api/v1/me/cover {slot: "home", source: "custom", id: own.id}, без новой загрузки; удалить его — только «Удалить фото» (DELETE /api/v1/me/cover?slot=home&photo=1). Не трогает колоды, дневную цель (dailyGoal) и цель блока прогресса (homeGoal), шапку профиля, цели трекера и прочие настройки. Ответ — как у GET /api/v1/settings (без appearance.gallery) плюс ok и message. Порог — общий с PATCH.
curl -X POST https://naminori.fun/api/v1/settings/appearance/reset -H "Authorization: Bearer nmn_…" \
-H "Content-Type: application/json" -d '{"confirm":true}'
→ { "ok": true, "message": "Оформление сброшено",
"appearance": { "theme": "system", "palette": "classic", …,
"homeArt": { "source": "none", "id": null, "crop": null, "url": null,
"own": { "id": "0123456789abcdef-h-a1b2c3d4.webp", "crop": null,
"url": "/api/cover/0123456789abcdef-h-a1b2c3d4.webp" } } }, … }/api/v1/me/target-levelСменить целевой уровень (N5…N1) из приложения — то же, что выбор уровня на сайте. Это МАССОВАЯ запись: вся опубликованная грамматика уровней ниже выбранного помечается «знаю» (карточки уходят в повторение с долгим сроком), иначе «Учить новое» подсовывало бы N5 человеку на N3; словарь не засевается. Поэтому тело {level, confirm: true}: без confirm — 400 confirm required (покажите предупреждение и переспросите), неизвестный уровень — 400 invalid body. Засев только добавляет: повтор безвреден. Тот же уровень ещё раз доотмечает пункты ниже него без карточки (их число — lowerUncovered в /api/v1/me). Понижение уровня само ничего не стирает: в ответе upperAutoMarked — сколько на новом уровне и выше осталось автоматических отметок, по которым человек ни разу не отвечал (upperAutoMarkedRange — их уровни, «N3–N2»). Больше нуля — спросите «Вернуть в «Учить новое» N пунктов?» и пришлите тот же уровень с returnAutoMarked: true: сервер снимет только их (ручные «Уже знаю» и повторённое останутся), засев ниже при этом не идёт, в ответе ещё returned — сколько снято; повтор — 0. Ответ {targetLevel, upperAutoMarked, upperAutoMarkedRange}. Порог — 5 в минуту.
curl -X PUT https://naminori.fun/api/v1/me/target-level -H "Authorization: Bearer nmn_…" \
-H "Content-Type: application/json" -d '{"level":"N3","confirm":true}'
→ { "targetLevel": "N3", "upperAutoMarked": 256, "upperAutoMarkedRange": "N3–N2" }Время занятий (трекер)
Трекер времени — вкладки «Трекер» и «Статистика» в приложении и раздел /time на сайте; числа одни и те же. Время бывает АВТОМАТИЧЕСКОЕ (повторения — по ответам; читалка, манга, YouTube, видео на компьютере и текст игр — по пульсу POST /api/v1/time/heartbeat; пробники; игры) и РУЧНОЕ (секундомер, таймер, запись руками, импорт). Всё — в миллисекундах. Автоматическое — целиком, без потолков за сутки (с октября 2026): время пульса (читалка, манга, YouTube, видео на компьютере, текст игр, тренажёры) на нескольких устройствах сразу сервер засчитывает один раз — прибавка от последнего пинга любой поверхности; ответы на карточки, игры и пробники складываются с ним, караоке — одна песня за раз, попытка пробника — не больше 6 часов; ручное — не больше 12 часов в сутки. Дни до снятия потолков посчитаны по прежним дневным потолкам и не пересчитываются. Сутки трекера — по поясу человека (timeZone в настройках) с его часом смены дня; это не обязательно те же сутки, что у карточек (/api/v1/overview: часы сервера и час смены) и у рейтинга (сутки сервера). Виды (activity) записей: reviews (карточки), grammar, reading и reading-free (чтение с разбором и свободное), listening и listening-free, speaking, writing, pronunciation (произношение), jlpt (пробники), games (только автоматически), other. Навыки (skill) — семь навыков трекера reading, listening, speaking, cards, grammar, writing, pronunciation и other (пробники, игры, «Другое»): вид reviews — навык cards, оба чтения — reading, оба аудирования — listening. Режим контакта (contactMode) у ручной записи: flow (поток), analysis (разбор), hard (сложность), background (фон) или none (без режима). ДОГОВОР «НАВЫК + РЕЖИМ → ВИД»: новый клиент шлёт навык (activity: "reading") и режим, а сервер сам хранит вид, чтобы старые экраны подписывали запись так же: flow и background у чтения и аудирования — вид reading-free / listening-free, analysis и hard — reading / listening, none и прочие навыки — вид как прислан. Обратно: у записи без режима (сделанной до октября 2026) режим выводится из вида — *-free — flow, reading и listening — analysis.
/api/v1/time/summaryИтоги: today, week (с понедельника), month, last28 — у каждого среза autoMs, manualMs, totalMs, byActivity (по видам) и bySkill (по навыкам: семь навыков и other, все восемь ключей всегда, целые мс); total — за всё время (autoMs, manualMs, totalMs); goalHours — цель «часов всего»; timeZone — пояс, по которому резались сутки; skillGoalsMin — дневные цели колец по навыкам, минуты (умолчания подставлены, 0 — без цели); autoTimeTracking — включено ли «Считать автоматически». ВНИМАНИЕ, СМЫСЛ ДВУХ КЛЮЧЕЙ byActivity ИЗМЕНИЛСЯ в октябре 2026: byActivity.reviews теперь — только карточки (слова, кана, свои карточки), а ответы по грамматике ушли в byActivity.grammar; итоги (totalMs и прочие) от этого не изменились — та же сумма разложена иначе. byActivity.pronunciation — время тренажёра питча: считается со дня выпуска этого обновления, по разрывам между ответами (не больше 2 минут на ответ; после перерыва дольше ответ считается первым в сессии и времени не получает), прошлое не пересчитывается. «Произношение» есть только в ЛИЧНЫХ итогах: в рейтинг, в достижения «N часов» и в чужой профиль оно не идёт. Порог — 60 в минуту.
{ "ok": true,
"today": { "autoMs": 2700000, "manualMs": 1800000, "totalMs": 4500000,
"byActivity": { "reviews": 900000, "grammar": 300000, "reading": 1500000, "listening-free": 1800000 },
"bySkill": { "reading": 1500000, "listening": 1800000, "speaking": 0, "cards": 900000,
"grammar": 300000, "writing": 0, "pronunciation": 0, "other": 0 } },
"week": { … }, "month": { … }, "last28": { … },
"total": { "autoMs": 512000000, "manualMs": 98000000, "totalMs": 610000000 },
"goalHours": 1000, "timeZone": "Europe/Moscow",
"skillGoalsMin": { "reading": 90, "listening": 45, "speaking": 15, "cards": 20,
"grammar": 0, "writing": 0, "pronunciation": 0 },
"autoTimeTracking": true }/api/v1/time/feedЛента трекера по дням, свежие сверху. Страница — days НЕПУСТЫХ дней раньше before. Ответ {days: [{date, totalMs, itemsMs, cappedNote?, rankMs?, items}], nextBefore, continue}. totalMs — засчитанное за день (то же число, что в итогах, вместе со скрытыми строками), и сумма countedMs ВСЕХ строк дня, включая скрытые (они приходят только с hidden=1), равна ему до миллисекунды; itemsMs — сумма ms строк до потолков (тоже со скрытыми): больше totalMs — рисуйте «засчитано N из M». cappedNote — готовая фраза под днём, каким потолком срезано (например «Не засчитано 1 ч: записанное вручную — до 12 ч в сутки.», у дней до снятия дневных потолков — «Не засчитано 1 ч по прежним дневным потолкам — с 1 октября их нет.»); показывайте как есть, нет поля — потолки день не тронули (перекрытие ручной записи с автоматическим временем — не потолок, его видно по overlapMs); только у filter=all. rankMs (не всегда, только у дней до снятия дневных потолков в октябре 2026) — сколько из дня засчитано в топ по времени, когда прежний потолок поверхностей топа уменьшил число топа за этот день хотя бы на минуту: рисуйте под днём «В топ засчитано rankMs из totalMs — по прежним дневным потолкам.»; totalMs — весь свой день, а в топ идёт только автоматическое и без «Произношения», поэтому, если в дне есть строки kind не auto или activity pronunciation, допишите «Записанное вручную в топ не идёт.», ««Произношение» в топ не идёт.» или, если есть и то и другое, «Записанное вручную и «Произношение» в топ не идут.»; нет поля — подписи нет. У filter=favorites обе суммы — по избранным. ЛИСТАНИЕ: следующая страница — before = nextBefore; days может прийти КОРОЧЕ запрошенного и даже пустым при nextBefore не null (за один запрос сервер просматривает не больше 60 дней с занятиями, а дни, где всё скрыто, места не занимают) — тогда просто грузите дальше; конец ленты — только nextBefore: null. Строка items: key — ключ строки (te:<id> — ручная запись; у автоматических as:… — сессия поверхности, rv:<навык>:<мс начала> — сессия повторений, jl:… — пробник, kb:… — битва кандзи, ka:… — караоке); kind — auto | manual | stopwatch | timer | import; source — откуда автоматическое время (reading, manga, youtube, desktop-video, browser-video — видео на сайте во встроенном браузере Android, только с японскими субтитрами, game, trainer — тренажёры каны, чисел и письма на сайте, reviews, jlpt, kanji-battle, karaoke); entryId — id ручной записи для PATCH и DELETE /api/v1/time/entries/{id}; skill, activity, title (null — названия нет; ролики из расширения приходят как «YouTube» без материала: расширение названий не шлёт), subkind, startedAt, endedAt, ms, countedMs, overlapMs (сколько из ручной записи уже посчитано автоматически), material {type: book | video | file | game | deck | trainer, id} (trainer: id — kana | numbers | draw, тренажёр сайта), deckIds, cards и passed (у повторений), volume и volumeUnit, contactMode, breakthrough, note, favorite, hidden, editable (true — ручная запись, её можно править). Сессии повторений нарезаны сервером: новая сессия — после перерыва больше 30 минут между ответами одного навыка или со сменой дня. continue {title, skill, activity, subkind, material} — «Продолжить»: самая свежая не скрытая строка с настоящим названием (книга, ролик, файл, колода, тренажёр сайта — material trainer, ручная запись с названием); только у filter=all на первой странице, иначе null. Порог — 120 в минуту.
Параметры: before=ГГГГ-ММ-ДД (нет — с сегодняшнего) · days=1..14 (7) · filter=all|favorites (all) · day=ГГГГ-ММ-ДД — ровно один день (панель дня календаря; перекрывает before и days) · hidden=1 — и скрытые строки, с hidden: true. Параметры мягкие: кривое значение — как не прислано
{ "ok": true, "days": [ { "date": "2026-10-07", "totalMs": 4500000, "itemsMs": 4620000, "items": [
{ "key": "as:…", "kind": "auto", "source": "reading", "skill": "reading", "activity": "reading",
"title": "かがみの孤城", "subkind": "book", "startedAt": "2026-10-07T18:02:11.000Z",
"ms": 1620000, "countedMs": 1500000, "material": { "type": "book", "id": "…" },
"favorite": false, "hidden": false, "editable": false },
{ "key": "te:…", "entryId": "…", "kind": "stopwatch", "skill": "listening", "activity": "listening-free",
"title": "Frieren, 5 серия", "subkind": "anime", "startedAt": "2026-10-07T15:00:00.000Z",
"endedAt": "2026-10-07T15:50:00.000Z", "ms": 3000000, "countedMs": 3000000, "overlapMs": 0,
"volume": 1, "volumeUnit": "episodes", "contactMode": "flow", "breakthrough": false,
"note": null, "favorite": true, "hidden": false, "editable": true } ] } ],
"nextBefore": "2026-10-07",
"continue": { "title": "かがみの孤城", "skill": "reading", "activity": "reading",
"subkind": "book", "material": { "type": "book", "id": "…" } } }/api/v1/time/statsВкладка «Статистика»: total {ms, since, goalHours, goalPct, remainingMs} — всего и цель «часов всего»; inOut — «вход и выход» за всё время (inputMs — чтение и аудирование, studyMs — карточки, грамматика и пробники, outputMs — речь и письмо, и их доли input, study, output; произношение, игры и «Другое» — ни в одной группе); avgPerDay {ms, prevMs, trendPct} — среднее в день по семи ЗАКРЫТЫМ дням (вчера и шесть до него) против семи до них, trendPct — null, пока прошлой недели нет; bestDay {date, ms} — лучший день вместе с ручным временем, после потолков, или null; streakDays — сколько дней подряд с любым занятием (от сегодня, а если сегодня пусто — от вчера). Это НЕ серия повторений из /api/v1/overview: числа разные, подписывайте их по-разному. month {month, minMonth, maxMonth, days: [{date, ms, mode}], modeShare} — календарь месяца «Часы / Дневник» до сегодняшнего дня; mode — режим дня: breakthrough (был «Прорыв»), иначе первый из flow, analysis, hard, background среди ручных записей дня, null — режимов нет (у автоматического времени режима нет); modeShare — доли режимов по минутам записей. radar — доли семи навыков от всего времени. words {savedThisWeek, perHour} — слов намайнено с понедельника и сколько это на час чтения и аудирования этой недели (null — часов не было). week {from, goalMs, days: [{date, bySkill}]} — неделя по навыкам с понедельника, у будущих дней bySkill: null, goalMs — сумма дневных целей по навыкам. top [{title, skill, ms, material}] — топ материалов текущего месяца. Числа кэшируются на 60 секунд; ручная запись, правка и удаление сбрасывают кэш сразу. Порог — 60 в минуту.
Параметры: month=ГГГГ-ММ — месяц календаря (кривое — текущий, вне истории — ближайший край: от minMonth до maxMonth)
{ "ok": true,
"total": { "ms": 610000000, "since": "2026-05-14", "goalHours": 1000, "goalPct": 16.9, "remainingMs": 2990000000 },
"inOut": { "inputMs": 400000000, "studyMs": 180000000, "outputMs": 20000000,
"input": 0.667, "study": 0.3, "output": 0.033 },
"avgPerDay": { "ms": 5400000, "prevMs": 4800000, "trendPct": 12.5 },
"bestDay": { "date": "2026-09-14", "ms": 30600000 }, "streakDays": 23,
"month": { "month": "2026-10", "minMonth": "2026-05", "maxMonth": "2026-10",
"days": [ { "date": "2026-10-01", "ms": 5400000, "mode": "flow" } ],
"modeShare": { "flow": 0.6, "analysis": 0.3, "hard": 0.1, "background": 0 } },
"radar": { "reading": 0.4, "listening": 0.3, "speaking": 0.02, "cards": 0.2, "grammar": 0.06,
"writing": 0.02, "pronunciation": 0 },
"words": { "savedThisWeek": 84, "perHour": 7.2 },
"week": { "from": "2026-10-05", "goalMs": 10200000,
"days": [ { "date": "2026-10-05", "bySkill": { "reading": 3600000, … } },
{ "date": "2026-10-11", "bySkill": null } ] },
"top": [ { "title": "かがみの孤城", "skill": "reading", "ms": 36000000,
"material": { "type": "book", "id": "…" } } ] }/api/v1/time/entriesРучные записи. GET ?from=&to= (ГГГГ-ММ-ДД, по умолчанию последние 30 учебных дней; кривая дата — 400 invalid dates) → {ok, from, to, entries} — до 500 записей. Запись: id, activity, minutes, date (учебный день), startedAt, note, source (manual, timer, stopwatch, import-refold), leagueEligible, createdAt и с октября 2026 поля листа «Сохранить сессию»: title (до 120 знаков), subkind (чип «Что именно?» своего навыка), volume и volumeUnit (pages | chapters | episodes | minutes | cards | items), contactMode, breakthrough («Прорыв»), favorite, а также overlapMs — сколько из записи уже посчитано автоматически и countedMs = minutes × 60 000 − overlapMs (столько запись даёт в итоги до дневных потолков; одно и то же время дважды не считается). POST — записать: {activity, minutes (1…720), date?, startedAt?, note?, source?: manual | timer | stopwatch, title?, subkind?, volume?, volumeUnit?, contactMode?, breakthrough?, favorite?}. Руками пишется любой вид, кроме games. Вид нормализуется по договору «навык + режим → вид» (см. описание раздела); contactMode не прислан — выводится из вида, null — «без режима». Чипы subkind по навыкам: reading — book, manga, article, light-novel, interactive; listening — anime, drama, youtube, podcast; speaking — native, shadowing, tutor; cards — mining-deck, other-deck; grammar — textbook, grammar-cards; writing — diary, essay, chat; pronunciation — tongue-twisters, repeat-after. Чип чужого навыка и объём без единицы — 400 invalid body с details по полю. overlapMs считается у секундомера и таймера с startedAt: секундомер поверх повторений не удвоит время. Запись вне правил — 422 rejected с готовой фразой в message (больше 12 часов за раз или в день, дата в будущем, дальше двух лет назад). leagueEligible: false — запись задним числом («перенос»): крупный перенос часов ставит человеку карантин рейтинга на текущий месяц (status quarantine в GET /api/v1/leaderboard); сайт перед таким переносом переспрашивает, а по API запись делается без вопроса — предупредите человека сами. Ответ POST — {ok, entry}. Пороги: чтение 120, запись 60 в минуту.
curl -X POST https://naminori.fun/api/v1/time/entries -H "Authorization: Bearer nmn_…" \
-H "Content-Type: application/json" \
-d '{"activity":"listening","contactMode":"flow","minutes":50,"source":"stopwatch",
"startedAt":"2026-10-07T15:00:00Z","title":"Frieren, 5 серия","subkind":"anime",
"volume":1,"volumeUnit":"episodes"}'
→ { "ok": true, "entry": { "id": "…", "activity": "listening-free", "contactMode": "flow", "minutes": 50,
"date": "2026-10-07", "source": "stopwatch", "title": "Frieren, 5 серия", "subkind": "anime",
"volume": 1, "volumeUnit": "episodes", "breakthrough": false, "favorite": false,
"overlapMs": 0, "countedMs": 3000000, "leagueEligible": true, … } }/api/v1/time/entries/{id}Правка и удаление СВОЕЙ ручной записи (id — entryId строки ленты). PATCH {minutes?, activity?, date?, note?, title?, subkind?, volume?, volumeUnit?, contactMode?, breakthrough?, favorite?} — пишется только присланное, правила те же, что у создания (тот же договор «навык + режим → вид»; при смене навыка прежний чип subkind снимается сам). «В избранное» у ручной записи — это PATCH {favorite: true}, а не метка ленты. Дата меняется только у записи без времени начала: у секундомера и таймера — 422 rejected. При смене минут перекрытие с автоматическим пересчитывается. Звезду и название можно поставить и у старой записи, которую иначе уже нельзя сдвинуть (импорт с историей старше двух лет). Ответ {ok, entry}. DELETE → {ok: true}. Чужая или удалённая запись — 404 entry not found, кривой id — 400 invalid id. Пороги: PATCH — общий с POST (60 в минуту), DELETE — свой, 60.
curl -X PATCH https://naminori.fun/api/v1/time/entries/<uuid> -H "Authorization: Bearer nmn_…" \
-H "Content-Type: application/json" -d '{"favorite":true,"breakthrough":true}'
→ { "ok": true, "entry": { "id": "…", "favorite": true, "breakthrough": true, … } }/api/v1/time/feed/marks«В избранное» и «Скрыть из ленты» у АВТОМАТИЧЕСКОЙ строки ленты. Тело {key, mark: favorite | hidden, on: true | false} — одна ручка на «поставить» и «снять». key — ключ строки из /api/v1/time/feed как есть (as:…, rv:…, jl:…, kb:…, ka:…); день метки не присылается — начало сессии сервер берёт сам, поэтому смена пояса уносит строку в другой день вместе со звездой. Скрыть — не править: итоги не меняются, строка лишь уходит из ленты (видна с hidden=1). Ручные записи так не помечаются — у них поле favorite (PATCH /api/v1/time/entries/{id}). Чужой или несуществующий ключ — 404 not found, больше 5 000 меток — 409 marks full (снять метку можно всегда). Ответ {ok: true}. Порог — 60 в минуту.
curl -X PUT https://naminori.fun/api/v1/time/feed/marks -H "Authorization: Bearer nmn_…" \
-H "Content-Type: application/json" -d '{"key":"rv:cards:1791900000000","mark":"hidden","on":true}'
→ { "ok": true }/api/v1/time/auto«Считать автоматически» — вкл/выкл. Тело {enabled} → {autoTimeTracking}. Выключено: пульс не пишет время (counted: false), автоматическое время за этот отрезок не идёт ни в трекер, ни в рейтинг, а самого человека нет в рейтинге по времени, пока выключено (статус auto-off в GET /api/v1/leaderboard). Статистику карточек (секунды на карточку, время колоды, сводку дня /api/v1/stats/today) выключатель не трогает — это не учёт времени. Нынешнее состояние — autoTimeTracking в /api/v1/me и /api/v1/time/summary. Порог — 10 в ЧАС: это выключатель, а не кнопка.
curl -X PUT https://naminori.fun/api/v1/time/auto -H "Authorization: Bearer nmn_…" \
-H "Content-Type: application/json" -d '{"enabled":false}'
→ { "autoTimeTracking": false }/api/v1/time/heartbeatПульс активного времени — пока человек читает, смотрит или играет, шлите раз в 15 секунд. Тело {surface: reading | manga | youtube | desktop-video | browser-video | game | trainer, ref (что именно идёт: 1…200 знаков), resume? (true — первый пинг после старта или паузы: только отметить начало), tz? (пояс устройства именем IANA — запоминается, если человек не выбрал пояс руками), title? (название материала для ленты, до 200 знаков; пишется последнее непустое)}. За пинг засчитывается не больше 45 секунд. Строка ленты — на материал (surface + ref), а прибавка считается от последнего пинга человека на ЛЮБОЙ поверхности: два ролика, открытые попеременно, или ролик на телефоне и видео на компьютере одновременно не удвоят время. У game ref ключом не служит — игра всегда одна строка. surface youtube с ref = id ролика (11 знаков) отмечает ролик открытым в «Смотреть позже». Ответ {ok, activeMs, counted, timeZone}; counted: false — «Считать автоматически» выключено, время не записано. Порог — 20 в минуту.
curl -X POST https://naminori.fun/api/v1/time/heartbeat -H "Authorization: Bearer nmn_…" \
-H "Content-Type: application/json" \
-d '{"surface":"youtube","ref":"dQw4w9WgXcQ","title":"日常会話 #12","tz":"Asia/Tokyo"}'
→ { "ok": true, "activeMs": 15000, "counted": true, "timeZone": "Asia/Tokyo" }Повторения (SRS)
Полный цикл внешнего клиента повторений: очередь → показ карточки → оценка. Расписание — тот же FSRS, что на сайте.
/api/v1/dueСколько карточек ждёт повторения сейчас (грамматика + слова). Необязательный deckId сужает счётчик слов до одной личной колоды. С later=1 (с октября 2026) — ещё и учебные, что ВЕРНУТСЯ СЕГОДНЯ ПОЗЖЕ: карточки в изучении и переучивании со сроком дальше 20 минут, но до конца учебного дня. В счётчики к повторению они не входят (очередь до срока их не отдаёт), поэтому экран «повторять нечего» без них сказал бы «на сегодня всё», когда карточка вернётся через час. later — сколько таких ({ grammar, vocab }; у слов — с тем же deckId), laterAt — когда очередь отдаст ближайшую (её срок минус 20 минут, ISO; null — нет таких), now — «сейчас» сервера: «через сколько» считайте как laterAt − now, а не по часам устройства. Как в Anki: «На сейчас — всё. Сегодня вернутся ещё N — ближайшая через …». В отпуске — нули. Без параметра ответ прежний до последнего ключа.
Параметры: deckId=<uuid> · later=1
{ "grammar": 26, "vocab": 46, "total": 72 }
// later=1:
{ "grammar": 0, "vocab": 0, "total": 0, "later": { "grammar": 1, "vocab": 3 }, "laterAt": { "grammar": "2026-10-15T09:40:00.000Z", "vocab": "2026-10-15T09:12:00.000Z" }, "now": "2026-10-15T09:02:00.000Z" }/api/v1/queueСодержимое очереди повторений. У type=vocab ДВЕ формы элемента, различайте их по полю kind: "vocab" — слово из словаря (jp/kana/ru + уровень, часть речи, пример, питч и контекст майнинга), "anki" — карточка из импорта .apkg или созданная вручную, у неё front/back/reading вместо jp/kana/ru. Клиент, написанный только под "vocab", покажет на каждой импортированной карточке undefined. type=grammar — cloze-вопросы, форма совсем другая (см. второй пример). Среди них идут ДОБИВАНИЯ («призраки», ghost: true) — повторения проваленных пунктов на коротких сроках 12 ч → 1 д → 3 д, исчезающие после трёх успехов. У ДОБИВАНИЯ ЕСТЬ СРОК ЖИЗНИ: брошенное больше чем на 30 суток просрочки, оно списывается и из очереди пропадает — насовсем, пока тот же пункт не провалят снова. Часы просрочки стоят, пока человек в режиме каникул. Отдельного поля у этого нет: списанное добивание просто не приходит ни в /queue, ни в счётчик /due. cardId у них всегда null (оценивается не карточка пункта, а само добивание); добавьте ghosts=1 — и у них появится ghostId, которым их оценивает POST /api/v1/review { ghostId, rating }. Без этого параметра ответ ровно прежний, поле не появляется нигде: клиент, который про добивания не знает, отбросит их по пустому cardId, как отбрасывал вчера. НОВЫЕ СЛОВА В «ВСЕ КОЛОДЫ» (type=vocab без deckId, в обеих выдачах) собираются ПО КОЛОДАМ — в порядке списка GET /api/v1/vocab/decks: колода, её подколоды, следующая колода. Каждая колода берёт новые своим порядком (effective.newOrder: deck — как в колоде, random — вразброс: случайный отбор, устойчивый весь день, frequency — по частотности, см. раздел «Частотность слов»; при общей vocabNewOrder=random колоды «вразброс» набирают новые вместе — из всех сразу) и до своего лимита (limits.newPerDay; свой лимит родителя накрывает всю его ветку — и в очереди по deckId родителя; очередь по deckId подколоды родителей выше не видит: только её лимит или общий vocabPerDay и лимиты её подколод); колоды без своего лимита по очереди делят общий vocabPerDay. Карточки без колоды — после всех колод. Повторения, выдача по deckId и очередь человека с одной колодой — как раньше; сколько новых в день — по правилу у PUT /api/v1/vocab/decks/{id}. ОСНОВНАЯ КОЛОДА (isDefault) в этой, перемешанной, выдаче идёт первой порцией — её повторения и её новые, затем остальные колоды, — только пока она стоит первой в списке (выше неё одни её родители). Передвинули её ниже другой колоды (PUT /api/v1/vocab/decks/order) — отдельной порции нет: очередь одна, по vocabQueueOrder из /api/v1/settings, а новые в ней — по порядку колод. order=stable порций не знает. КОЛОДА КАРТОЧКИ: с deckInfo=1 у каждого слова и каждой карточки импорта появляется deckId — колода карточки (null — без колоды). В «Все колоды» показывайте карточку по настройкам ЕЁ колоды (effective из GET /api/v1/vocab/decks: режим карточки, автоозвучка, «только на слух», ввод чтения). Без параметра элемент прежний до последнего ключа. УЧЕБНЫЕ КАРТОЧКИ — ПЕРВЫМИ (с 13 октября 2026). Карточки в изучении и переучивании, чей срок уже наступил (провалены или недоучены раньше), в этой, перемешанной, выдаче идут в самом начале, по сроку — раньше повторений, новых и порции основной колоды; так же у type=grammar (за ними — остальное вперемешку, как было). Раньше они тасовались среди повторений, а при долге больше пятисот карточек в порцию могли не попасть вовсе. С learning=1 у таких карточек (и у слов, и у грамматики; у добиваний его нет) появляется learningDue — когда карточке пора, ISO: и у тех, чей срок наступил, и у тех, чей срок позже сегодня. У учебной, отвеченной ещё вчера (до начала учебного дня), learningDue — начало дня: как в Anki, такая карточка доступна с утра в любой час. Срок в будущем — не показывайте карточку сразу: придержите её до этого момента (как Anki: учебная показывается, когда наступил срок; если больше показать нечего — можно заранее, в пределах 20 минут). С learning=1 обычная выдача (без order=stable) ставит ВСЕ учебные — и со сроком чуть позже — в начало: сперва те, чей срок ещё не подошёл, за ними наступившие в прежнем порядке; limit не срезает ту, что придётся держать до срока (с октября 2026). Без параметра элемент и порядок прежние до последнего ключа. ЕЩЁ ПРИМЕРЫ (с октября 2026, только добавлением): у слова, к которому приложили фразы после сцены, — moreContexts = [{ kind, sourceTitle, sourceUrl, snippet, image, audio, videoId, startMs }], новые первыми, до пяти; image/audio — /api/media/… (склейте с адресом сайта), videoId и startMs — ролик и момент фразы. Поля нет — примеров нет.
Параметры: type=vocab|grammar (vocab) · limit=1..100 (50; в режиме order=stable — до 500) · deckId=<uuid> (колода вместе с подколодами) · order=stable — устойчивая выдача с курсором (см. ниже) · cursor=<строка из nextCursor> · ghosts=1 — отдавать ghostId у добиваний грамматики (иначе их нечем оценить) · intervals=1 — приложить к каждому элементу сроки для кнопок оценки (см. «Сроки на кнопках оценки» ниже) · context=1 — приложить к КАЖДОЙ карточке очереди (и учебному слову, и импортированной из Anki) живое предложение из корпуса (режим «в предложении»: фраза подбирается так, чтобы незнакомых слов в ней было как можно меньше). По запросу, а не всегда: клиенту вроде часов лишний текст в каждой карточке ни к чему. Поле context равно null, если корпус для этого слова ничего не нашёл — тогда режим просто не предлагают. У импортированной карточки в context есть ещё targetJp/targetKana — НАШЕ слово, по которому фраза и нашлась: выделять в предложении надо его, потому что на лице такой карточки стоит что угодно («原因[げんいん]», «6つ,六つ[ムッツ 0]»), и этой подстроки в предложении нет. Показывать ли фразу вообще — это выбор человека в настройках аккаунта, он приезжает в /api/v1/me полем vocabContextSentenceMode; клиенту со своим тумблером оно даёт начальное значение, клиенту без тумблера — окончательное. · mark=1 (вместе с context=1) — в context появляется поле marked: та же фраза в разметке, где изучаемое слово уже обёрнуто в <g>…</g> («{友達|ともだち}にプレゼントを<g>{残|のこ}して</g>いった。»). Сервер находит слово и в изменённой форме (残す во фразе с 残していった) — поиском словарной формы подстрокой его не найти. marked равно null, если слово во фразе не нашлось; без параметра поля нет вовсе, и форма context прежняя. У type=grammar mark=1 добавляет к каждому вопросу sentenceMarked — фразу примера в той же разметке: чтения кандзи {漢|かん} и целевая конструкция в <g>…</g> («{今|いま}、{本|ほん}を<g>{読|よ}んでいます</g>。»), чтобы клиент показал фуригану и у грамматики. Вопрос с пропуском и чтениями — замена <g>…</g> на «___» (сегмент <g> в примере ровно один); sentenceJp остаётся голым текстом, как раньше; sentenceMarked равно null, если в тексте примера есть знаки самой разметки. · deckInfo=1 — приложить к каждой карточке слов deckId, её колоду (см. «Колода карточки» выше). · learning=1 — приложить к учебным карточкам learningDue, момент их срока (см. «Учебные карточки — первыми» выше).
{ "type": "vocab", "total": 46, "items": [
{ "cardId": "…", "kind": "vocab", "jp": "勉強", "kana": "べんきょう", "ru": "учёба",
"level": "N5", "wordId": "<uuid слова>", "partOfSpeech": "сущ.", "exampleJp": "…", "exampleRu": "…",
"accent": 0, "pitch": { "reading": "べんきょう", "type": "平板", "accent": 0 },
"video": { "videoId": "…", "startMs": 61200, "endMs": 63400, "subtitle": "…" },
"textContext": { "kind": "reading", "sourceTitle": "…", "snippet": "…" },
"context": { "jp": "毎日日本語を勉強しています。", "kana": "まいにち…", "ru": "Каждый день учу японский.", "sentenceId": 4512 } },
{ "cardId": "…", "kind": "anki", "front": "猫", "back": "кошка", "reading": "ねこ",
"context": { "jp": "{猫|ねこ}が{好|す}きです。", "kana": "ねこがすきです。", "ru": "Люблю кошек.",
"targetJp": "猫", "targetKana": "ねこ" } } ] }
// с deckInfo=1 у каждого элемента type=vocab добавится "deckId": "<uuid>" (null — без колоды)
// с learning=1 у карточки в изучении или переучивании добавится "learningDue": "2026-10-13T09:10:00.000Z"
// type=grammar — другая форма элемента:
{ "type": "grammar", "total": 26, "items": [
{ "cardId": "…", "ghost": false,
"point": { "slug": "te-iru", "title": "ている", "meaningRu": "длительное действие" },
"question": "今、本を___。", "questionKana": "いま、ほんを___。",
"sentenceJp": "今、本を読んでいます。", "sentenceRu": "Сейчас я читаю книгу.",
"answer": "読んでいます", "alternatives": [ "読んでいる" ], "traps": [],
"after": "。", "needRegister": null, "hintRu": "…" },
// с mark=1 у вопроса есть ещё "sentenceMarked": "{今|いま}、{本|ほん}を<g>{読|よ}んでいます</g>。"
// добивание проваленного пункта — только при ghosts=1 у него есть ghostId
{ "cardId": null, "ghost": true, "ghostId": "…", "point": { … }, "question": "…", … } ] }/api/v1/queue?order=stableСКАЧАТЬ ОЧЕРЕДЬ ЦЕЛИКОМ — для клиента, который занимается без сети. Обычная выдача перемешана и обрывается на limit, поэтому «взять следующие сто» через offset дало бы случайные сто, а не следующие — offset у этой ручки нет вовсе и он молча игнорируется. С order=stable сервер перестаёт тасовать (повторения по сроку, затем новые по порядку колоды), а в ответе появляются hasMore и nextCursor — непрозрачная строка, которую надо вернуть как есть в параметре cursor. Повторяйте, пока nextCursor не станет null; limit шлите на КАЖДОЙ странице (без него страница снова 50). Курсор помнит выборку: с чужим или испорченным курсором ответ 400 invalid cursor, а не тихий кусок чужой очереди. СКЛАДЫВАЙТЕ ПО cardId: пока очередь не меняется, страницы стыкуются без дублей и без дыр, но если между страницами вы отвечаете на сайте, карточка с новым сроком может приехать второй раз (а появившаяся в уже пройденном отрезке — приедет на следующей выгрузке). total — полный долг очереди, знаменатель прогресса; в обычном (перемешанном) режиме то же поле означает длину выданной порции, так что два режима дадут разные числа. Порядок здесь всегда «повторения по сроку, затем новые» и НЕ тасуется (перемешивание с сайта не повторяется): новые идут колодами в порядке списка колод (см. «Новые слова в „Все колоды“» выше), внутри колоды — её порядком новых: как в колоде, по частотности — по рангу, у «вразброс» — случайным ключом дня (с октября 2026: и набор новых, и их порядок одни весь день, новый день — новый набор; раньше набор был «первые по порядку колоды»), а при общей vocabNewOrder=random колоды «вразброс» в «Все колоды» идут вместе, на месте первой из них в списке колод, — отбор из всех сразу. У вас на руках вся очередь, раскладывайте её как нужно, но НОВЫЕ колод с effective.newOrder deck и frequency не тасуйте между собой и между колодами — иначе ни порядок колод, ни «по частотности» до человека не дойдут; тасовать можно повторения и новые колод random на их местах. Дневной лимит новых слов общий на все страницы, а не на каждую. КУРСОР по-прежнему непрозрачный, формат прежний: в нём бывают новые необязательные поля (ранг частотности у колоды «по частотности», номер колоды и отпечаток порядка колод в «Все колоды») — возвращайте его как есть; курсоры, выданные раньше, читаются. Длина курсора — до 1280 знаков (было до 1024): не ограничивайте её у себя жёстче. ПЕРЕСТАНОВКА ПОСРЕДИ СКАЧИВАНИЯ. В «Все колоды» сервер сам замечает, что выше места курсора поменялся порядок колод или порядок новых у колоды (перетаскивание на любом устройстве, новая или удалённая колода, смена основной, смена effective.newOrder), и продолжает НОВЫЕ С НАЧАЛА в нынешнем порядке: ничего не теряется, уже скачанные новые приезжают ещё раз — складывайте по cardId; перестановки ниже места курсора продолжению не мешают. У выбранной колоды (deckId) сменили effective.newOrder посреди скачивания — начните скачивание заново, без курсора: иначе часть карточек задвоится или не приедет до следующего раза. УЧЕБНЫЕ КАРТОЧКИ здесь стоят на своих местах по сроку (порядок страниц и курсор не менялись) — первыми их ставит клиент: просите learning=1 и по полю learningDue показывайте наступившие раньше остальных, а со сроком в будущем — придерживайте до срока.
Параметры: order=stable · cursor=<nextCursor> · limit=1..500 (50) · type, deckId, deckInfo, learning, context — как обычно
# страница за страницей, пока nextCursor не станет null
curl "https://naminori.fun/api/v1/queue?type=vocab&order=stable&limit=500" -H "Authorization: Bearer nmn_…"
→ { "type": "vocab", "total": 1072, "items": [ … 500 … ],
"hasMore": true, "nextCursor": "eyJ2IjoxLCJxIjoi…" }
curl "https://naminori.fun/api/v1/queue?type=vocab&limit=500&cursor=eyJ2IjoxLCJxIjoi…" -H "Authorization: Bearer nmn_…"
→ { …, "hasMore": false, "nextCursor": null }/api/v1/queue?intervals=1СРОКИ НА КНОПКАХ ОЦЕНКИ — через сколько карточка придёт снова, если ответить так, как в Anki. С intervals=1 у каждого элемента очереди появляется intervals: четыре готовые подписи по оценкам 1…4 — again («Снова»), hard («Сложно»), good («Знаю»), easy («Легко»), а у ответа — intervalsAt, момент расчёта. В приложениях Naminori кнопок «Сложно» и «Легко» нет (после проверки ответа в грамматике — одна «Дальше»), но оценки 2 и 4 ручки принимают, как раньше, и их сроки отдаются для клиентов, которые эти оценки ставят. Считает их тот же планировщик и с теми же настройками, что применит POST /api/v1/review: свои шаги обучения, своё желаемое усвоение (и усвоение колоды, если у неё своё), личные веса. Подпись совпадает с назначенным сроком, ПОКА ОТВЕТ ДАЁТСЯ В ТЕ ЖЕ СУТКИ UTC, ЧТО И intervalsAt (граница — 03:00 по Москве). Точный рубеж сервер называет сам — intervalsUntil: конец этих суток UTC либо конец учебного дня человека, если он раньше (с концом учебного дня меняется знак «<»). Рубеж прошёл — попросите свежие подписи (POST /api/v1/queue/intervals) или не показывайте срок: у карточки на шаге обучения «Знаю» в тот же день — это «2 д», а назавтра сервер назначит уже 8. Клиенту с очередью, скачанной впрок, это правило обязательно. ПОДПИСИ — СТРОКИ, показывайте их как есть: меньше минуты — целые секунды («<30 с»: у человека шаги в секундах, как в Anki «30s»), «<1 мин», минуты до часа («10 мин»), часы до суток («12 ч», «1,5 ч»), целые дни до тридцати («11 д»), дальше месяцы с одной десятой («1,5 мес»), от года — годы («1,4 г»). Знак «<» стоит у срока, который наступит до конца сегодняшнего учебного дня человека: очередь отдаёт всё, что созревает до этой границы, так что карточка может прийти и раньше. Особые подписи: «пауза» — этот провал поставит карточку на паузу (у человека включено «приставучую — на паузу»), «добито» — этот успех закроет добивание. У ДОБИВАНИЙ грамматики (ghost: true) сроки свои: провал — «12 ч», успехи — «1 д», «3 д», «добито»; оценка 2 для добивания — провал, поэтому hard у него равен again. intervals: null — срок для этой карточки посчитать не удалось, показывайте кнопку без него. В зубрёжке и пробных вопросах срока нет — они расписание не двигают. Без параметра ответ прежний до последнего ключа.
Параметры: intervals=1 · type, deckId, limit, order, cursor, ghosts, context — как обычно
{ "type": "vocab", "total": 46, "intervalsAt": "2026-10-06T09:12:40.118Z",
"intervalsUntil": "2026-10-06T20:59:59.999Z", "items": [
// новое слово: «Знаю» — сразу в повторение (при усвоении ниже 85 % — "<10 мин")
{ "cardId": "…", "kind": "vocab", "jp": "勉強", …,
"intervals": { "again": "<1 мин", "hard": "<6 мин", "good": "2 д", "easy": "8 д" } },
// зрелая карточка
{ "cardId": "…", "kind": "anki", "front": "猫", …,
"intervals": { "again": "<10 мин", "hard": "3,8 мес", "good": "5,4 мес", "easy": "8,7 мес" } } ] }
// type=grammar&ghosts=1&intervals=1 — добивание после двух успехов
{ "cardId": null, "ghost": true, "ghostId": "…", …,
"intervals": { "again": "12 ч", "hard": "12 ч", "good": "добито", "easy": "добито" } }/api/v1/queue/intervalsСВЕЖИЕ СРОКИ ДЛЯ КАРТОЧЕК, КОТОРЫЕ УЖЕ НА РУКАХ — без перекачки очереди. Нужна клиенту, который скачал очередь и занимается по ней долго: подписи за это время перестают быть правдой — прошёл рубеж intervalsUntil либо человек сменил настройки расписания (усвоение, шаги, веса) на другом устройстве. Второе клиент узнаёт по ответу оценки: POST /api/v1/review с "intervals": true возвращает interval — назначенный срок теми же словами, и если он не совпал с подписью нажатой кнопки (сравнивайте без знака «<»), остальным подписям очереди верить нельзя. Тогда снимите их и попросите здесь заново. Считает та же функция, что в очереди, с настройками на эту секунду. Чужие и удалённые id в ответе просто отсутствуют — пачку они не роняют.
Параметры: тело: { cardIds?: [uuid …] — до 2500, ghostIds?: [uuid …] — до 500 }
curl -X POST https://naminori.fun/api/v1/queue/intervals \
-H "Authorization: Bearer nmn_…" -H "Content-Type: application/json" \
-d '{ "cardIds": ["…", "…"] }'
→ { "intervalsAt": "2026-10-06T21:00:03.512Z", "intervalsUntil": "2026-10-07T00:00:00.000Z",
"intervals": { "<cardId>": { "again": "<10 мин", "hard": "8 д", "good": "11 д", "easy": "19 д" } },
"ghostIntervals": {} }/api/v1/queue?started=1ВСЁ НАЧАТОЕ — ЗАПАС ДЛЯ РАБОТЫ БЕЗ СЕТИ НА ДНИ ВПЕРЁД. Не очередь дня, а все начатые карточки (повторения и учебные) всех колод без условия срока; у грамматики — все начатые пункты и живые добивания. Новых нет — их выдаёт «Учить новое». Только устойчивая выдача страницами по cursor (свой курсор — с курсором очереди дня не стыкуется); deckId не читается. У элемента — stock: { due, lastReview, learning } — по нему клиент без сети показывает карточку, когда ей пора, тем же правилом, что у очереди дня: обычная — если due не позже конца учебного дня; учебная (learning: true) — если due не дальше 20 минут вперёд, а отвеченная до начала дня — с начала дня. В ответе — dayEndsAt (когда начнётся следующий учебный день, как day.endsAt у /api/v1/overview; дальше — те же часы следующих дней) и vacationUntil: отпуск выдачу не гасит — не показывать запас на отпуске должен сам клиент. Ответ без сети копится у клиента и уходит пачкой /api/v1/review/batch с моментом ответа.
Параметры: started=1 · type · cursor=<nextCursor> · limit=1..500 (50) · ghosts, intervals, learning, context, mark, deckInfo — как обычно
curl "https://naminori.fun/api/v1/queue?type=vocab&started=1&limit=500&deckInfo=1" -H "Authorization: Bearer nmn_…"
→ { "type": "vocab", "total": 1169, "dayEndsAt": "2026-10-25T01:00:00.000Z", "vacationUntil": null,
"items": [ { "cardId": "…", "kind": "vocab", "jp": "勉強", …,
"stock": { "due": "2026-10-29T07:12:40.118Z", "lastReview": "2026-10-20T06:58:02.004Z", "learning": false } }, … ],
"hasMore": true, "nextCursor": "eyJ2IjoxLCJxIjoi…" }/api/v1/queue/stockСКОЛЬКО ВЕСИТ «ВСЁ НАЧАТОЕ» — до скачивания, чтобы спросить человека: карточек слов и грамматики (то же условие, что у started=1), файлов картинок и звука у этих карточек и их вес (разные имена /api/media/…, вес — из описи файлов), оценка синтеза (/api/v1/audio — звук учебного слова и фраза грамматики) и текста, totalBytes — всё вместе. Синтез и текст — оценка по средним весам, файлы — по описи.
Параметры: —
curl "https://naminori.fun/api/v1/queue/stock" -H "Authorization: Bearer nmn_…"
→ { "cards": { "vocab": 1020, "grammar": 149 }, "media": { "files": 2210, "bytes": 41200000 },
"tts": { "clips": 610, "bytes": 6350000 }, "textBytes": 2626800, "totalBytes": 50176800 }/api/v1/media/{file}ФАЙЛ КАРТОЧКИ С CORS — тот же кадр или звук, что /api/media/{file} (пути вида /api/media/<uuid>.<ext> из image, audio, rich.audioWord у элементов очереди), но с заголовками Access-Control-Allow-Origin для клиента на чужом происхождении, которому файл надо положить к себе (скачать для работы без сети). Range, ETag и кэш — те же. По токену (в отличие от /api/media), потолок — 1200 файлов в минуту. Для показа в <img>/<audio> CORS не нужен — берите /api/media.
Параметры: file — имя из пути /api/media/<file>
curl "https://naminori.fun/api/v1/media/0b6c…e1.mp3" -H "Authorization: Bearer nmn_…" -o clip.mp3
/api/v1/reviewОценить карточку: FSRS-перепланирование + лог + дневная статистика. rating: 1 Again · 2 Hard · 3 Good · 4 Easy. Необязательный elapsedMs (миллисекунды, 0…30 минут) — сколько времени ушло на карточку; без него «сводка дня» у вашего клиента всегда покажет 0 с/карточку. Необязательные answerMode («input» — набранный ответ, «self_grade» — самооценка), givenAnswer (что человек набрал; хранится до 200 знаков) и wasAlmost (была попытка «почти верно», вторая попытка после опечатки) ложатся в журнал ответа, как у ответов на сайте, и видны в «Инфо о карточке»; без них ответ записывается самооценкой, а с givenAnswer без answerMode — вводом. Слово с вводом чтения — см. запись «POST /api/v1/review (ввод чтения)» в разделе «Колоды: вид, порядок, настройки, статистика». Незнакомое значение answerMode не отказ: оно пишется так же, как отсутствующее (есть givenAnswer — ввод, нет — самооценка). givenAnswer длиннее 10 000 знаков — отказ 400 (храним всё равно только 200, режьте у себя). Необязательный reviewedAt (ISO с зоной, не дальше 72 часов назад) — когда человек ответил на самом деле: FSRS считает интервал от него, а не от момента приёма. День активности (серия, топ) при этом считается по дню ПРИЁМА оценки: чинить серию задним числом нельзя. Повтор уже принятой оценки (тот же cardId и тот же reviewedAt) узнаётся РАНЬШЕ окна: даже досланный позже 72 часов, он отвечает { "ok": true, "duplicate": true }, и расписание второй раз не сдвигается — поэтому reviewedAt не снимайте сами, шлите его всегда. Непринятая оценка с reviewedAt вне окна получает 400 с "code": "out_of_range" — только тогда пересылайте её без reviewedAt (ляжет временем приёма). Если reviewedAt РАНЬШЕ последнего ответа по этой карточке (человек успел ответить на неё с другого устройства, пока ваша оценка ехала офлайн), она только записывается в журнал, а расписание не трогается — последним остаётся более свежий ответ; ответ ручки — { "ok": true, "cardId": "…", "superseded": true }, без undo: это успех, пересылать такую оценку не нужно. Тот же ответ получает оценка, чей reviewedAt раньше СБРОСА карточки больше чем на 5 минут («Учить заново», повторный импорт Anki «с нуля», «Сбросить прогресс»): ответ из прошлой жизни карточки не пишется вовсе — ни расписания, ни журнала, ни счётчика дня. ДОБИВАНИЯ («призраки»). Тело { ghostId, rating } оценивает добивающее повторение проваленного пункта грамматики — идентификатор берётся из /api/v1/queue?type=grammar&ghosts=1. Правила те же, что на сайте: успех — это оценка ≥ 3 (двойка «Hard» для добивания считается провалом), провал ОБНУЛЯЕТ счётчик успехов и возвращает короткий срок, три успеха закрывают добивание навсегда. Ответ у него другой: вместо состояния FSRS — successes, resolved и due. Уже закрытое добивание отвечает 404 — как и удалённое: в очереди его больше нет, а провальная оценка со старым списком иначе воскрешала бы его и обнуляла три сделанных успеха. Тем же 404 отвечает ПРОСРОЧЕННОЕ БОЛЬШЕ ЧЕМ НА 30 СУТОК добивание: у него вышел срок жизни, и принять оценку значило бы вернуть списанное в очередь. Для вас это одно событие — «в очереди его нет», — поэтому и ответ один. Скачанную очередь держите не дольше суток: срок добивания считается на момент ПРИЁМА оценки, а не на reviewedAt. ЗАВОДИТ добивания клиент, приславший "ghosts": true в теле: тогда второй Again подряд по грамматике порождает призрака, как на сайте, а его id уезжает в undo.spawnedGhostId, чтобы откат ответа убрал за собой и добивание. Без этого признака не заводится ничего — и это защита от долга, который вам нечем закрыть: ghostId без ghosts=1 в очереди не появляется, а /api/v1/due добивания считает, так что счётчик на телефоне не погас бы никогда. Признак один и тот же в обе стороны: показываете добивания — получаете их. cardId и ghostId можно слать явным null (строгим клиентам так проще) — null равнозначен отсутствию поля, но ровно одно из двух должно быть заполнено. Ответ содержит undo-токен для отката оценки. Накопленное за офлайн отправляйте ПАКЕТОМ — /api/v1/review/batch: по одной оценке на запрос пятьсот повторений упираются в лимит 240 запросов в минуту. НАЗНАЧЕННЫЙ СРОК СЛОВАМИ: с "intervals": true в теле ответ несёт interval — срок, который оценка назначила, той же строкой, что стояла бы на кнопке («11 д», «<10 мин», «пауза», «добито»). Это пара к intervals=1 у очереди: сверьте его с показанной подписью (без знака «<») и, если не совпало, попросите подписи заново — POST /api/v1/queue/intervals. Без признака ответ прежний до ключа.
curl -X POST https://naminori.fun/api/v1/review \
-H "Authorization: Bearer nmn_…" -H "Content-Type: application/json" \
-d '{"cardId":"<uuid>","rating":3,"elapsedMs":4200}'
→ { "ok": true, "state": "review", "due": "2026-07-06T…", "stability": 4.2,
"undo": { "cardId": "…", "logId": "…", "snapshot": { … }, "sig": "v1.…" } }
// undo возвращайте в /review/undo как есть: sig — подпись сервера на снимке
# «умею добивания»: провал грамматики может завести призрака
-d '{"cardId":"<uuid>","rating":1,"ghosts":true}'
// у оценки, заведшей добивание, в undo добавится "spawnedGhostId": "<uuid>"
# добивание («призрак») — ghostId вместо cardId, и ответ другой формы
curl -X POST https://naminori.fun/api/v1/review -H "Authorization: Bearer nmn_…" -H "Content-Type: application/json" -d '{"ghostId":"<uuid>","rating":3}'
→ { "ok": true, "ghostId": "…", "ghost": true, "successes": 2,
"resolved": false, "due": "2026-09-12T…", "undo": { … } }/api/v1/review/batchОценить до 500 карточек ОДНИМ запросом — для клиента, который копил оценки без сети. Позиции применяются строго по массиву: одна карточка может встретиться дважды (шаги заучивания), и вторая оценка ложится поверх первой. reviewedAt — момент ответа, а не приёма (окно: 72 часа назад, до 5 минут вперёд на убегающие часы); он же ключ повтора: тройка (вид позиции, карточка, reviewedAt) — вид (карточка или добивание) в ключе не для красоты: лог добивания пишется на карточку его пункта, и без вида карточка со своим же добиванием в один момент времени съедали бы друг друга. Оборвалась связь на ОТВЕТЕ — шлите ту же пачку снова: принятые позиции ответят duplicate, расписание второй раз не сдвинется, счётчик дня не удвоится, а добивание не потеряет успех (три успеха закрывают его навсегда — зачтённый дважды повтор съел бы его за один заход). Оценка без reviewedAt такой защиты не имеет. Ответ всегда 200: судьба каждой позиции лежит в results НА ЕЁ МЕСТЕ по индексу — applied | duplicate | not_found (карточку удалили на другом устройстве) | out_of_range (момент вне окна у НЕпринятой позиции — перешлите без reviewedAt; повтор уже принятой узнаётся раньше окна и отвечает duplicate при любой давности, поэтому момент не снимайте заранее) | superseded (reviewedAt раньше последнего ответа по карточке — с другого устройства или раньше в этом же пакете: оценка записана в журнал, расписание не тронуто, undo нет; уберите её из очереди отправки, как duplicate; тот же статус — у reviewedAt раньше сброса карточки больше чем на 5 минут, такая позиция не пишется вовсе, см. POST /review). Счёт superseded появляется рядом с applied/duplicate/failed, только когда такие позиции были, и в failed не входит. undo-токен применённой позиции принимает та же /review/undo — но учтите: отмена стирает запись журнала, то есть сам ключ повтора, и присланная ПОСЛЕ отмены та же оценка применится заново. elapsedMs: до 30 минут на карточку и не больше 6 часов суммарно на запрос — что сверх, применяется без времени. ВРЕМЯ ЗАНЯТИЙ считается по моментам ответов: ответу засчитывается не больше, чем прошло с предыдущего ответа человека. У позиций без reviewedAt момент один на всю пачку — момент приёма, — и время получит только первая из них, остальные лягут с нулём: в «Сегодня», в «Время занятий» и в топ уйдёт один ответ вместо всей пачки. Шлите reviewedAt у каждой позиции — без него время пачки восстановить нечем. answerMode, givenAnswer и wasAlmost у позиции — журнал ответа, как у POST /review. ДОБИВАНИЯ идут в том же пакете: позиция { ghostId, rating } вместо { cardId, rating }; в results у неё стоит ghostId, а вместо состояния FSRS — successes, resolved и due; закрытое добивание отвечает not_found, как и удалённое, и так же — просроченное больше чем на 30 суток (у добивания есть срок жизни, см. POST /review). ЗАВОДЯТСЯ добивания по признаку "ghosts": true рядом с reviews (на весь пакет) — тот же признак, которым вы просите ghostId у очереди; без него провалы грамматики призраков не порождают. Битая позиция отвергает весь запрос (400), но в details теперь виден её НОМЕР — ключ вида reviews.317.cardId. День активности (серия, топ) считается по дню приёма пачки, даже если reviewedAt вчерашние. Лимит 60 запросов в минуту.
Параметры: тело: { reviews: [ { cardId, rating, elapsedMs?, reviewedAt?, answerMode?, givenAnswer?, wasAlmost? } … ], ghosts?: true, intervals?: true } — от 1 до 500 позиций; intervals: true добавляет каждой применённой позиции в results поле interval — назначенный срок словами, как у одиночной ручки; вместо cardId у позиции может стоять ghostId (добивание), но не оба сразу (незаполненное поле допускается как null); ghosts: true разрешает провалам грамматики заводить добивания
curl -X POST https://naminori.fun/api/v1/review/batch \
-H "Authorization: Bearer nmn_…" -H "Content-Type: application/json" \
-d '{"reviews":[
{"cardId":"<uuid>","rating":3,"elapsedMs":4200,"reviewedAt":"2026-09-08T09:12:44.000Z"},
{"cardId":"<uuid>","rating":1,"reviewedAt":"2026-09-08T09:13:02.000Z"} ]}'
→ { "ok": true, "applied": 2, "duplicate": 0, "failed": 0,
"results": [ { "status": "applied", "cardId": "…", "state": "review",
"due": "2026-09-15T…", "stability": 7.1, "undo": { … } },
{ "status": "applied", … } ] }/api/v1/review/undoОтменить последнюю оценку («↩ как в Anki»): тело — объект undo из ответа POST /review или из результата пакета, ЦЕЛИКОМ и без правок. Снимок прежнего состояния в токене подписан сервером (поле sig) и проверяется здесь: собранный или отредактированный руками токен отвергается с 400 — иначе подменённый в нём срок уезжал бы карточке прямо в расписание. Подпись обязательна, льготного срока нет: токен, полученный до перезапуска сервера, тоже отвечает 400 — в этом случае оцените карточку заново. Восстанавливает прежнее FSRS-состояние, стирает лог, −1 к дневному счётчику за сегодня; если отменяемый ответ завёл добивание (spawnedGhostId в токене) — убирает и его. Токен добивания ({ ghostId, logId, successes, due, resolvedAt }) принимается той же ручкой и возвращает счётчик успехов, срок и признак «добит». Ключ повтора живёт в этой же записи журнала, поэтому оценка, присланная снова уже ПОСЛЕ отмены, применится как новая — не ретрайте очередь отправки после отката.
curl -X POST https://naminori.fun/api/v1/review/undo \
-H "Authorization: Bearer nmn_…" -H "Content-Type: application/json" \
-d '{"cardId":"<uuid>","logId":"<uuid>","snapshot":{…},"sig":"v1.…"}'
→ { "ok": true, "cardId": "…" }/api/v1/learn-newДобавить порцию НОВЫХ слов или пунктов грамматики в SRS (аналог «Учить новое» на сайте) с дневными лимитами vocabPerDay/newPerDay. Слова ложатся в колоду deckId (без deckId или с чужим — в основную), и лимит — этой колоды: у колоды со своим лимитом новых — её остаток, а не vocabPerDay; у колоды без своего — общий vocabPerDay. Родители колоды: с явным deckId колода выбрана, и родители выше её не ограничивают (как queue?deckId=); без deckId слова ложатся в основную как в «Все колоды» — тогда и не больше остатка её родителя со своим лимитом. Правило — у PUT /api/v1/vocab/decks/{id}. added>0 → перезапросите /queue; added=0 и remaining=0 — лимит дня; added=0 и remaining>0 — новый контент закончился; для слов при этом courseOff: true значит, что человек сам выключил слова курса («Не предлагать слова курса» на сайте), а слова, которые он удалил из повторений, курс больше не предлагает. То же courseOff: true — и для грамматики, и для слов — приходит в «Свободном режиме» (freeMode в /api/v1/settings): курс выключен целиком. Для слов пропущенный level по-прежнему означает ВСЕ уровни: меняется только порядок — если у пользователя выбран целевой уровень, его слова идут первыми, остальные следом. level=N5..N1 — жёсткий срез (только этот уровень), level=all — обычный порядок от простых к сложным без учёта профиля.
Параметры: type=vocab|grammar · count=1..50 (10) · level=N5..N1|all · deckId (только vocab)
curl -X POST https://naminori.fun/api/v1/learn-new \
-H "Authorization: Bearer nmn_…" -H "Content-Type: application/json" \
-d '{"type":"vocab","count":10}'
→ { "ok": true, "added": 10, "remaining": 15 }/api/v1/cardsМои SRS-карточки с фильтрами и пагинацией. Для vocab/grammar в ответе есть «лицо»: слово — word = { id, jp, kana, ru }, пункт грамматики — grammar = { id, slug, title, meaningRu }; у остальных типов оба равны null. ВАЖНО про лицо: word — это СЛОВАРНАЯ статья, а frontOverride/backOverride — личная правка человека поверх неё; показывать надо frontOverride ?? word.jp, а писать правку — в POST /api/v1/vocab/cards {action:"update"}, которая ложится в ту же накладку. Клиент, который прочитал word.jp, дописал к нему своё и отправил обратно, ЗАТИРАЕТ правку человека молча и без отмены — так уже случилось у нашего же моста аддонов. Нет ключа frontOverride в ответе — значит сервер старый и про накладку не знает; это не «правки нет», и писать лицо вслепую в таком случае нельзя. sourceTitle — подпись «откуда слово» (своя сцена карточки, иначе имя колоды импорта). Неизвестное значение фильтра — 400 с details, а не «молча вся колода»: опечатка в type/state диагностируется сразу. СОСТОЯНИЕ SRS: state (new, learning, review, relearning), due — срок следующего повторения, stability и difficulty — параметры FSRS, reps — сколько раз карточку показывали, lapses — сколько раз она провалена, suspended — снята с повторений, buriedUntil — спрятана до этого момента (null — не спрятана), leech — «приставучая». КОГДА СЛОВО ДОБАВЛЕНО: createdAt — момент, когда карточка заведена в Naminori (ISO 8601, UTC). У намайненного слова это и есть дата майнинга; у карточки, импортированной из Anki, — дата импорта, а не день, когда её завели в Anki. Повторный майнинг того же слова дату не меняет. ЧАСТОТНОСТЬ: jitenRank — место слова в частотном списке Jiten, 1 — самое частое; null — слова в списке нет (он обрезан на сотой тысяче) либо на карточке не слово: грамматика, кана, фраза. Подробнее — раздел «Частотность слов» выше. ОТКУДА СЛОВО: source = { kind, title, url } или null, если источник не записан. kind — вид: youtube (сцена YouTube, url ведёт на ролик с секундой реплики), asbplayer (внешний майнинг: субтитры видео, страница в браузере, игра или экран), reading (книга в читалке), manga (манга или снимок экрана с текстом), grammar (пример из урока грамматики), deck (колода сообщества), anki (импорт из Anki, title — имя колоды в файле), custom (своя карточка, созданная вручную), course («Учить новое»), level (добавлено при выборе уровня), manual (отмечено вручную), lookup (добавлено из словаря без контекста). Вид отдаётся таким, каким его записал клиент при майнинге, — тем же словом, что принимает text.kind у POST /api/vocab/add; снимок экрана с телефона приходит как manga, а страницу и видео с субтитрами задним числом не различить — оба asbplayer. url — полный адрес либо путь на сайте, начинающийся с «/» (склейте с базовым адресом). ЭТО АДРЕС СТРАНИЦЫ, НА КОТОРОЙ ЧЕЛОВЕК МАЙНИЛ СЛОВО, и записан он как есть, вместе с параметрами и якорем: там бывает ролик с секундой, но бывает и личное — открытый чат в веб-мессенджере, поисковый запрос, страница почты. Токен читает только данные своего владельца, но дальше адрес уходит уже в ваши руки: не публикуйте его и не передавайте третьим лицам (в общий список, в статистику, в чужой сервис) без согласия человека. Тот же адрес отдаёт очередь (textContext.sourceUrl) — правило для него то же. title всегда равен sourceTitle. КОЛОДА: deck = { id, name, path } — личная колода, path — полное имя через «::», как в Anki; null у грамматики, каны и питча. ИМПОРТИРОВАННЫЕ И СВОИ КАРТОЧКИ (type: custom): у них нет ни word, ни grammar — слово лежит в anki = { word, reading, deckName }; word равен null, если на лице не слово, а фраза или картинка. Оборота здесь нет — он в GET /api/v1/vocab/decks/{id}/cards. РАСПИСАНИЕ: lastReview — последний ответ (или null), scheduledDays — текущий интервал FSRS в днях. СОРТИРОВКА И КУРСОР: без sort, dir и cursor ответ прежний — порядок по сроку (дальние первыми), листание через offset. Любой из трёх параметров включает упорядоченную выдачу: в ответе появляются sort, dir, hasMore и nextCursor — непрозрачная строка, которую возвращают как есть в cursor (так же, как у очереди). Фильтры шлите на КАЖДОЙ странице: курсор помнит выборку, и с другими фильтрами ответ — 400 invalid cursor; sort и dir можно не повторять, курсор несёт их сам. offset вместе с cursor — 400. sort=created — по дате добавления (по умолчанию свежие первыми), sort=due — по сроку, sort=frequency — по частотности (самые частые первыми; слова без ранга всегда в хвосте, при любом dir). ЧТОБЫ ВЫГРУЗИТЬ ВСЁ, есть два пути. БЫСТРЫЙ — sort=due с курсором: срок обслуживает индекс, и страница не дорожает с глубиной, как дорожает offset. Но срок меняется от каждого ответа: если человек повторяет, пока идёт выгрузка, карточка, на которую он ответил, может прийти второй раз (её срок уехал туда, куда выгрузка ещё не дошла) или не прийти вовсе (срок уехал туда, где выгрузка уже прошла). Поэтому складывайте результат по cardId и не считайте его точным снимком. ТОЧНЫЙ — sort=created&dir=asc: дата добавления у карточки не меняется, поэтому всё, что было на момент начала выгрузки, приходит ровно по разу — ответы, удаления и новые карточки между страницами ничего не сдвигают. Добавленное во время выгрузки приходит в хвост, но не обязательно всё (большой импорт ставит карточкам время своего начала) — заберите его следующим запросом с since. Этот путь медленнее: у sort=created и sort=frequency свой потолок — 60 запросов в минуту. sort=frequency работает только с type=vocab (слова из словаря) и на выборке до 20 000 карточек — больше ответит 400 с советом сузить фильтр; импортированные карточки отсортируйте у себя по jitenRank.
Параметры: type=vocab|grammar|kana|custom|pitch · state=new|learning|review|relearning · suspended=true|false · deckId=<uuid> (только сама колода; subdecks=1 — вместе с подколодами) · tag=<тег> · flag=0..7 · since=<дата> · until=<дата> — отбор по дате добавления: с since включительно и до until, не включая его; ISO 8601 с зоной (2026-10-01T00:00:00Z) или просто дата (2026-10-01 — полночь UTC) · ids=<id через запятую, до 200> — конкретные карточки (по умолчанию limit равен их числу); чужие и несуществующие id просто не приходят · sort=due|created|frequency · dir=asc|desc · cursor=<nextCursor> · limit=1..200 (50) · offset
{ "cards": [ { "cardId": "…", "type": "vocab", "state": "review", "due": "…",
"stability": 12.4, "difficulty": 5.1, "reps": 6, "lapses": 1,
"suspended": false, "buriedUntil": null, "leech": false,
"tags": ["кандзи"], "flag": 0, "deckOrder": 12,
"word": { "id": "…", "jp": "猫", "kana": "ねこ", "ru": "кошка" }, "grammar": null,
"frontOverride": "猫(ねこ)", "backOverride": null,
"sourceTitle": "かがみの孤城 | ッツ Ebook Reader",
"createdAt": "2026-09-20T12:30:41.000Z", "lastReview": "2026-10-01T08:00:12.000Z",
"scheduledDays": 9, "jitenRank": 1527,
"source": { "kind": "reading", "title": "かがみの孤城 | ッツ Ebook Reader",
"url": "/entertainment/reading/…?ch=2&p=7" },
"deck": { "id": "…", "name": "Аниме", "path": "Японский::Аниме" },
"anki": null } ], "limit": 50, "offset": 0 }
# что я намайнил за сентябрь, свежие первыми
curl "https://naminori.fun/api/v1/cards?since=2026-09-01&until=2026-10-01&sort=created" \
-H "Authorization: Bearer nmn_…"
→ { "cards": [ … ], "limit": 50, "offset": 0, "sort": "created", "dir": "desc",
"hasMore": true, "nextCursor": "eyJ2IjoxLCJxIjoi…" }
# следующая страница: фильтры те же, sort можно не повторять
curl "https://naminori.fun/api/v1/cards?since=2026-09-01&until=2026-10-01&cursor=eyJ2IjoxLCJxIjoi…" \
-H "Authorization: Bearer nmn_…"
→ { …, "hasMore": false, "nextCursor": null }
# выгрузить ВСЕ карточки быстро: по сроку, страницами по 200, пока nextCursor не
# станет null. Складывайте по cardId: карточка, на которую человек ответил во
# время выгрузки, может прийти дважды или не прийти вовсе — срок у неё сменился.
curl "https://naminori.fun/api/v1/cards?sort=due&limit=200" -H "Authorization: Bearer nmn_…"
# выгрузить ВСЕ карточки точно: дата добавления не меняется, поэтому каждая
# карточка приходит ровно один раз (потолок этой сортировки — 60 запросов в минуту)
curl "https://naminori.fun/api/v1/cards?sort=created&dir=asc&limit=200" -H "Authorization: Bearer nmn_…"
# десять самых частых слов, которые я ещё не начал учить
curl "https://naminori.fun/api/v1/cards?type=vocab&state=new&sort=frequency&limit=10" \
-H "Authorization: Bearer nmn_…"
# карточки по id — например, дополнить очередь датой добавления и частотностью
curl "https://naminori.fun/api/v1/cards?ids=<uuid>,<uuid>,<uuid>" -H "Authorization: Bearer nmn_…"
# карточка грамматики: пункт в grammar, word равен null
{ "cardId": "…", "type": "grammar", "word": null,
"grammar": { "id": "…", "slug": "temo-ii", "title": "〜てもいい", "meaningRu": "можно (разрешение)" },
"jitenRank": null, "anki": null, … }
# импортированная из Anki карточка: слово в anki, а не в word
{ "cardId": "…", "type": "custom", "word": null, "grammar": null,
"anki": { "word": "原因", "reading": "げんいん", "deckName": "日本語::N3" },
"jitenRank": 982, "source": { "kind": "anki", "title": "日本語::N3", "url": null }, … }
# дата со смещением: «+» в адресной строке надо кодировать как %2B
# since=2026-10-01T00:00:00%2B03:00 (или шлите время в UTC с буквой Z)
# ЛИЦО КАРТОЧКИ = frontOverride ?? word.jp (и backOverride ?? word.ru).
# word — словарь, он общий; frontOverride — правка ЭТОГО человека. Читать
# лицо из word.jp, дописывать к нему и слать обратно — значит затереть её.
# tags — теги карточки (у импортированных из Anki приехали из файла),
# flag — цветная метка Anki 0…7, deckOrder — номер в очереди новых (у импорта .apkg без ручного номера — номер из Anki).
# sourceTitle — подпись «откуда слово»; null, если источника нет.
# Отбор по тегу ТОЧНЫЙ, по написанию: «Кандзи» и «кандзи» — разные теги.
# (Экран «Все карточки» отбирает их как один: он фильтрует в браузере по
# списку, который и так везёт целиком, а здесь запрос к базе и GIN-индекс,
# который обслуживает только точное сравнение.)
# Тег из одних пробелов или длиннее 200 знаков — 400 с details, а не пустой
# список: молчаливая выдача неотличима от «таких карточек нет».
# flag=0 — это «карточки без метки», а не отсутствие фильтра./api/v1/cardsУправление карточкой: suspend/unsuspend (убрать из очереди, обратимо), bury/unbury (спрятать до следующего учебного дня), setTags/addTags/removeTags (теги карточки), setFlag (цветная метка Anki 0…7, 0 — снять), reposition (номер в очереди новых — пишется как есть, другие карточки не сдвигаются; null — в хвост; номер решает среди новых, добавленных одной пачкой, а после «Перемешать новые» — по всей колоде с подколодами; у карточек импорта .apkg ручной номер главнее номера из Anki — с октября 2026), unleech (снять метку «приставучая», вместе с паузой), reschedule (перенести срок повторения: due и scheduledDays). ВНИМАНИЕ ПРО reschedule: срок у нас считает FSRS, и приняв чужой, мы вычёркиваем аккаунт из среза «По баллам» на текущий месяц — иначе массовый перенос делает «сегодняшними» сразу много разных карточек, и дедуп баллов по паре «карточка + день» такого не ловит. Ответ ручки несёт это в поле leaderboard; предупредите человека ДО того, как звать её, а не после. Удаления здесь нет сознательно — оно живёт в редакторе личных колод (POST /api/v1/vocab/cards).
curl -X POST https://naminori.fun/api/v1/cards \
-H "Authorization: Bearer nmn_…" -H "Content-Type: application/json" \
-d '{"cardId":"<uuid>","action":"suspend"}'
# теги: set ставит набор целиком, add дописывает, remove снимает.
# Поле tags обязательно: setTags без него — 400, а НЕ «снять все теги»
# (снять все — это явный setTags с пустым списком).
-d '{"cardId":"<uuid>","action":"setTags","tags":["кандзи","N3"]}'
→ { "ok": true, "tags": ["кандзи","N3"] }
# пробел внутри тега станет подчёркиванием: в Anki теги разделены пробелом,
# и «из аниме» вернулось бы из выгрузки двумя тегами вместо одного
-d '{"cardId":"<uuid>","action":"addTags","tags":["из аниме"]}'
→ { "ok": true, "tags": ["кандзи","N3","из_аниме"] }
# тег длиннее 200 знаков не принимается (400): потолок стоит ради GIN-индекса
# по массиву тегов, в индексный элемент помещается около 2,7 КБ
-d '{"cardId":"<uuid>","action":"setFlag","flag":3}'
-d '{"cardId":"<uuid>","action":"reposition","position":0}'
-d '{"cardId":"<uuid>","action":"unleech"}'
# Перенос срока. Ответ вернёт leaderboard:{excluded:true, scope:"points", …} —
# покажите это человеку: он выходит из зачёта по баллам за текущий месяц.
-d '{"cardId":"<uuid>","action":"reschedule","due":"2026-10-01T09:00:00+03:00","scheduledDays":7}'
# КАРТОЧКИ УЖЕ НЕТ (удалили с другого устройства) — это 404, а не 200 с
# ok:false внутри. Смотрите на код ответа: тело у отказа — {"error":"…"}./api/v1/kanaПрогресс тренажёра каны по знакам — тот же, что тепловая карта /kana на сайте. Ключ — id знака: «hiragana:し», «katakana:ア» (208 знаков: годзюон, дакутэн, ёон обеих азбук). Знака, которого человек не касался, в ответе нет. Порог — 120 в минуту.
{ "stats": { "hiragana:あ": { "reps": 6, "lapses": 1, "stability": 4.2, "state": "review" } } }/api/v1/kana/gradeОтветы тренажёра каны пачкой (до 500) — расчёт тот же, что у сайта: лёгкий FSRS, оценки только 1 (неверно) и 3 (верно). Тело {grades: [{kanaId, rating, at?}]}; at — момент ответа (ISO), без него — время приёма. Неизвестный kanaId — 400 на всю пачку. Позиции применяются по порядку; ответ не новее уже учтённого для этого знака — status stale (не применяется, поэтому пачку с оборванным ответом можно слать ещё раз); at старше 30 дней или дальше 5 минут в будущем — out_of_range. Ответ {ok, applied, stale, outOfRange, results: [{kanaId, status}]}. Порог — 60 в минуту.
curl -X POST https://naminori.fun/api/v1/kana/grade -H "Authorization: Bearer nmn_…" \
-H "Content-Type: application/json" \
-d '{"grades":[{"kanaId":"hiragana:し","rating":3,"at":"2026-10-15T09:00:00Z"}]}'
→ { "ok": true, "applied": 1, "stale": 0, "outOfRange": 0, "results": [{ "kanaId": "hiragana:し", "status": "applied" }] }/api/v1/pitchТренажёр питча (тона) — стартовый экран /entertainment/pitch: hasMaterial (есть ли хоть одно слово для тренировки), due (созревших карточек питча — «Повторить N»), reviewBatch (сколько созревших спрашивается за раз) и baselines — личные классы за 30 дней [{key, label, wrong, total}] (тип акцента и фонетические приметы; для сравнения на экране итогов). Порог — 120 в минуту.
{ "hasMaterial": true, "due": 12, "reviewBatch": 50, "baselines": [{ "key": "nakadaka", "label": "中高", "wrong": 9, "total": 21 }] }/api/v1/pitchТренажёр питча — то же ядро, что у сайта. Тело по полю op: {op:"queue", types, devoiced?, human?, moraMin?, moraMax?} → {words} — до 200 слов режима «Слух» под срез (types — heiban, atamadaka, nakadaka, odaka; devoiced — any, exclude, only; moraMax 0 — без верхней границы): сперва созревшие карточки, затем новые слова; слово {id, jp, kana, ru, mora, accent, features, source: trainer | legacy, humanUrl — живая запись или null}; {op:"due"} → {words} — только созревшие, мимо фильтров, не больше reviewBatch; {op:"grade", vocabId, correct, mode?, elapsedMs?, givenAccent?, heardCount?, playCount?} → {ok} — FSRS-оценка (верно — Good, мимо — Again), журнал ответа и активность дня; {op:"averages", …срез как у queue} → {classes, total, wrong, fewLearners} — средние по ученикам «Слуха» за 30 дней на том же срезе (обезличено, при малом числе учеников — fewLearners и пусто; порог 10 в минуту); {op:"achievements"} → {ok} — пересчёт достижений после сотни без ошибок. Клипы вариантов — /api/audio/pitch?text=<фраза>&accent=<позиция>[&speaker=<голос>]. Порог — 240 записей в минуту.
curl -X POST https://naminori.fun/api/v1/pitch -H "Authorization: Bearer nmn_…" \
-H "Content-Type: application/json" \
-d '{"op":"grade","vocabId":"…","correct":true,"mode":"listen","givenAccent":0,"elapsedMs":4100,"heardCount":3,"playCount":4}'
→ { "ok": true }/api/v1/numbersТренажёр чисел и счётных слов — то, что нужно экрану настроек: слабые формы за 30 дней (их тренажёр возвращает чаще; ключ — «счётное слово/число», «hon/3», у времени «time/7:45», у дат «date/7/14») и сводка (total, correct, accuracy 0–1, streak, longestStreak, avgMs). Порог — 120 в минуту.
{ "weak": ["hon/3", "num/7283"], "summary": { "total": 412, "correct": 371, "accuracy": 0.9, "streak": 8, "longestStreak": 31, "avgMs": 3400 } }/api/v1/numbersСессия тренажёра чисел — то же ядро, что у сайта /practice/numbers. Тело по полю op: {op:"start", mode, settings} → {id} (mode — numbers, prices, time, dates, counters); {op:"answer", sessionId, counterId, n, submitted, elapsedMs?, replays?} → {ok, correct} — верность сервер считает заново по counterId и n, ответ идёт в историю и в активность дня; пустой submitted — «не успел»; {op:"end", sessionId} → {ok}. Порог — 60 сессий и 180 ответов в минуту.
curl -X POST https://naminori.fun/api/v1/numbers -H "Authorization: Bearer nmn_…" -H "Content-Type: application/json" -d '{"op":"answer","sessionId":"…","counterId":"hon","n":3,"submitted":"3","elapsedMs":2100,"replays":0}'
→ { "ok": true, "correct": true }/api/v1/drawТренажёр письма кандзи — то же, что видит /entertainment/draw. Без параметров — экран выбора: level (уровень по умолчанию — целевой уровень аккаунта, иначе N5), levels [{level, total, written}], cards — наборы всех уровней по порядку курса ({id, level, number, total, preview — все знаки набора, size, strokesLabel, written, fixed, msSpent, lastAt}), banner (null или {kind: resume, set} / {kind: next, done, next} — «Вы остановились» / «Набор пройден»), prefs {mode: guide | blind, captionKanji: auto | shown | hidden, lastSet}. С ?set=n5-03 — набор: set {id, level, number, total}, nextSetId, items [{ch, strokes, words: [{jp, kana, ru}], status: new | learning | known | mastered, drawn, cleanRuns}] и startQueue — индексы знаков для «Учить / Продолжить / Повторить» (недописанный набор — по порядку с первого незакреплённого, дописанный — вперемешку); неизвестный набор — 404. Порядок черт знака — файл /kanjivg/<кодовая точка пятью шестнадцатеричными знаками>.json (KanjiVG, CC BY-SA 3.0). Порог — 120 в минуту.
{ "set": { "id": "n5-01", "level": "N5", "number": 1, "total": 4 }, "nextSetId": "n5-02",
"items": [{ "ch": "一", "strokes": 1, "words": [{ "jp": "一", "kana": "いち", "ru": "один (1)" }], "status": "known", "drawn": 2, "cleanRuns": 2 }, …],
"startQueue": [1, 2, 3, …] }/api/v1/drawЗапись тренажёра письма — то же ядро, что у сайта. Тело по полю op: {op:"drawn", kanji, mistakes, ms} → {ok} — знак дописан (слать сразу, не в конце набора; kanji — ровно один кандзи, mistakes — неверных черт за написание, ms — от первой черты до последней; больше 500 промахов и 10 минут срезается); {op:"prefs", lastSet?, captionKanji?, mode?} → {ok} — набор для «Продолжить», глазик подписи и режим «Образец: виден · скрыт» (общий с повторением слов на сайте). Порог — 120 знаков и 120 записей настроек в минуту.
curl -X POST https://naminori.fun/api/v1/draw -H "Authorization: Bearer nmn_…" \
-H "Content-Type: application/json" \
-d '{"op":"drawn","kanji":"日","mistakes":0,"ms":5200}'
→ { "ok": true }/api/v1/onomatopoeiaТренажёр ономатопей — то, что нужно экрану настроек: сводка (total — ответов всего, accuracy 0–1 или null, streak — верных подряд, cards — слов в повторениях, weakMotion — до пяти классов движения с ошибками {motionClass, wrong, total}) и due — сколько карточек созрело (кнопка «Повторить N», та же выборка, что у сайта /practice/onomatopoeia). Порог — 120 в минуту.
{ "summary": { "total": 64, "accuracy": 0.81, "streak": 5, "cards": 22, "weakMotion": [{ "motionClass": "tremor", "wrong": 4, "total": 9 }] }, "due": 7 }/api/v1/onomatopoeiaСессия тренажёра ономатопей — то же ядро, что у сайта /practice/onomatopoeia. Тело по полю op: {op:"queue", levels, categories, kinds, count, dueOnly?} → {questions} — очередь собирает сервер (уровни 1–6, категории, типы упражнений feel-word | word-feel | word-meaning | cloze | pair, count 5–60; dueOnly — только созревшие, мимо фильтров, до 50); у вопроса kind, word, options, answerIndex, sentence; {op:"start", settings} → {id}; {op:"answer", sessionId, wordId, kind, submitted, elapsedMs?, replays?, attempt?} → {ok, correct, typo, alsoRight, alsoWord, otherWord, retry} — верность сервер считает заново (у вариантов submitted — slug выбранного, у клоуза — набранный текст), ответ идёт в расписание FSRS, журнал и активность дня; retry: true — вторая попытка клоуза (опечатка или слово из той же темы), ответ НЕ записан, следующий — с attempt: 2. Ответ на слово сессии принимается ОДИН раз (в сессии каждое слово спрашивается один раз): повтор того же ответа — досылка без сети, у которой потерялся отклик, — отвечает тем, что записано, и второй раз ничего не пишет; чужая или неизвестная сессия, неизвестное слово — 404 not found (повтор не поможет); ok: false — потолок ответов, пришлите позже. {op:"end", sessionId} → {ok}. Порог — 60 очередей и сессий и 180 ответов в минуту.
curl -X POST https://naminori.fun/api/v1/onomatopoeia -H "Authorization: Bearer nmn_…" \
-H "Content-Type: application/json" \
-d '{"op":"answer","sessionId":"…","wordId":"…","kind":"cloze","submitted":"どきどき","elapsedMs":3100,"attempt":1}'
→ { "ok": true, "correct": true, "typo": false, "alsoRight": false, "alsoWord": null, "otherWord": null, "retry": false }Личные колоды
Колоды слов в стиле Anki — те же, что на /vocab/decks. Не путать с /api/v1/decks: там публичные колоды сообщества. Этими ручками пользуются наш десктоп, мобильное приложение и расширение (выбор колоды при майнинге).
/api/v1/vocab/decksМои колоды со счётчиками: id, name, description и isDefault (основная колода — туда падают слова без явного deckId), всего карточек, новых, к повторению — и свои настройки колоды (limits): лимит новых и повторений в день, желаемое усвоение — долей (desiredRetention, 0.70…0.99) и процентом (desiredRetentionPercent, 70…99 — тем же видом, что в PUT и в /api/v1/settings). null — «как в общих настройках». Очередь (/api/v1/queue, /api/v1/due) и оценка (/api/v1/review) эти настройки уже учитывают. ПОДКОЛОДЫ: список идёт деревом (родитель, за ним подколоды); parentId — родитель (null — верхний уровень), path — полное имя через «::», как в Anki, depth — глубина (0 — верхний уровень, не больше 4), collapsed — свёрнута ли ветка. cardCount/newCount/dueCount — собственные карточки колоды (их можно складывать по всем колодам), total — те же числа по всей ветке: повторение по колоде (queue?deckId=) берёт и подколоды. ПОРЯДОК СПИСКА — обход дерева: родитель, за ним его подколоды; братья — по position (ручной порядок, PUT /api/v1/vocab/decks/order), колоды без него — после, и среди них основная ветка первой, дальше по времени создания, при равном времени (подколоды одного импорта .apkg) — по имени в естественном порядке: «N1, N2 … N10», регистр не важен. Тем же порядком «Все колоды» набирают новые слова (GET /api/v1/queue) и идут колоды на сайте. С РЕДИЗАЙНА (октябрь 2026), только добавлением; объект колоды тот же, что в decks[] у GET /api/v1/overview. Есть всегда: reviewDueCount и total.reviewDueCount — из dueCount одни повторения, без новых; subtitle — строка под именем (своё описание; у основной колоды без описания — подсказка сервера, у прочих — null); icon — значок, одно из: hammer, book-2, star, language, device-gamepad-2, flower, sword, headphones (по умолчанию book-2); color — цвет, одно из: coral, green, violet, blue, yellow, pink, neutral (по умолчанию coral; пару «значок / подложка» рисуйте своей темой); lookCustom — задал ли человек вид сам (для «Сбросить к умолчанию» и чтобы не отмечать «выбрано» на умолчании); position — место среди братьев (null — колоду не ставили руками); options — свои настройки показа колоды, как хранятся, null в ключе — «как в общих»: cardMode (word — слово, sentence — в предложении), newOrder (deck — как в колоде, random — вразброс, frequency — по частотности, см. раздел «Частотность слов»), typeReading («Вводить чтение», см. раздел «Колоды: вид, порядок, настройки, статистика»), contextImage (кадр из контекста), autoplay (автоозвучка), listenOnly («только на слух»), separate («Отдельная колода»: её слова не мешают добавлять их в другие колоды — POST /api/v1/vocab/status отвечает по ним card: false, если в теле не назвать колоду, куда положите слово (deckId или путь deckPath «A::B» рядом с words: тогда card — в её «пуле», как ответит и добавление), а добавление слова кладёт его ещё раз в ту колоду, куда просят; дубли — только с такими колодами; у основной колоды не действует; подколоды отдельной колоды — в её ветке); effective — те же ключи после наложения общих настроек (vocabContextSentenceMode, vocabNewOrder, vocabTypeReading, audioAutoplay; у кадра общей настройки нет — вкл., у «только на слух» и «отдельной» — выкл.): карточку колоды показывайте по effective; подколода настроек показа родителя не наследует; progress = { total, started, mastered, percent } — по ветке: started — вышли из новых, mastered — освоены (в повторении и с устойчивостью FSRS от 21 дня), percent — mastered от total с одной десятой. Только со stats=1 (список без флага зовут на каждом листе лукапа, цифры дня ему ни к чему; флаг мягкий — всё, кроме 1, означает «без него»): today = { introduced, limit, own, extra } — «Сегодня 17 из 40»: own true — своя норма колоды (у колоды со своим лимитом — счёт по всей её ветке), false — общий котёл колод без своего лимита, у таких колод цифры одни на всех; introduced бывает больше limit — сверх нормы («Доп. занятие» на сайте, норму снизили после занятий, занимались офлайн): пишите «Сегодня: 31 · сверх нормы 21», а не «31 из 10»; extra (с октября 2026, только добавлением) — сколько из этого «сверх нормы» покрыл сегодняшний «Доп. занятие» (0 — ничего или в пределах нормы; добор на весь аккаунт, поэтому у колоды — не больше её перерасхода): «…, из них 10 — «Доп. занятие»»; todayDue = { reviews, news, total } — сколько человек реально увидит сегодня, выбрав эту колоду (queue?deckId=): по ветке, с её лимитами и лимитами её подколод, лимиты родителей выше не ограничивают; secPerCard — секунд на карточку за 30 дней по ветке (мало замеров — по всем колодам, нет и там — null); etaMin — «~M мин» на todayDue.total: round(total × S / 60), не меньше 1, где S — secPerCard, но не меньше 4 и не больше 60 с (то же правило, что у «~M мин» на главной сайта и в приложениях; пустая колода — 0, null — не из чего считать). ОТПУСК: пока у человека идёт «Отпуск» (на сайте; очередь слов в нём пуста), к повторению — нули: dueCount и reviewDueCount (и в total), а через них todayDue и etaMin; всего, новых и progress — как есть.
Параметры: stats=1 — цифры дня у каждой колоды (today, todayDue, secPerCard, etaMin)
{ "decks": [ { "id": "…", "name": "Kaishi", "description": null, "subtitle": null, "isDefault": false,
"icon": "book-2", "color": "coral", "lookCustom": false, "position": 0,
"cardCount": 312, "newCount": 8, "dueCount": 14, "reviewDueCount": 6,
"limits": { "newPerDay": 30, "reviewsPerDay": null, "desiredRetention": 0.95, "desiredRetentionPercent": 95 },
"parentId": "…", "path": "Японский::Kaishi", "depth": 1, "collapsed": false,
"total": { "cardCount": 312, "newCount": 8, "dueCount": 14, "reviewDueCount": 6 },
"options": { "cardMode": null, "newOrder": "frequency", "typeReading": true,
"contextImage": null, "autoplay": null, "listenOnly": null, "separate": null },
"effective": { "cardMode": "sentence", "newOrder": "frequency", "typeReading": true,
"contextImage": true, "autoplay": true, "listenOnly": false, "separate": false },
"progress": { "total": 312, "started": 240, "mastered": 96, "percent": 30.8 } } ] }
# ?stats=1 — у каждой колоды ещё цифры дня:
"today": { "introduced": 17, "limit": 30, "own": true, "extra": 0 },
"todayDue": { "reviews": 6, "news": 8, "total": 14 }, "secPerCard": 7.4, "etaMin": 2/api/v1/vocab/decksСоздать колоду. Тело: { name (1..80), description? (до 300), parentId? }. parentId — завести подколоду; имя с «::» — путь, как в Anki: «Японский::Kaishi» найдёт или заведёт «Японский» и положит под неё новую «Kaishi». Путь — целиком до 400 знаков, каждая колода в нём — до 80. Вложенность — не больше пяти уровней.
curl -X POST https://naminori.fun/api/v1/vocab/decks \
-H "Authorization: Bearer nmn_…" -H "Content-Type: application/json" \
-d '{"name":"Аниме","description":"слова из сабов"}'
→ { "ok": true, "id": "<uuid>" }/api/v1/vocab/decks/{id}Переименовать колоду / сменить описание и вид / задать свои настройки / перенести. Тело: { name?, description?, icon?, color?, options?, limits?, parentId?, collapsed? } — присланное меняется, остальное не трогается. parentId — перенести колоду под другую (uuid) или на верхний уровень (null); нельзя в себя, в свою подколоду и глубже пяти уровней — 400 с message; перенесённая колода теряет место в ручном порядке (position: null). collapsed — свернуть/развернуть ветку в списке (помнится на всех устройствах). В имени не может быть «::» — для вложения есть parentId. description: null — очистить подпись. icon и color — значение из набора (см. GET /api/v1/vocab/decks) или null — вернуть умолчание; цвет принимается и синонимом из черновика спецификации (purple — violet, gray и grey — neutral), а отдаётся всегда именем из набора; незнакомое — 400 bad input с details.icon или details.color. options — СЛИЯНИЕМ по ключам: { "options": { "typeReading": true } } включает ввод чтения и не трогает остальные настройки колоды; null в ключе — «как в общих»; незнакомый ключ отбрасывается, кривое значение — 400 bad input с details.options. limits = { newPerDay, reviewsPerDay, desiredRetentionPercent } — целое число или null («как в общих»); ПОЛЕ, КОТОРОГО НЕТ В limits, НЕ ТРОГАЕТСЯ (до октября 2026 оно становилось null; клиент, который шлёт все три поля, получает прежний итог). Новых и повторений — 0…9999 в день, усвоение — 70…99 %. ДНЕВНОЙ ЛИМИТ НОВЫХ. Колода со своим newPerDay берёт свой лимит и общим не ограничена; общий vocabPerDay — «Новых в день (для колод без своего лимита)»: его по очереди, в порядке списка колод, делят колоды без своего лимита. Без вложенных лимитов (ни у одного родителя нет своего) «Все колоды» за день — сумма своих лимитов плюс общий. Пример: общий 20, F без своего, K — 30, M — 10 → 20 + 30 + 10 = 60 новых в день; перестановка таких колод меняет порядок, а не число. Свой лимит колоды накрывает всю её ветку, когда учат её саму или «Все колоды»: родитель 10, подколода 30 → не больше 10 в день на обе; колоды без своего лимита под таким родителем берут из общего, но не больше остатка родителя. Выбрана одна подколода (GET /api/v1/queue?deckId= подколоды, POST /api/v1/learn-new с её deckId) — как в Anki по умолчанию, родители выше её не ограничивают: действует только её лимит (своего нет — общий vocabPerDay) и лимиты её подколод, а пройденное в ней всё равно тратит лимит родителя. Пример: родитель 10, подколода 30, лимит родителя исчерпан — «Все колоды» и родитель новых больше не дают; подколода, выбранная сама, даёт свои 30, если лимит родителя выбрали его собственные карточки (или другая подколода), и ещё 20, если его выбрала она сама (10 из её 30). В «Все колоды» и у родителя подколоды берут из его остатка в порядке списка, поэтому перестановка может изменить, сколько новых даст день. Пример: общий 20, родитель P — 10 (своих новых нет), под ним S без своего и L — 30, рядом D без своего, во всех трёх по 40 новых: порядок S, L → 20 новых за день, порядок L, S → 30. S первой берёт 10 из общего и выбирает остаток P — L не достаётся ничего, D — оставшиеся 10 общего; L первой берёт 10 из своего лимита и выбирает остаток P — S не достаётся ничего, а общий целиком, 20, уходит D. Лимит повторений не режет карточки на шагах обучения. Ответ — { ok, limits?, deck }: limits — перечитанные из базы, только если их присылали; deck — колода целиком, как в GET /api/v1/vocab/decks (без цифр дня).
curl -X PUT https://naminori.fun/api/v1/vocab/decks/<uuid> \
-H "Authorization: Bearer nmn_…" -H "Content-Type: application/json" \
-d '{"limits":{"newPerDay":30,"reviewsPerDay":null,"desiredRetentionPercent":95}}'
→ { "ok": true, "limits": { "newPerDay": 30, "reviewsPerDay": null, "desiredRetention": 0.95, "desiredRetentionPercent": 95 },
"deck": { "id": "…", "name": "Kaishi", … } }
# вид и свои настройки — присланное меняется, прочее не трогается
-d '{"icon":"sword","color":"violet","options":{"typeReading":true,"newOrder":"frequency"}}'
→ { "ok": true, "deck": { "id": "…", "icon": "sword", "color": "violet", "lookCustom": true, … } }
· 400 { "error": "bad request", "message": "Желаемое усвоение — целое число от 70 до 99 %" }
· 404 { "error": "not found", "message": "Колода не найдена" }/api/v1/vocab/decks/{id}Сделать колоду колодой по умолчанию (туда падают слова из майнинга без явного deckId).
/api/v1/vocab/decks/{id}Удалить колоду. Колоду по умолчанию удалить нельзя. Если в колоде есть карточки — 409 { error: "deck not empty", cardCount }; передайте moveToDeckId, чтобы перенести их в другую колоду, либо deleteCards=1, чтобы снести их вместе с колодой (необратимо; в ответе deletedCards). Оба параметра сразу — 400 (судим по наличию moveToDeckId, а не по тому, разобрался ли он: кривой uuid рядом с deleteCards тоже 400, а не молчаливый снос). Кривой moveToDeckId сам по себе — 400. Подколоды: subdecks=lift (по умолчанию) поднимает их на уровень выше, subdecks=delete удаляет всю ветку — тогда перенос или снос касается карточек всей ветки; в ответе deletedDecks и liftedSubdecks.
Параметры: moveToDeckId=<uuid> | deleteCards=1 & subdecks=lift|delete
/api/v1/vocab/decks/{id}/cardsКарточки колоды — это и есть данные редактора колод в десктоп-приложении. Все параметры необязательны: без них приходит весь список, как раньше (и в прежнем порядке: новые → учатся → знаю → освоено, внутри — по сроку). q ищет по слову, чтению, переводу и имени исходной колоды Anki; с синтаксисом в духе Anki — по полю: слово:食 · перевод:"есть рис" · чтение:た* · заметка: (пустая) · пример:… · тег:N3 · -тег:аниме · колода:"日本語::N5" (с подколодами) · is:new|learn|review|suspended|leech|due · flag:1 · level:n4 · has:audio|image; английские имена полей Anki (expression, meaning, reading, sentence) — синонимы. Без «*» значение ищется подстрокой, со «*» — шаблоном на всё поле; минус впереди — «не»; строка без синтаксиса ищется целиком, как раньше. state/pause/type/ankiDeck/tag/level/flag — те же разрезы, что у фильтров на сайте; sort — порядок (new — как пойдут новые, added, due, word, lapses, freq, status — прежний); perPage включает страницы (page — с единицы, номер за краем зажимается к последней). В ответе: total (всего в колоде), filtered (под фильтром), page, pageCount и facets со счётчиками для каждого разреза — счётчик показывает, сколько останется ПОСЛЕ включения этого фильтра. У карточки position — место новой карточки в очереди колоды: тот же порядок новых, что у GET /api/v1/queue?deckId= (с подколодами, с «Перемешать новые» и порядком колоды «по частотности» / «вперемешку»); новые на паузе тоже нумеруются, у начатых — null.
Параметры: q=<строка> & state=all|new|learning|review|relearning & pause=all|active|paused & type=all|vocab|anki & ankiDeck=<имя> & tag=<тег> & level=N5…N1|none & flag=0…7 & sort=new|added|due|word|lapses|freq|status & page=<n> & perPage=<1..1000>
{ "deck": { "id": "…", "name": "Аниме", "isDefault": false },
"cards": [ { "cardId": "…", "front": "猫", "back": "кошка", "state": "review", "due": "…" } ],
"total": 4512, "filtered": 3, "page": 1, "pageCount": 1, "perPage": 200,
"facets": { "state": { "new": 1, "learning": 0, "review": 2, "relearning": 0 },
"paused": 3, "active": 4509, "vocab": 120, "anki": 4392,
"ankiDecks": [ { "name": "日本語::N5", "count": 4392 } ],
"tags": [ { "name": "N3", "count": 812 } ], "levels": { "N5": 120, "none": 4392 }, "flags": { "0": 4500, "1": 12 } } }/api/v1/vocab/decks/{id}/cardsСоздать СВОЮ карточку в колоде (Anki-стиль, itemType=custom) — как кнопка «Добавить карточку» на сайте. Тело: { front (1..500), back (до 2000) } и необязательные reading, partOfSpeech, exampleJp, exampleRu, note. С ними front трактуется как слово, back — как перевод, а лицо и оборот собирает сервер.
→ { "ok": true, "card": { "cardId": "…", "front": "…", "back": "…" } }/api/v1/vocab/decks/{id}/bulkВставить список слов в колоду — как панель «Добавить слова списком» на сайте: каждое слово ищется в словаре и заводится полноценной карточкой. Тело: { words: string[] } (можно и одной строкой — режем по запятым, 、, точке с запятой, переносам и табам). За раз берётся 200 слов, куски длиннее 32 символов пропускаются — и то, и другое возвращается в skipped, а не выбрасывается молча. Различать их обязательно: tooLong — подмножество skipped, которое не пройдёт НИКОГДА (слать повторно бессмысленно), остальное в skipped — хвост сверх порции, его и досылают вторым запросом. elsewhere — слова, чья карточка у вас уже есть, но лежит в ДРУГОЙ колоде: сюда они не переехали, перенести можно через action=move у /api/v1/vocab/cards. Порог частоты ниже обычного: 10 отправок в минуту.
curl -X POST https://naminori.fun/api/v1/vocab/decks/<uuid>/bulk \
-H "Authorization: Bearer nmn_…" -H "Content-Type: application/json" \
-d '{"words":["母","父","兄, 姉, 弟"]}'
→ { "ok": true, "added": 4, "exists": 1, "notFound": [], "skipped": [], "tooLong": [], "total": 5,
"elsewhere": [ { "word": "母", "cardId": "<uuid>" } ] }/api/v1/vocab/decks/{id}/shuffle«Перемешать новые» колоды (с октября 2026) — как «Изменить позицию → Перемешать» в Anki и кнопка в листе колоды на сайте: новые карточки колоды и её подколод получают случайный порядок, разово (новые придут в нём при порядке «как в колоде»). Повторения и карточки на изучении не меняются. Тело необязательно: без него — все новые колоды; { cardIds: uuid[] } (до 20 000) — только выбранные новые меняются местами между собой, остальные не сдвигаются; чужие, не новые и не из этой колоды пропускаются (notNew — сколько выбранных уже не новые). Ответ: { ok, shuffled, notNew }. Ошибки: 400 bad id / bad json / bad input, 401, 404 not found (чужая или несуществующая колода), 429 (20 в минуту). Порядок после перестановки — номер карточки в очереди новых (тот же, что пишет POST /api/v1/cards reposition): у карточек колоды он ставится всем новым её дерева колод, добавленные потом встают в хвост.
curl -X POST https://naminori.fun/api/v1/vocab/decks/<uuid>/shuffle \
-H "Authorization: Bearer nmn_…"
→ { "ok": true, "shuffled": 1240, "notNew": 0 }Колоды: вид, порядок, настройки, статистика
С редизайна (октябрь 2026). Поля колоды — вид, ручной порядок, свои настройки показа, прогресс и цифры дня — описаны в записях GET и PUT /api/v1/vocab/decks раздела «Личные колоды» (тот же объект — в decks[] у /api/v1/overview), сбор новых «Все колоды» и deckInfo=1 — у GET /api/v1/queue. Здесь — ручка порядка, лист колоды, сброс прогресса колоды и ввод чтения у слов. Всё это — новые ручки, необязательные поля и параметры: клиент, который о них не знает, работает как раньше.
/api/v1/vocab/decks/orderРучной порядок колод одного уровня (перетаскивание ⠿). Тело: { parentId, ids } — parentId — родитель (null или нет ключа — верхний уровень), ids — ПОЛНЫЙ список его подколод (братьев) в новом порядке, от 1 до 200; каждой пишется position — её номер в списке. Ответ — { ok, decks }: все колоды, как в GET /api/v1/vocab/decks. Набор не совпал с братьями (колоду завели, удалили или перенесли с другого устройства; id повторяется; подколоды присланы без parentId) — 409 order mismatch: перечитайте список и повторите. Чужая или несуществующая колода в ids либо родитель — 404 not found. Колода, перенесённая к другому родителю, теряет position, новая встаёт в конец. ОСНОВНАЯ КОЛОДА (isDefault) переставляется наравне с остальными: в ids её присылают вместе с братьями, иначе 409. Пока порядок не трогали, основная (и её ветка) — первая. Своя первая порция в очереди (сайт и перемешанная выдача GET /api/v1/queue) у неё — только пока выше неё стоят одни её родители; передвинули её ниже другой колоды — она идёт на своём месте, как любая колода. Порядок списка = порядок сбора новых в «Все колоды» = порядок сайта и обеих выдач очереди. Перестановка посреди скачивания очереди ничего не теряет — см. GET /api/v1/queue?order=stable. Порог — 60 в минуту, общий с правкой колод.
curl -X PUT https://naminori.fun/api/v1/vocab/decks/order \
-H "Authorization: Bearer nmn_…" -H "Content-Type: application/json" \
-d '{"parentId":null,"ids":["<uuid>","<uuid>","<uuid>"]}'
→ { "ok": true, "decks": [ … как в GET /api/v1/vocab/decks … ] }
· 409 { "error": "order mismatch", "message": "Список колод изменился — обновите экран." }/api/v1/vocab/decks/{id}/statsЛист колоды — по ВЕТКЕ: колода и все её подколоды (deckIds), как повторение по колоде; deckId — сама колода. Дни — учебные сутки человека (его час смены дня), те же, что у очереди и дневных лимитов. progress = { total, started, mastered, percent } — как в списке колод. metrics: sinceDays — дней от создания самой ранней колоды ветки (или от первого ответа, если он раньше) до сегодня включительно; activeDays — дней с ответами; retention30 — удержание за 30 дней по ответам на карточки в повторении (state review), доля от 0 до 1, null — таких ответов меньше 10; todayMs и totalMs — время занятий колодой сегодня и за всё время (по правилу времени ответов, как у /api/v1/stats/today); wordsToday — новых карточек ветки, впервые показанных сегодня; secPerCard — секунд на карточку за 30 дней, null — замеров меньше трёх. heatmap = { from, to, days, record, avgPerActiveDay } — тепловая карта за 38 недель с понедельника: в days только дни с ответами — { date, count }, пустые дорисуйте сами, будущих дней нет; record — лучший день ЗА ВСЁ ВРЕМЯ, { date, count } (при равенстве — поздний), null — ответов не было; avgPerActiveDay — среднее по дням окна с занятиями. today — «Сегодня N из M», как в списке колод со stats=1; todayDue = { reviews, news, total, etaMin } — к повторению сегодня с лимитами и «~M мин» для кнопки «Начать». forecast — прогноз нагрузки, только с forecast=1 (иначе null): { retentionNow, secPerCard, capReviews, points }; retentionNow — нынешнее желаемое усвоение колоды; points — по усвоению 85, 90 и 95 % (и нынешнему, если оно не из них): { retention, reviewsPerDay, minutes }; capReviews — свой лимит повторений колоды (null — нет): показывайте min(reviewsPerDay, capReviews). forecast.secPerCard — секунды на карточку ветки, а при малом числе замеров — по всем колодам (metrics.secPerCard — только ветки); по ним же считаны points[].minutes и todayDue.etaMin. points[].minutes и todayDue.etaMin — null, если секунд на карточку взять неоткуда (меньше трёх замеров и в ветке, и по всем колодам). forecast: null и при forecast=1 — расчёт не уложился в потолок времени: спрячьте строку. Подпишите прогноз: смена усвоения не переносит уже назначенные сроки — первые недели нагрузка переходная. История, перенесённая импортом из Anki, в лист пока не входит. Просите forecast=1 только при раскрытом выборе усвоения. Кэш — 60 секунд (правка колоды, перенос, удаление и сброс прогресса его сбрасывают); чужая или удалённая колода — 404 not found. Порог — 60 в минуту.
Параметры: forecast=1 — приложить прогноз нагрузки
{ "deckId": "…", "deckIds": ["…", "…"],
"progress": { "total": 312, "started": 240, "mastered": 96, "percent": 30.8 },
"metrics": { "sinceDays": 41, "activeDays": 33, "retention30": 0.88, "todayMs": 540000,
"totalMs": 30600000, "wordsToday": 12, "secPerCard": 7.4 },
"heatmap": { "from": "2026-01-19", "to": "2026-10-08",
"days": [ { "date": "2026-10-08", "count": 52 } ],
"record": { "date": "2026-09-14", "count": 210 }, "avgPerActiveDay": 62 },
"today": { "introduced": 17, "limit": 30, "own": true, "extra": 0 },
"todayDue": { "reviews": 6, "news": 8, "total": 14, "etaMin": 2 },
"forecast": null }
# с forecast=1:
"forecast": { "retentionNow": 0.95, "secPerCard": 7.4, "capReviews": null,
"points": [ { "retention": 0.85, "reviewsPerDay": 31, "minutes": 4 }, … ] }/api/v1/vocab/cards (forgetDeck, unforgetDeck)«Сбросить прогресс колоды» — ветки ручки редактора карточек. { action: "forgetDeck", deckId, subdecks?, confirm } — карточки колоды снова новые (state new), журнал ответов не трогается; subdecks: true — вместе с подколодами (по умолчанию false — только сама колода). ПОДТВЕРЖДЕНИЕ — ЧИСЛОМ КАРТОЧЕК: первый вызов без confirm отвечает 400 confirm required и count — сколько карточек будет сброшено сейчас (новые не в счёт); спросите человека «Сбросить N карточек?» и повторите с confirm: N. Число не совпало (человек занимался между вопросом и ответом), прислано не число или true — снова 400 confirm required со свежим count. Успех — { ok, forgot, backupId, undoUntil }: forgot — сколько сброшено, backupId — снимок прежнего прогресса, undoUntil — до какого момента сброс можно отменить (30 дней). Сбрасывать нечего (count 0 — confirm: 0 даёт { ok, forgot: 0, backupId: null, undoUntil: null }): снимка нет, отменять нечего — unforgetDeck не зовите (без backupId он ответит 400). { action: "unforgetDeck", backupId } — отменить сброс (тост «Отменить»): { ok, restored, skipped } — restored карточкам вернули прежний прогресс, skipped — те, что после сброса уже начали учить заново (их не трогаем); повторная отмена — restored: 0; чужой, старше 30 дней или несуществующий снимок — 404 not found. Чужая или несуществующая колода — 404. Порог — 5 в минуту на обе ветки, сверх общих 240 у ручки.
curl -X POST https://naminori.fun/api/v1/vocab/cards \
-H "Authorization: Bearer nmn_…" -H "Content-Type: application/json" \
-d '{"action":"forgetDeck","deckId":"<uuid>","subdecks":true}'
→ 400 { "error": "confirm required", "message": "Это действие нужно подтвердить.", "count": 240 }
# спросить человека «Сбросить 240 карточек?» и повторить с этим числом:
-d '{"action":"forgetDeck","deckId":"<uuid>","subdecks":true,"confirm":240}'
→ { "ok": true, "forgot": 240, "backupId": "<uuid>", "undoUntil": "2026-11-07T09:12:00.000Z" }
# «Отменить»:
-d '{"action":"unforgetDeck","backupId":"<uuid>"}'
→ { "ok": true, "restored": 238, "skipped": 2 }/api/v1/review (ввод чтения)«ВВОДИТЬ ЧТЕНИЕ» У СЛОВ: человек печатает чтение, и неверное чтение — это «Снова». Включено у колоды карточки — effective.typeReading (колода перекрывает общую vocabTypeReading из /api/v1/me и /api/v1/settings); в «Все колоды» колоду карточки даёт GET /api/v1/queue?deckInfo=1. Новой ручки и новых полей в очереди нет: всё решает клиент. КОГДА ВВОД УМЕСТЕН. Чтение — kana у слова курса (kind vocab) и ВЕРХНЕЕ reading у карточки импорта (kind anki), а не view.reading: там бывает «スカート 2» с номером акцента, а у карточки без чтения в колоде — догадка автоматики, которую у омографов вроде 今日 можно не угадать. Показанное слово — jp и view.word. Ввод не предлагайте (обычная самооценка), если чтения нет, если в нём ни одного чтения каной («CD», «そら 2») или если слово само записано только каной (コーヒー, とにかく — ввод был бы перепечаткой показанного). У карточек колод со структурой полей (Yomitan, Lapis) верхнего reading обычно нет — им ввод пока не предлагается. Несколько чтений в строке — через ・、,,//;; и полуширинные ・ 、; точка окуриганы (た.べる) не считается; у составного катакана-чтения через ・ (コーヒー・カップ) верно и целиком, и любой кусок. ПРЕДПРОВЕРКА ВВОДА — до проверки: ромадзи переведите в кану (висящая n — в ん), пустое не отправляйте. Набранное не каной — НЕ ответ и не «Снова», а просьба: ввод остаётся в поле, оценки нет. Причина (при смеси — первая по порядку): cyrillic — есть кириллица, не та раскладка («переключите раскладку»); kanji — есть иероглиф, японская IME сконвертировала чтение («введите каной, без преобразования»); latin — всё прочее не-кана: латиница, цифры, знаки внутри слова, эмодзи («введите каной»). ПРОВЕРКА. Сравнение — как у ответа грамматики: невидимые знаки, ширина знаков, хвостовая пунктуация, пробелы и регистр не важны, катакана = хирагана («ー» в гласную не раскрывается); плюс следы клавиатуры: ぢ = じ, づ = ず и ромадзи-IME вокруг ん (minna — みんあ засчитывается как みんな, honya — ほにゃ как ほんや). correct — совпало с любым чтением: покажите оборот и обычные кнопки; almost — одно отличие при вводе от 4 знаков: встряхните поле и дайте ещё попытку; wrong — «Снова» (rating: 1) без вопроса. ОЦЕНКУ РЕШАЕТ КЛИЕНТ. В POST /api/v1/review (и в позиции /review/batch) — rating (неверное чтение — 1, верное — обычная самооценка), answerMode: "input", givenAnswer — набранное чтение (последняя попытка) и wasAlmost: true, если была попытка «почти». Сервер оценку не перепроверяет: что прислано, то и записано. Карточки слов, на которые хоть раз отвечали вводом, личный подбор весов FSRS не берёт целиком (их история — уже не самооценка), и в reviewCount у GET /api/v1/settings их ответов нет; так же — пункты грамматики с ответом вводом (клоуз).
# неверное чтение — «Снова»; набранное и попытка «почти» — в журнал
curl -X POST https://naminori.fun/api/v1/review \
-H "Authorization: Bearer nmn_…" -H "Content-Type: application/json" \
-d '{"cardId":"<uuid>","rating":1,"answerMode":"input","givenAnswer":"たべる","wasAlmost":true}'Редактор карточек
/api/v1/vocab/cardsОперации над своими карточками слов: move (перенести в другую колоду), forget (сбросить прогресс, state=new), delete (удалить из повторений), update (личная версия текста), suspend (снять с повторений и вернуть — тоггл над ОДНОЙ карточкой), setSuspended (массовая пауза/возврат: cardIds + suspended, направление задаётся явно), bury (отложить до следующего учебного дня), scene (правка контекста «откуда слово»: предложение, подпись источника, кадр, звук), forgetDeck и unforgetDeck (сброс прогресса колоды с подтверждением числом карточек и отменой в течение 30 дней — см. раздел «Колоды: вид, порядок, настройки, статистика»). Пакетные — move, forget, delete и setSuspended, до 1000 id за раз; остальные работают с одной карточкой. Массовую паузу делайте именно setSuspended: одиночный suspend на тысяче карточек упрётся в порог частоты и бросит работу на середине. Единственное место, где API удаляет карточки — сделано под редактор колод; общий /api/v1/cards по-прежнему без деструктивных операций. На чужой или несуществующей карточке операции отвечают ok: false, а не молчаливым успехом.
curl -X POST https://naminori.fun/api/v1/vocab/cards \
-H "Authorization: Bearer nmn_…" -H "Content-Type: application/json" \
-d '{"action":"move","cardIds":["<uuid>"],"toDeckId":"<uuid>"}'
→ { "ok": true, "moved": 1 }
# личная версия текста: "" снимает правку и возвращает оригинал словаря,
# отсутствие поля — «не трогать» (как Edit на сайте)
-d '{"action":"update","cardId":"<uuid>","back":"кошка (разг.)","note":"из 3 серии"}'
# снять с повторений (тоггл) и отложить до завтра
-d '{"action":"suspend","cardId":"<uuid>"}' → { "ok": true, "suspended": true }
# массово — направление явное, changed = сколько строк реально изменилось
-d '{"action":"setSuspended","cardIds":["<uuid>"],"suspended":true}'
→ { "ok": true, "changed": 1 }
-d '{"action":"bury","cardId":"<uuid>"}' → { "ok": true, "buriedUntil": "…" }
# «до завтра» двигает buriedUntil, а НЕ due: расписание FSRS не трогается,
# карточка лишь прячется из сегодняшней очереди.
# правка сцены: "" убирает кадр или звук, отсутствие поля — «не трогать».
# image/audio принимают ТОЛЬКО наш путь /api/media/<файл> из POST /api/media/store
-d '{"action":"scene","cardId":"<uuid>","snippet":"…","image":"/api/media/9f1c….webp"}'Словарь и лукап
/api/v1/lookup?q=食べた«Умный» лукап как в расширении: деинфлексия (食べた→食べる), совпадения всех длин секциями (Yomitan-style), питч. Скан идёт слева направо и продолжается после первого слова, поэтому 今学期 отдаёт и 今, и 学期. matchLength — сколько символов запроса покрыло ПЕРВОЕ слово (то, что подсвечивают под курсором), а не весь разобранный кусок. ОБЯЗАТЕЛЬНО смотрите headGroups: столько ПЕРВЫХ элементов groups относятся к слову под курсором, остальные — следующие слова строки; клиент, который этого не знает, покажет чужие статьи как значения того же слова. contextual: true — длина выбрана с оглядкой на продолжение (結んで в 「結んでもらおうか」, а не жадное 結んでも); такой ответ нельзя кэшировать по «форме + одному следующему символу». jitenRank — ЧАСТОТНОСТЬ главного слова: место пары «написание + чтение» в частотном списке Jiten, 1 — самое частое; слова в списке нет — поля нет вовсе (см. раздел «Частотность слов» выше). jitenKanaRank — место того же слова, записанного каной (только у слова с кандзи). Те же два поля — у каждой группы groups[], для её собственной пары. groups[].written — как слово написано в запросе (словарной формой), когда это не jp статьи: うるさい в тексте у статьи 煩い, катакана ウルサイ; нет поля — написание то же. Его и шлите полем written в POST /api/vocab/add (и /api/v1/vocab/mine) — сервер уже посчитал; у view=1 то же лежит в card.written. Пустой q — не ошибка: 200 и hit: null.
Параметры: q=слово (до 64 символов; длиннее — 400 { error: "query too long", maxLength: 64 }) · full=1 — статья ЦЕЛИКОМ (по умолчанию каждое значение режется до 350 знаков с пометкой «показана не полностью»: для попапа по наведению это правильно, для карточки повторения — нет) · ws=слово1,слово2 — пакет вместо q, чтобы вытянуть весь абзац заранее одним запросом (до 80 слов и 5200 символов строки; лишние слова отбрасываются молча, слишком длинная строка — 400 batch too long) · src=jitendex_ru,warodai,jitendex — ограничить словари (kolobok_light — «Колобок Лайт»: статьи Колобка коротко, без примеров; вместе с jitendex_ru отдаётся полный); БЕЗ src отдаются все — Колобок, Warodai, Jitendex и грамматика, — настройку пользователя ручка не читает, неизвестные имена отбрасываются. jmdict с 13.10.2026 заменён Колобком Лайт: в «все» не входит, по имени пока отвечает и будет отключён
{ "query": "食べた", "hit": { "jp": "食べる", "kana": "たべる", "matchLength": 3,
"headGroups": 1, "ru": "есть; кушать", "jitenRank": 190,
"groups": [ { "jp": "食べる", "kana": "たべる", "accent": 2, "senses": [ { "source": "jitendex_ru", "ru": "есть; кушать" } ] } ] } }
пакет на абзац (ключ в hits — ровно та строка, что пришла в ws=; null означает
«слова нет в словаре» — это ответ, его и надо класть в свой кэш):
GET /api/v1/lookup?ws=猫,が,存在しない単語
{ "queries": ["猫","が","存在しない単語"],
"hits": { "猫": { … }, "が": { … }, "存在しない単語": null } }/api/v1/dict?q=ねこПростой поиск по словарю: до 30 статей. Намеренно облегчённый в сравнении с /api/v1/lookup — без деинфлексии и питча, зато ищет и по русскому значению: запрос кириллицей автоматически превращается в поиск по переводу (сначала «значение начинается с этого слова», в конце — случайные вхождения подстроки). Форма ответа НЕ такая, как у лукапа: в каждом hit ровно одна группа, значения не обрезаются, поиск по префиксу запускается только если точное совпадение ничего не дало. Словари всегда все — настройка пользователя здесь не учитывается.
{ "query": "ねこ", "hits": [ { "jp": "猫", "kana": "ねこ", "ru": "кошка" } ] }/api/v1/search?q=…Поиск с главного экрана — грамматика и слова с тем же ранжированием, что окно Cmd+K на сайте. Ищет по японскому, кане, ромадзи и русскому значению. Ответ {ok, grammar, dict}: grammar — до 12 пунктов {slug, title, kana, meaningRu, level, published} (только опубликованные — черновиков ручка не отдаёт, published всегда true), dict — слова {term, reading, ru} по словарям, выбранным человеком в настройках. Пустой запрос или длиннее 64 символов — пустая выдача, а не 400: строку поиска набирают по букве. Ответ кэшируется браузером на минуту (Cache-Control: private). Порог — 240 в минуту, как у лукапа.
Параметры: q — строка поиска (до 64 символов)
{ "ok": true,
"grammar": [ { "slug": "temo-ii", "title": "〜てもいい", "kana": "てもいい", "meaningRu": "можно; разрешено",
"level": "N5", "published": true } ],
"dict": [ { "term": "猫", "reading": "ねこ", "ru": "кошка" } ] }/api/vocab/addДобавить слово в SRS (майнинг). Опционально text-контекст «откуда слово» — покажется на повторении. Поля text.image / text.audio принимают ТОЛЬКО наш относительный путь вида /api/media/<файл> — его выдаёт POST /api/media/store (см. ниже). Необязательный deckId кладёт слово в конкретную личную колоду. Кроме text есть второй вид контекста — video: сцена YouTube с тайм-кодом реплики, на повторении карточка покажет её. КАК СЛОВО НАПИСАНО В ТЕКСТЕ (с октября 2026, только добавлением): необязательное written — запись из текста в словарной форме, когда она не совпадает с jp статьи (うるさい у статьи 煩い, катакана ウルサイ): новая карточка заводится этим лицом (frontOverride), статья, значения и дедуп — по jp/kana. Слово уже в колоде — дубля нет, лицо не меняется, а ответ называет, каким написанием карточка лежит: via (нет поля — лежит тем, что прислали). Тот же written принимает POST /api/v1/vocab/mine. ЕЩЁ ОДИН ПРИМЕР: у слова, что уже в колоде, новая фраза (или другой ролик) не заменяет сцену карточки, а ложится ещё одним примером — ответ тогда несёт context: "example"; context: "scene" — фраза заполнила пустую сцену; нет поля — ничего не добавилось (та же фраза, само слово).
Параметры: тело: jp · kana · ru · written? · deckId? · text? {kind: reading|manga|grammar|asbplayer, sourceUrl, sourceTitle, snippet, image?, audio?} · video? {videoId, startMs, endMs, subtitle}
curl -X POST https://naminori.fun/api/vocab/add \
-H "Authorization: Bearer nmn_…" -H "Content-Type: application/json" \
-d '{"jp":"猫","kana":"ねこ","ru":"кошка","deckId":"<uuid>",
"text":{"kind":"reading","sourceUrl":"https://…","sourceTitle":"…","snippet":"…"}}'
то же, но контекст — сцена YouTube:
-d '{"jp":"猫","kana":"ねこ","ru":"кошка",
"video":{"videoId":"dQw4w9WgXcQ","startMs":61200,"endMs":63400,"subtitle":"猫が好きです。"}}'/api/media/storeЗагрузить кадр сцены или аудио реплики (multipart/form-data, поле file, до 8 МБ; jpg/png/webp/avif, mp3/m4a/aac/ogg/weba/wav — тип проверяется по содержимому). Возвращает относительный URL, который нужно положить в text.image / text.audio у POST /api/vocab/add. Без этого шага прикрепить медиа-контекст к намайненному слову нельзя: внешние ссылки схема не принимает. Звук в форматах webm/ogg/wav перекодируется в mp3 — иначе на iPhone он молча не играет. WAV без сигнала (пик ниже 0,005 полной шкалы, −46 dBFS — пустая запись) не принимается: 422 silent audio; слово в таком случае добавляйте без звука. Остальные форматы звука на тишину не проверяются.
curl -X POST https://naminori.fun/api/media/store \
-H "Authorization: Bearer nmn_…" -F "file=@scene.jpg"
→ { "ok": true, "url": "/api/media/9f1c….jpg", "name": "9f1c….jpg" }/api/v1/audio?q=学校Озвучка любого японского текста (MP3, edge-tts, голос Nanami). До 200 символов, 120 запросов/мин. Для слова можно добавить пару jp и kana (…&jp=橋&kana=はし): если для неё есть живая запись носителя, придёт она — у 橋 и 箸 чтение одно, а звучат они по-разному; записи нет, а чтение делят слова разного тона (看護 и 漢語 — かんご), — свой клип слова с его ударением; иначе синтез по q.
/api/v1/translateПеревод одной японской реплики на русский силами сервера (Gemini). Нужен клиентам, которые показывают перевод на лету — например, оверлею для визуальных новелл. Почему через нас, а не напрямую: бесключевой веб-эндпоинт Google Translate отвечает антибот-страницей и 429 на каждый запрос (режется адрес, а не HTTP-клиент), у сервера же Gemini ходит через прокси с ретраями. Повтор той же реплики (у любого человека, и после перезапуска сервера) отдаётся из общего кэша — мгновенно и не тратя квоту. На нашем ключе у перевода суточный потолок на человека; когда он выбран или ИИ занят, сервер пробует бесплатный запасной переводчик и только потом отказывает. Если человек сохранил на сайте свой ключ ИИ, перевод идёт на нём (его поставщик, его счёт), а не нашим Gemini; отказ его поставщика приходит 502 с причиной, начинающейся с «Ваш ключ ИИ:».
Параметры: тело: text (japanese, режется до 400 символов) · surface (необязательно: desktop, extension, mobile… — подпись в учёте расхода ключа, прав не даёт). Пустой — 400 empty text, нераспарсенное тело — 400 bad json. Отказ: в error — русская фраза для человека, в code — причина: daily_limit (429, суточный лимит, завтра снова), too_fast (429, больше 60 фраз в минуту), busy (503, ИИ перегружен, заголовок Retry-After и поле retryAfterSec), failed (502, ошибка поставщика)
curl -X POST https://naminori.fun/api/v1/translate \
-H "Authorization: Bearer nmn_…" -H "Content-Type: application/json" \
-d '{"text":"また明日ね"}'
→ { "ok": true, "text": "До завтра!" } # повтор: + "cached": true/api/v1/furiganaЧтения японских фраз — разметкой {漢字|かな}, окуригана снаружи ({笑|わら}う). Тот же движок, что размечает песни и главы книг на сайте (kuromoji и наши поправки чтений). Нужен клиенту, у которого фраза пришла без чтений: намайненный абзац, реплика ролика, пример словарной статьи. Ответ — словарь «фраза → разметка»; фразу без кандзи сервер возвращает как есть, отсеянные (пустые, длиннее предела, сверх пачки) в ответ не попадают. Чтения имён и редких слов движок иногда угадывает неверно.
Параметры: тело: texts — до 40 фраз, каждая до 1000 знаков, всего до 8000 знаков. Работа считается как у разбора: 200 единиц по 2000 знаков в минуту на человека, сверх — 429 rate limited. Движок недоступен — 503 unavailable
curl -X POST https://naminori.fun/api/v1/furigana \
-H "Authorization: Bearer nmn_…" -H "Content-Type: application/json" \
-d '{"texts":["保存用メッセージを送った。"]}'
→ { "ok": true, "m": { "保存用メッセージを送った。": "{保存|ほぞん}{用|よう}メッセージを{送|おく}った。" } }/api/v1/grammar/lookup?q=てもいい«Что это за конструкция»: по куску японского находит грамматические пункты Naminori. Нужен клиенту, который разбирает предложение и хочет показать не только слова, но и грамматику. Поле total — это число ВОЗВРАЩЁННЫХ пунктов (после limit), а не сколько всего совпало.
Параметры: q — до 80 символов (в ответе возвращается уже обрезанным) · limit
/api/v1/dicts/naminori-grammarСловарь грамматики Naminori (конструкции N5–N1 с размеченными поверхностями) в формате Yomitan v3 zip — тем же способом, что и словарь слов. Параметр v НЕ выбирает версию: собирается и отдаётся всегда текущий архив, а v влияет только на заголовки кэширования (совпал с текущей ревизией — immutable, иначе no-cache).
Параметры: v — ревизия для кэш-заголовков (необязательно)
/api/v1/dicts/naminori-ru/meta«Паспорт» словарного пакета: ревизия и РАСКЛАД ПО ИСТОЧНИКАМ (сколько статей из каждого словаря). По нему клиент понимает, что скачанный когда-то пакет устарел. Судить надо именно по раскладу, а не по числу статей: 23.08.2026 с сервера пропал основной русский словарь, пакет пересобрался без него, и число термов осталось прежним — подмену по количеству заметить было нельзя. Поле frequency — отпечаток частотности внутри пакета (порядок омографов и ранги Jiten); он же входит в ревизию: сменилась частотность — сменилась и ревизия, хотя статей столько же. Публично, без токена.
/api/v1/dicts/namesПакет имён для офлайна: фамилии, имена и места из словаря IPADIC (тот же источник, что у блока «Имя или название» в ответе лукапа). Текст (text/plain), первая строка — заголовок «#naminori-names 1», дальше по строке на запись: «запись<TAB>чтение:виды[:кириллица],…». Виды буквами: f — фамилия, g — имя, p — имя человека, t — топоним, c — страна; кириллица по Поливанову — только у людей. «~» перед чтением — чтение заметно реже лучшего: показывать его стоит, только когда сосед сужает вид (田中町 — топоним). Около 3 МБ текста, по сети ~0,8 МБ (сжатие). ETag — ревизия, повтор с If-None-Match — 304. Публично, без токена.
/api/media/{файл}Отдаёт намайненный кадр или звук реплики, ранее загруженный через POST /api/media/store. Публично (адрес — случайный UUID). ВНИМАНИЕ: у этой ручки, в отличие от всех /api/v1/*, НЕТ CORS — кросс-доменный fetch из расширения или веб-клиента упрётся в политику; в <img> и <audio> файл работает как обычно. Поддерживает Range: без этого <audio> в iOS Safari не проигрывает файл.
/api/v1/dicts/naminori-ruГотовый словарь Naminori (Yomitan v3 zip, питч Kanjium и частотность форм) — для импорта в Yomitan-совместимые клиенты и наше расширение. Основной режим — пакет на один словарь: src=jitendex_ru (Колобок + грамматика Naminori), src=kolobok_light (Колобок Лайт — тот же словарь коротко, тоже с грамматикой; ставится вместо полного), src=jitendex; у таких пакетов постоянные названия. Без src — полный пакет, src=a,b — комбинированный (оба для старых клиентов; src=jmdict — тоже только для них); грамматика Naminori только там, где есть Колобок. Готовый пакет держится в кэше сутки; если его пришлось собирать заново — не чаще трёх сборок за 10 минут с адреса (иначе 429), а одновременные сборки встают в очередь (503, если очередь занята).
Встречи слов
Где человек уже встречал слово: лукапы и прочитанное/просмотренное в его собственном контенте. Личные данные — видны только владельцу токена; удаление и выгрузка — только на сайте (/encounters). Если человек выключил запись, ответ POST — { disabled: "all" } (или "seen" — только увиденное), и клиенту стоит перестать слать на время.
/api/v1/encountersПачка встреч. Копите на клиенте и шлите раз в 10–15 с, не на каждое наведение. lookup — человек ОТКРЫЛ статью (не предзагрузка и не токенизация строки); seen — прочитал или посмотрел (абзац, реплика субтитров, строка игры) — абзац сервер сам режет на предложения. Повтор той же фразы не плодит строк (дедуп по хэшу), повтор лукапа — счётчик. Адрес хранится без query и hash (у ютуба — только v); почта, мессенджеры и банки отбрасываются целиком. Медиа — только ссылки на уже существующие файлы: /api/media/… или локальный файл клиента (local).
Параметры: тело: items (1–100, до 20 000 знаков текста): t (lookup|seen) · lemma (словарная форма, обязательна у lookup) · reading · surface (форма под курсором) · text (предложение или абзац) · src: { kind: book|manga|youtube|video|web|game|hook|clip|grammar|site|other, key?, title?, url? } · loc?: { ms, endMs | ch, p | page } · media?: { image | local } · at (мс, не старше недели). Разбор новых предложений — до 60 000 знаков в минуту, сверх — 429 busy с retryAfterSec (повторите пачку целиком).
curl -X POST https://naminori.fun/api/v1/encounters \
-H "Authorization: Bearer nmn_…" -H "Content-Type: application/json" \
-d '{"items":[{"t":"lookup","lemma":"食べる","surface":"食べた","text":"昨日パンを食べた。","src":{"kind":"web","url":"https://example.jp/a","title":"Статья"}}]}'
→ { "ok": true, "stored": 1, "skipped": 0 }/api/v1/encounters/summary?w=この&r=このДля попапа лукапа: в скольких ваших предложениях встречалось слово (n), сколько раз искали (looked), в скольких источниках (sources), 3 последних предложения с источником и местом, и частые соседи — «чаще всего у вас: この人 (12)» (только если предложений ≥ 4). Зовите ОТДЕЛЬНО, после показа статьи: лукап от этого не ждёт. Кэш браузера — минута.
Параметры: w — лемма · r — чтение (необязательно; kuromoji даёт словарную форму в том написании, каким слово было в тексте) · k — знаки слова (необязательно, до 24 кандзи): тогда в ответе ещё kanji: [{ c, n }] — в скольких ваших предложениях стоит каждый знак
/api/v1/encounters?w=猫Ваши предложения — со словом, со знаком или все, новые сверху, с источником и местом (loc). next — курсор для следующей страницы (before=). Та же выборка, что на странице «Где встречались слова» (/encounters?w=&r= или ?k=) — ведите туда ссылку «Где встречалось» из своего лукапа.
Параметры: w · r (чтение: слово, написанное каной, тоже найдётся) · k (один кандзи — предложения, где он стоит) · source (id источника) · before · limit (до 200)
/api/v1/encounters/sourcesИсточники встреч (книги, ролики, сайты, игры) с числом предложений и лукапов.
/api/v1/encounters/topСамые встречаемые у вас слова (словарные формы, от двух предложений).
Параметры: limit (до 500)
Контент
/api/v1/content-suggestions«⚑ Ошибка?» — сообщить об ошибке во фразе «В предложении» или в примере к слову курса; разбирают проверяющие сайта. objectType: context — фраза корпуса, objectId — context.sentenceId из /api/v1/queue?context=1 (у своей фразы колоды его нет — жаловаться не на что); vocab_example — пример слова курса (exampleJp/exampleRu), objectId — wordId элемента очереди (только у слов с уровнем). reason: translation (неверный перевод), japanese (ошибка в японском), unnatural (неестественно); proposed — «как правильно», необязательно, до 500 знаков. Повторная жалоба того же человека на тот же объект правит прежнюю. hidden: true — третья «ошибка в японском» от разных людей: фраза снята из выдачи до проверки. 404 — фразы уже нет или её сняли; 429 — больше 30 жалоб за сутки (или слишком часто); в обоих message — для человека.
{ "objectType": "context", "objectId": 4512, "reason": "translation", "proposed": "Она помыла четыре тарелки." }
→ { "ok": true, "hidden": false, "message": "Спасибо! Проверяющие посмотрят и поправят курс." }/api/v1/reading/books/{id}Книга или том манги с полки «Чтения»: место чтения (progress) и оглавление без содержимого. У книги — chapters (i, title, chars — знаков в главе, furigana, images) и pace — скорость чтения с готовыми подписями (view). У манги — pages: i, w × h картинки, blocks — сколько блоков распознанного текста, chars — сумма длин строк этих блоков (знаков на странице; 0 — текста нет или он ещё распознаётся), rtl — направление листания из настройки аккаунта (меняется PATCH /api/v1/settings { mangaRtl }) и pace — скорость манги, своя, отдельно от книг: volume: true, view.chip — средняя по тому. Блоки страницы — /api/v1/reading/books/{id}/page/{n}, картинка — /api/v1/reading/books/{id}/image/{n}. Чужая и несуществующая книга — один и тот же 404.
{ "book": { "id": "…", "title": "空色ポスト 1", "format": "manga", "ocrStatus": "ready" },
"progress": { "chapterIndex": 11, "paragraphIndex": 0, "percent": 6 },
"rtl": true,
"pages": [ { "i": 0, "w": 1066, "h": 1600, "blocks": 0, "chars": 0 }, { "i": 1, "w": 1066, "h": 1600, "blocks": 5, "chars": 87 } ] }/api/v1/grammarКаталог опубликованной грамматики N5–N1 в методическом порядке. total — сколько пунктов подошло ВСЕГО (не размер страницы), так что каталог можно листать по offset.
Параметры: level=N5..N1 · q=поиск (название/кана/ромадзи/перевод) · limit=1..600 (100) · offset=0…
{ "total": 552, "limit": 100, "offset": 0, "points": [ { "slug": "temo-ii", "level": "N5", "title": "〜てもいい", "meaningRu": "можно; разрешено" } ] }/api/v1/grammar/temo-iiПолный пункт: объяснение (markdown), нюансы, структура, похожие конструкции + до 30 примеров. Разметка фуриганы `{漢|かな}`, целевая конструкция в `<g>…</g>`.
{ "point": { "slug": "temo-ii", "title": "〜てもいい", "explanationMd": "…", "structure": […] },
"examples": [ { "jp": "{食|た}べ<g>てもいい</g>ですか。", "ru": "Можно поесть?", "answer": "てもいい" } ] }/api/v1/reading/books/{id}Одна книга или том манги владельца токена: book, progress и оглавление — у книги chapters [{ i, title, chars, furigana, images }] и pace, у манги pages [{ i, w, h, blocks }] (blocks — ЧИСЛО распознанных блоков страницы, сами блоки — GET …/page/{n}) и pace — средняя по тому (volume: true). bytes — сколько весят файлы книги на сервере: у манги все страницы, у книги — её картинки и обложка (текст глав сюда не входит); по нему клиент показывает размер перед «Скачать на телефон». Чужая и несуществующая книга — один и тот же 404.
/api/v1/grammar/lookup?q=食べてもいいですか«Что это за конструкция»: запрос нормализуется (тильда-заполнитель, многоточие, скобочные пояснения, полноширинные пробелы) и результаты ранжируются — точное совпадение, начало, вхождение в любую сторону. Главное отличие от каталожного ?q= — вхождение «наоборот»: живая фраза 食べてもいいですか находит пункт 〜てもいい, чего ILIKE по названию не сделает никогда.
Параметры: q=конструкция (до 80 символов) · limit=1..10 (5)
/api/v1/grammar/checkПроверить ответ на грамматическую клозу ТОЧНО как на сайте: сначала ромадзи → кана (как поле ввода в тренажёре), затем общая проверка (кандзи↔кана, эквивалентные формы, вежливость). Позволяет принимать ответ, набранный латиницей, и не переписывать выверенную логику приёма у себя. Тело: { input, answer, alternatives?, pointSlug?, traps?, after? } — answer, alternatives, traps и after приходят из GET /api/v1/queue?type=grammar, возвращайте их как есть. after — текст предложения после пропуска: по нему проверка видит, грамотен ли в этом месте тот же ответ в другом регистре (перед «よ。» и в конце фразы «なくてもいいです» засчитывается наравне с «なくてもいい», перед «と思う» — нет), и не считает ошибкой частицу, повторяющую ту, что стоит сразу после пропуска. Пустая строка — законное значение (пропуск в самом конце). Без after сервер находит хвост сам по pointSlug и ответу и засчитывает только то, что грамотно во всех примерах пункта с этим ответом; остальное остаётся второй попыткой (verdict wrong_register, не ошибка). Вердикты: correct · almost (опечатка) · trap (другая верная конструкция) · wrong_register (тот же ответ не в той форме) — на эти три давайте вторую попытку без оценки и показывайте messageRu; wrong — ошибка. У trap поле trapKind: "form" — ловушка на ошибку формы той же конструкции (не то время глагола у 〜とき), а не другая конструкция: показывайте messageRu без слов «здесь другая конструкция»; null — обычная ловушка.
curl -X POST https://naminori.fun/api/v1/grammar/check \
-H "Authorization: Bearer nmn_…" -H "Content-Type: application/json" \
-d '{"input":"nakutemoiidesu","answer":"なくてもいい","pointSlug":"nakute-mo-ii","after":"よ。"}'
→ { "correct": true, "verdict": "correct", "converted": "なくてもいいです", "answer": "なくてもいい", "messageRu": null }/api/v1/decksПубличные колоды сообщества (по популярности, до 100). В каждой строке — id, title, description, level, saves (сколько раз сохранили), items (карточек) и url — адрес колоды на сайте. Дальнейшая работа с колодой (посмотреть состав, сохранить себе) возможна только на сайте: ручек /decks/{id} в API нет.
{ "decks": [ { "id": "…", "title": "Кухня и еда", "level": "N4",
"saves": 128, "items": 300, "url": "/decks/shared/…" } ] }/api/v1/youtube/picksПодборка японских роликов по сложности ДЛЯ ВАС — до открытия ролика. Ролики — только из корпуса «Сцен» (их субтитры разобраны заранее). newPct — доля словоупотреблений ролика, которых у вас ещё нет в изученном: знакомым считается слово, карточку которого хоть раз показывали (в том числе «Уже знаю»; карточка в new — ещё новое), отложенное целиком выпадает из счёта, частицы и связки идут в знаменатель как знакомые, имена и числа не считаются. Если в настройках выбран целевой уровень, слова JLPT всех уровней ниже него тоже считаются знакомыми — это basis.assumedUpTo (null — не домысливаем); basis.known — сколько знакомых написаний дали ваши карточки. Ступени — по целому проценту: easy меньше 5, normal 5–15, hard больше 15; counts — сколько роликов в каждой. growth — «слов в зоне роста»: новые слова, которые хоть раз стоят во фразе, где всё остальное вам знакомо (он считается по склеенным фразам корпуса и потому обычно меньше числа «рекомендованных» реплик в открытом ролике). Это оценка до просмотра, а не «100 − понятность» из /api/v1/comprehension: числа не обязаны складываться. Порядок: не смотренные раньше смотренных, ближе к вашим интересам и знакомым каналам — выше; не больше двух роликов канала в начале списка. С seed порядок — жребий этого зерна (близость к интересам и посильность — его вес, не жёсткий порядок): шлите новое зерно на каждый заход в список и то же — с offset для следующих страниц, тогда страницы не повторяются и не теряют роликов; seed в ответе — эхо (null — порядок без зерна, прежний). jlpt — уровень JLPT ролика по его словам (N5…N1, строкой; один для всех людей): самый лёгкий уровень, слова которого вместе с более лёгкими покрывают 95 % употреблений ролика из списков JLPT (частицы и связки — к N5, слова вне списков — имена, сленг — уровень не поднимают); dist — доли употреблений N5, N4, N3, N2, N1 и «вне списков» в целых процентах; оба null, если уровней слов на сервере нет. rubric — короткая рубрика (тема ролика или жанр канала) или null. durationSec — длительность в секундах (если YouTube её не сообщил — до конца последней реплики субтитров) или null. corpus.measured — у скольких роликов уже есть разбор; 0 — подборки пока нет, спрячьте её. Фраз субтитров и слов ролика ответ не несёт. Пустая ступень — не ошибка; сбой — 500. Лимит 30 запросов в минуту.
Параметры: level=easy|normal|hard (normal; незнакомое — normal) · limit=1..50 (20) · offset=0..1000 (0) · seed=1..2147483647 (зерно захода; нет или мусор — порядок без зерна)
curl "https://naminori.fun/api/v1/youtube/picks?level=normal&seed=48151623" -H "Authorization: Bearer nmn_…"
→ { "ok": true, "level": "normal", "counts": { "easy": 0, "normal": 247, "hard": 199 },
"basis": { "known": 4120, "assumedUpTo": "N4" },
"items": [ { "videoId": "…", "title": "…", "channel": "…",
"thumb": "https://i.ytimg.com/vi/…/mqdefault.jpg",
"newPct": 9, "growth": 6, "rubric": { "key": "vlog", "title": "Влоги" }, "durationSec": 1872,
"jlpt": "N3", "dist": [78, 5, 6, 2, 2, 7] } ],
"next": 20, "corpus": { "videos": 472, "measured": 446 }, "seed": 48151623 }/api/v1/youtube/captionsЯпонские субтитры ролика — те же, что у майнинга YouTube на сайте: реплики с началом и длительностью в миллисекундах (durMs — сколько строка на экране: растянута до следующей реплики, чтобы не мигала). Ролик, который кто-то уже открывал, отдаётся сразу из сохранённых; новый сервер скачивает у YouTube — до минуты с лишним, отвечайте ожиданием, а не повтором. kind: manual — субтитры автора ролика, unknown — какие нашлись (бывают и распознанные YouTube). Отказ — { ok: false, error } с понятным человеку текстом; reason: busy — сервер занят (429, ждать retryAfterSec секунд — срок есть и в теле, и в Retry-After), wall — YouTube временно не пускает сервер (тоже со сроком), net — сбой, который стоит повторить позже (срок неизвестен); без reason — отказ про сам ролик (нет японских субтитров, закрыт, удалён), повтор не поможет. Кривая ссылка — 400. Лимит новых роликов — 10 в минуту, общий с сайтом; всех запросов ручки — 120 в минуту (сверх — 429 с reason: busy).
Параметры: v=id ролика (11 знаков) или ссылка на него (youtube.com/watch?v=…, youtu.be/…, /shorts/…)
curl "https://naminori.fun/api/v1/youtube/captions?v=dQw4w9WgXcQ" -H "Authorization: Bearer nmn_…"
→ { "ok": true, "lang": "ja", "kind": "unknown", "cues": [ { "startMs": 0, "durMs": 2400, "text": "こんにちは" } ] }/api/v1/youtube/favorites«Смотреть позже» — те же ролики, что под звёздочкой на сайте. GET → {ok, items: [{videoId, title, channel, durationSec, addedAt, lastOpenedAt}]}, недавно открытые первыми; title, channel и durationSec бывают null; сбой базы — 500 server error, а не пустой список (пустой ответ значит «роликов нет» — его можно кэшировать). POST {videoId (11 знаков), title?, channel?, durationSec?, restore?} → {ok, favorite}: положить ролик; restore {addedAt, lastOpenedAt} (ISO) — «Вернуть» после «убрать»: строка заводится с прежними датами (позже «сейчас» — нет). Повтор того же ролика — та же строка (дата добавления прежняя, пустые поля дополняются, «открыт» — сейчас); без названия или канала сервер сам спросит YouTube. Больше 100 роликов — 409 favorites full (освежить уже лежащий можно и на потолке). Кривое тело — 400 invalid body, нераспарсенное — 400 invalid json. «Открыт» (lastOpenedAt) отдельной ручкой не ставится: его отмечает пульс POST /api/v1/time/heartbeat с surface youtube и ref = id ролика — даже при выключенном «Считать автоматически». Пороги: чтение 120, запись 30 в минуту.
curl -X POST https://naminori.fun/api/v1/youtube/favorites -H "Authorization: Bearer nmn_…" \
-H "Content-Type: application/json" -d '{"videoId":"dQw4w9WgXcQ"}'
→ { "ok": true, "favorite": { "videoId": "dQw4w9WgXcQ", "title": "…", "channel": "…", "durationSec": 213,
"addedAt": "2026-10-08T09:00:00.000Z", "lastOpenedAt": "2026-10-08T09:00:00.000Z" } }/api/v1/youtube/favorites/{videoId}Убрать ролик из «Смотреть позже» → {ok: true}. Ролика и так нет (убрали с другого устройства, повтор после обрыва связи) — тоже {ok: true}: удаляется только своя строка, чужой у этой ручки не бывает. Кривой id — 400 invalid id. Порог — общий с POST, 30 в минуту.
Служебное
/api/v1/extension/versionВерсия расширения в готовом zip и ссылка на скачивание — установленная копия сравнивает её со своей и зовёт обновиться. Единственная ручка без токена. 503, если zip ещё не собран.
{ "version": "3.2.10", "url": "https://naminori.fun/apps/naminori-extension.zip" }Пример: мини-клиент повторений на JS
const API = "https://naminori.fun";
const H = { Authorization: "Bearer nmn_…", "Content-Type": "application/json" };
// 1. Скачиваем очередь ЦЕЛИКОМ — страницами по курсору, чтобы работать без сети.
// Складываем по cardId: если между страницами вы отвечаете на сайте,
// карточка с новым сроком может приехать второй раз.
const byId = new Map();
let url = `${API}/api/v1/queue?type=vocab&order=stable&limit=500`;
while (url) {
const page = await fetch(url, { headers: H }).then((r) => r.json());
for (const card of page.items) byId.set(card.cardId, card);
// limit повторяем на каждой странице: без него страница снова 50.
url = page.nextCursor
? `${API}/api/v1/queue?type=vocab&limit=500&cursor=${encodeURIComponent(page.nextCursor)}`
: null;
}
const queue = [...byId.values()];
// 2. Занимаемся хоть в самолёте. Момент ответа запоминаем СВОЙ — по нему
// считается интервал FSRS, и он же защищает от двойной отправки.
const done = [];
for (const card of queue) {
console.log(card.jp, "→ ваш ответ…", card.ru);
done.push({ cardId: card.cardId, rating: 3, reviewedAt: new Date().toISOString() });
}
// 3. Появилась связь — отправляем накопленное пачками по 500.
for (let i = 0; i < done.length; i += 500) {
const r = await fetch(`${API}/api/v1/review/batch`, {
method: "POST", headers: H,
body: JSON.stringify({ reviews: done.slice(i, i + 500) }),
}).then((r) => r.json());
// Ответ потерялся по дороге? Пошлите тот же кусок ещё раз: уже принятые
// позиции вернутся как duplicate и расписание второй раз не сдвинут.
console.log(r.applied, "принято,", r.duplicate, "повтор,", r.failed, "не вышло");
}Базовый URL: https://naminori.fun. По вопросам интеграций пишите в Telegram @s1n3off или на shirokuma9282@gmail.com. API стабилен в пределах v1; о breaking changes предупредим здесь.