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) — 240 запросов/мин. Запись:/reviewи/vocab/cards— 240/мин,/review/batch— 60/мин (до 500 оценок в каждом),/vocab/addи/audio— 120/мин,/learn-new, создание и правка колод,/media/store— 60/мин. - Деструктивные операции доступны только в редакторе личных колод (
POST /api/v1/vocab/cardsиDELETE /api/v1/vocab/decks/{id}). Удалить аккаунт, изменить настройки или выполнить админ-операцию через API нельзя.
Профиль и статистика
/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Профиль: имя, целевой уровень, стрик, сколько выучено грамматики и слов.
{ "name": "...", "targetLevel": "N4", "streak": 16, "grammarLearned": 461, "vocabLearned": 2182 }/api/v1/overviewВесь кабинет ОДНИМ запросом: профиль, стрик (текущий + рекорд), сегодняшний прогресс (сделано / осталось по типам), состояние колоды (всего / новые / изучено), активность за 7 дней, время ближайшей карточки и список личных колод. Собрано для Telegram-бота, но подходит любому клиенту, которому нужен «экран дня» без пяти запросов подряд.
{ "name": "…", "targetLevel": "N4", "streak": { "current": 16, "best": 41 },
"today": { "doneVocab": 40, "doneGrammar": 12, "doneTotal": 52,
"remainingVocab": 6, "remainingGrammar": 0, "remainingTotal": 6 },
"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 },
"decks": [ { "id": "…", "name": "Аниме", "isDefault": false,
"cardCount": 312, "newCount": 8, "dueCount": 14 } ] }/api/v1/statsДневная статистика занятий — для графиков и heatmap (виджеты, Obsidian, дашборды).
Параметры: days=1..90 (30)
{ "days": 30, "streak": 16, "totals": { "reviewsDone": 812, "newGrammar": 24, "newVocab": 310 },
"daily": [ { "date": "2026-07-02", "reviewsDone": 46, "newGrammar": 2, "newVocab": 25 } ] }/api/v1/stats/todayСводка дня в стиле Anki для экрана «сессия завершена»: сколько карточек, сколько времени, секунд на карточку и фраза под темп. cards — все повторения за день; minutes/secPerCard считаются по записям с замером времени (elapsedMs в POST /api/v1/review).
{ "cards": 52, "timeMs": 684000, "minutes": 11.4, "secPerCard": 13.2,
"phrase": "Ровный темп — так и держите." }Повторения (SRS)
Полный цикл внешнего клиента повторений: очередь → показ карточки → оценка. Расписание — тот же FSRS, что на сайте.
/api/v1/dueСколько карточек ждёт повторения сейчас (грамматика + слова). Необязательный deckId сужает счётчик слов до одной личной колоды.
Параметры: deckId=<uuid>
{ "grammar": 26, "vocab": 46, "total": 72 }/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|grammar (vocab) · limit=1..100 (50; в режиме order=stable — до 500) · deckId=<uuid> · order=stable — устойчивая выдача с курсором (см. ниже) · cursor=<строка из nextCursor> · ghosts=1 — отдавать ghostId у добиваний грамматики (иначе их нечем оценить) · context=1 — приложить к каждому слову живое предложение из корпуса (режим «в предложении»: фраза подбирается так, чтобы незнакомых слов в ней было как можно меньше). По запросу, а не всегда: клиенту вроде бота лишний текст в каждой карточке ни к чему. Поле context равно null, если для слова корпус ничего не нашёл — тогда режим просто не предлагают.
{ "type": "vocab", "total": 46, "items": [
{ "cardId": "…", "kind": "vocab", "jp": "勉強", "kana": "べんきょう", "ru": "учёба",
"level": "N5", "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": "Каждый день учу японский." } },
{ "cardId": "…", "kind": "anki", "front": "猫", "back": "кошка", "reading": "ねこ" } ] }
// type=grammar — другая форма элемента:
{ "type": "grammar", "total": 26, "items": [
{ "cardId": "…", "ghost": false,
"point": { "slug": "te-iru", "title": "ている", "meaningRu": "длительное действие" },
"question": "今、本を___。", "questionKana": "いま、ほんを___。",
"sentenceJp": "今、本を読んでいます。", "sentenceRu": "Сейчас я читаю книгу.",
"answer": "読んでいます", "alternatives": [ "読んでいる" ], "hintRu": "…" },
// добивание проваленного пункта — только при 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 — полный долг очереди, знаменатель прогресса; в обычном (перемешанном) режиме то же поле означает длину выданной порции, так что два режима дадут разные числа. Порядок здесь всегда «повторения по сроку, затем новые по колоде» и НЕ повторяет настройки очереди с сайта (перемешивание/порядок новых) — у вас на руках вся очередь, раскладывайте её как нужно. Дневной лимит новых слов общий на все страницы, а не на каждую.
Параметры: order=stable · cursor=<nextCursor> · limit=1..500 (50) · type, deckId, 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/reviewОценить карточку: FSRS-перепланирование + лог + дневная статистика. rating: 1 Again · 2 Hard · 3 Good · 4 Easy. Необязательный elapsedMs (миллисекунды, 0…30 минут) — сколько времени ушло на карточку; без него «сводка дня» у вашего клиента всегда покажет 0 с/карточку. Необязательный reviewedAt (ISO с зоной, не дальше 72 часов назад) — когда человек ответил на самом деле: FSRS считает интервал от него, а не от момента приёма. День активности (серия, топ) при этом считается по дню ПРИЁМА оценки: чинить серию задним числом нельзя. ДОБИВАНИЯ («призраки»). Тело { 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 запросов в минуту.
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": { … } } }
# «умею добивания»: провал грамматики может завести призрака
-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). undo-токен применённой позиции принимает та же /review/undo — но учтите: отмена стирает запись журнала, то есть сам ключ повтора, и присланная ПОСЛЕ отмены та же оценка применится заново. elapsedMs: до 30 минут на карточку и не больше 6 часов суммарно на запрос — что сверх, применяется без времени. ДОБИВАНИЯ идут в том же пакете: позиция { 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? } … ], ghosts?: true } — от 1 до 500 позиций; вместо 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 или из результата пакета. Восстанавливает прежнее 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":{…}}'
→ { "ok": true, "cardId": "…" }/api/v1/learn-newДобавить порцию НОВЫХ слов или пунктов грамматики в SRS (аналог «Учить новое» на сайте) с дневными лимитами vocabPerDay/newPerDay. added>0 → перезапросите /queue; added=0 и remaining=0 — лимит дня; added=0 и remaining>0 — новый контент закончился.
Параметры: type=vocab|grammar · count=1..50 (10) · level=N5..N1 · 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 в ответе есть «лицо» (слово/пункт). Неизвестное значение фильтра — 400 с details, а не «молча вся колода»: опечатка в type/state диагностируется сразу.
Параметры: type=vocab|grammar|kana|custom|pitch · state=new|learning|review|relearning · suspended=true|false · deckId=<uuid> · limit=1..200 (50) · offset
{ "cards": [ { "cardId": "…", "type": "vocab", "state": "review", "due": "…",
"suspended": false, "word": { "jp": "猫", "kana": "ねこ", "ru": "кошка" } } ], "limit": 50, "offset": 0 }/api/v1/cardsУправление карточкой: suspend/unsuspend (убрать из очереди, обратимо), bury/unbury (спрятать до следующего учебного дня). Удаления здесь нет сознательно — оно живёт в редакторе личных колод (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"}'Личные колоды
Колоды слов в стиле Anki — те же, что на /vocab/decks. Не путать с /api/v1/decks: там публичные колоды сообщества. Этими ручками пользуются наш десктоп, мобильное приложение и расширение (выбор колоды при майнинге).
/api/v1/vocab/decksМои колоды со счётчиками: всего карточек, новых, к повторению.
{ "decks": [ { "id": "…", "name": "Аниме", "description": null, "isDefault": false,
"cardCount": 312, "newCount": 8, "dueCount": 14 } ] }/api/v1/vocab/decksСоздать колоду. Тело: { name (1..80), description? (до 300) }.
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? }.
→ { "ok": true } · 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.
Параметры: moveToDeckId=<uuid> | deleteCards=1
/api/v1/vocab/decks/{id}/cardsКарточки колоды — это и есть данные редактора колод в десктоп-приложении. Все параметры необязательны: без них приходит весь список, как раньше. q ищет по слову, чтению, переводу и имени исходной колоды Anki; state/pause/type/ankiDeck — те же разрезы, что у фильтров на сайте; perPage включает страницы (page — с единицы, номер за краем зажимается к последней). В ответе: total (всего в колоде), filtered (под фильтром), page, pageCount и facets со счётчиками для каждого разреза — счётчик показывает, сколько останется ПОСЛЕ включения этого фильтра.
Параметры: q=<строка> & state=all|new|learning|review|relearning & pause=all|active|paused & type=all|vocab|anki & ankiDeck=<имя> & 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 } ] } }/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/cardsОперации над своими карточками слов: move (перенести в другую колоду), forget (сбросить прогресс, state=new), delete (удалить из повторений), update (личная версия текста), suspend (снять с повторений и вернуть — тоггл над ОДНОЙ карточкой), setSuspended (массовая пауза/возврат: cardIds + suspended, направление задаётся явно), bury (отложить до следующего учебного дня), scene (правка контекста «откуда слово»: предложение, подпись источника, кадр, звук). Пакетные — 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 — длина выбрана с оглядкой на продолжение (結んで в 「結んでもらおうか」, а не жадное 結んでも); такой ответ нельзя кэшировать по «форме + одному следующему символу». Пустой 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,jmdict,warodai,jitendex — ограничить словари; БЕЗ src отдаются все, настройку пользователя ручка не читает, неизвестные имена отбрасываются
{ "query": "食べた", "hit": { "jp": "食べる", "kana": "たべる", "matchLength": 3,
"headGroups": 1, "ru": "есть; кушать",
"groups": [ { "jp": "食べる", "kana": "たべる", "accent": 2, "senses": [ { "source": "jmdict", "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/vocab/addДобавить слово в SRS (майнинг). Опционально text-контекст «откуда слово» — покажется на повторении. Поля text.image / text.audio принимают ТОЛЬКО наш относительный путь вида /api/media/<файл> — его выдаёт POST /api/media/store (см. ниже). Необязательный deckId кладёт слово в конкретную личную колоду. Кроме text есть второй вид контекста — video: сцена YouTube с тайм-кодом реплики, на повторении карточка покажет её.
Параметры: тело: jp · kana · ru · 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 он молча не играет.
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 запросов/мин.
/api/v1/translateПеревод одной японской реплики на русский силами сервера (Gemini). Нужен клиентам, которые показывают перевод на лету — например, оверлею для визуальных новелл. Почему через нас, а не напрямую: бесключевой веб-эндпоинт Google Translate отвечает антибот-страницей и 429 на каждый запрос (режется адрес, а не HTTP-клиент), у сервера же Gemini ходит через прокси с ретраями. Повтор той же реплики отдаётся из общего кэша на 2000 строк — мгновенно и не тратя квоту.
Параметры: тело: text (japanese, режется до 400 символов). Пустой — 400 empty text, нераспарсенное тело — 400 bad json, отказ Gemini — 502 с причиной в поле error
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/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 с сервера пропал основной русский словарь, пакет пересобрался без него, и число термов осталось прежним — подмену по количеству заметить было нельзя. Публично, без токена.
/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=jmdict, src=jitendex; у таких пакетов постоянные названия. Без src — полный пакет, src=a,b — комбинированный (оба для старых клиентов); грамматика Naminori только там, где есть Колобок. Готовый пакет держится в кэше сутки; если его пришлось собирать заново — не чаще трёх сборок за 10 минут с адреса (иначе 429), а одновременные сборки встают в очередь (503, если очередь занята).
Контент
/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/grammar/lookup?q=食べてもいいですか«Что это за конструкция»: запрос нормализуется (тильда-заполнитель, многоточие, скобочные пояснения, полноширинные пробелы) и результаты ранжируются — точное совпадение, начало, вхождение в любую сторону. Главное отличие от каталожного ?q= — вхождение «наоборот»: живая фраза 食べてもいいですか находит пункт 〜てもいい, чего ILIKE по названию не сделает никогда.
Параметры: q=конструкция (до 80 символов) · limit=1..10 (5)
/api/v1/grammar/checkПроверить ответ на грамматическую клозу ТОЧНО как на сайте: сначала ромадзи → кана (как поле ввода в тренажёре), затем общая проверка (кандзи↔кана, эквивалентные формы, вежливость). Позволяет принимать ответ, набранный латиницей, и не переписывать выверенную логику приёма у себя. Тело: { input, answer, alternatives?, pointSlug? } — answer и alternatives приходят из GET /api/v1/queue?type=grammar.
curl -X POST https://naminori.fun/api/v1/grammar/check \
-H "Authorization: Bearer nmn_…" -H "Content-Type: application/json" \
-d '{"input":"temoii","answer":"てもいい","pointSlug":"temo-ii"}'/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/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 или на hello@naminori.fun. API стабилен в пределах v1; о breaking changes предупредим здесь.