APIリファレンス
RESTで2Novelを自動化できます。作品の作成、章の追加、翻訳、結果の取得を、ブラウザーなしで行えます。すべてのリクエストは、APIキーで認証します。
認証
すべてのエンドポイントにAPIキーが必要です。設定 → APIキーで作成できます。キーは作成直後に一度だけ表示されるため、その場で必ず保存してください。すべてのリクエストで Authorization ヘッダーに指定します。
https://2novel.app/apiAuthorization: Bearer ltr_xxxxxxxxxxxx$KEY を使います。export KEY="ltr_xxxxxxxxxxxx"
export BASE="https://2novel.app/api"ワークフロー
翻訳の一連の流れは5つのステップです。ステップ3は処理をキューに入れるだけで、すぐに 202 を返します。章が done になるまではステップ5で待機し、ステップ4で訳文を読み取ります。
- 1
作品を作成
POST /books — bookId が返る
- 2
章を追加
POST /books/{id}/chapters — chapterId が返る
- 3
章を翻訳
POST /translation/books/{id}/translate — キューに登録
- 4
訳文を取得
GET /books/{id}/chapters/{chapterId} — translatedText を読む
- 5
翻訳の状態
GET /books/{id} — chapters[].status を確認
/books1 · 作品を作成
book.id は以降のすべてのステップで必要になるため、控えておいてください。| フィールド | 型 | 説明 |
|---|---|---|
| title* | string | 作品のタイトル(1〜300文字)。 |
| sourceLang | string | 原文の言語を 'zh'、'ja'、'ko'、'en' などの言語タグで指定します(既定は 'auto'で、章ごとに自動判定されます)。言語ペアの固定リストはなく、原文・訳文とも任意の言語を設定できます。エンジンには中国語・日本語・韓国語・ベトナム語向けの追加ルールがあります。 |
| targetLang | string | 訳文の言語を 'vi'、'en' などの言語タグで指定します(既定は 'vi')。 |
| autoApprove | boolean | 未解析の章を翻訳する前に、自動で解析(設定集(Book Bible)の作成)を行って適用します。既定は true で、省略すると有効になります。解析せずにそのまま翻訳するには false を送信します。 |
curl -X POST "$BASE/books" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "凡人修仙传",
"sourceLang": "zh",
"targetLang": "ja",
"autoApprove": true
}'201 Created
{
"book": {
"id": "b1a2c3d4-...",
"title": "凡人修仙传",
"sourceLang": "zh",
"targetLang": "ja",
"autoApprove": true,
"createdAt": "2026-06-30T08:00:00.000Z"
}
}/books/{id}/chapters2 · 章を追加
multipart/form-data にも対応しており、複数のファイル(PDF/DOCX/TXT)を一度にアップロードできます。その場合は files と startOrderNo フィールドを送信してください。各ファイルが1章になり、ファイル名順に番号が振られます。| フィールド | 型 | 説明 |
|---|---|---|
| orderNo* | number | 章の順序番号(正の整数。作品内で重複不可)。 |
| title | string | 章のタイトル(300文字まで。空でも可)。 |
| sourceText* | string | 翻訳する原文(1〜500,000文字)。 |
curl -X POST "$BASE/books/$BOOK_ID/chapters" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{
"orderNo": 1,
"title": "第一章 上山",
"sourceText": "韩立坐在这座普普通通的茅屋之中..."
}'201 Created
{
"chapters": [
{
"id": "c9f8e7d6-...",
"bookId": "b1a2c3d4-...",
"orderNo": 1,
"title": "第一章 上山",
"sourceText": "韩立坐在...",
"translatedText": null,
"translatedTitle": null,
"status": "idle",
"errorMsg": null,
"prepassAt": null
}
]
}/translation/books/{id}/translate3 · 章を翻訳
queued は、今回キューに入った章の数です。進捗はステップ5で確認し、結果はステップ4で取得します。| フィールド | 型 | 説明 |
|---|---|---|
| chapterIds | string[] | 翻訳する章(最大1000件)。省略すると作品のすべての章を翻訳します。 |
| model | string | モデル名(1〜64文字)。省略するとワークスペースの既定モデルを使います。 |
| useBible | boolean | 設定集(Book Bible)を有効にします(用語・登場人物の一貫性を保ちます)。既定は true です。 |
curl -X POST "$BASE/translation/books/$BOOK_ID/translate" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{
"chapterIds": ["c9f8e7d6-..."],
"useBible": true
}'202 Accepted
{
"queued": 1
}/books/{id}4 · 章の一覧
sourceText/translatedText は含まれないため、1000章の作品でもレスポンスは小さく、継続的にポーリングできます。hasTranslation は、その章に訳文があるかどうかを示します。訳文のタイトルは translatedTitle にあります(空の間は元の title が代わりに入ります)。各章の全文は、すぐ下のエンドポイントで取得してください。curl "$BASE/books/$BOOK_ID" \
-H "Authorization: Bearer $KEY"200 OK
{
"book": { "id": "b1a2c3d4-...", "title": "凡人修仙传", ... },
"chapters": [
{
"id": "c9f8e7d6-...",
"orderNo": 1,
"title": "第一章 上山",
"translatedTitle": "第一章 山へ",
"status": "done",
"hasTranslation": true,
"sourceCharCount": 2431,
"errorMsg": null,
"prepassAt": "2026-06-30T08:05:00.000Z"
}
]
}/books/{id}/chapters/{chapterId}4b · 1章の内容を取得
sourceText と translatedText の全文付きで返します。訳文を実際に読み取る、またはダウンロードするときに呼び出すエンドポイントです。章の翻訳が完了する(status: "done")までは、translatedText は null です。訳文を編集するには、同じパスに PATCH を送信します(フィールドは title、translatedText)。curl "$BASE/books/$BOOK_ID/chapters/$CHAPTER_ID" \
-H "Authorization: Bearer $KEY"200 OK
{
"chapter": {
"id": "c9f8e7d6-...",
"bookId": "b1a2c3d4-...",
"orderNo": 1,
"title": "第一章 上山",
"translatedTitle": "第一章 山へ",
"sourceText": "韩立坐在这座普普通通的茅屋之中...",
"translatedText": "韓立はこのごく普通の茅葺き小屋の中に座っていた……",
"status": "done",
"errorMsg": null,
"prepassAt": "2026-06-30T08:05:00.000Z"
}
}/books/{id}5 · 翻訳の状態を確認
status フィールドがあり、idle → queued → running → done と遷移します(失敗した場合は failed となり、errorMsg が付きます)。すべての章が done になるまで、数秒おきにポーリングしてください。/jobs エンドポイントでは、キューの現在の詳細を確認できます。# Option 1 — per-chapter status (same endpoint as step 4)
curl "$BASE/books/$BOOK_ID" \
-H "Authorization: Bearer $KEY" | jq '.chapters[] | {orderNo, status}'
# Option 2 — the book's live job detail
curl "$BASE/translation/books/$BOOK_ID/jobs" \
-H "Authorization: Bearer $KEY"200 OK (GET /translation/books/{id}/jobs)
{
"jobs": [
{
"id": "j1...",
"bookId": "b1a2c3d4-...",
"chapterId": "c9f8e7d6-...",
"model": "gpt-4o",
"status": "running",
"attempts": 1,
"error": null
}
]
}設定集(Book Bible)
設定集は作品ごとの一貫性を保つ層で、翻訳時にすべての章へ適用される用語集・登場人物・人物関係・慣用句をまとめたものです。以下のエンドポイントで読み取りと編集ができます。パスはすべて /books/{id} 配下にあり、同じAPIキーを使います。
| エンドポイント | 用途 |
|---|---|
| GET /books/{id}/glossary | 作品の用語一覧。 |
| POST /books/{id}/glossary | 用語の追加・編集(sourceTerm, targetTerm, category, note, locked)。 |
| DELETE /books/{id}/glossary/{termId} | 用語の削除。 |
| GET /books/{id}/characters | 登場人物の一覧。 |
| POST /books/{id}/characters | 登場人物の追加(nameVi, nameCn, aliases, role, faction, realm, …)。 |
| GET /books/{id}/characters/{slug} | slug で指定した登場人物1人の詳細。 |
| PATCH /books/{id}/characters/{slug} | 登場人物の編集(名前、人物関係、能力、装備、年表)。 |
| DELETE /books/{id}/characters/{slug} | 登場人物の削除。 |
| GET /books/{id}/relations | 登場人物間の関係。fromSlug/toSlug/fromOrder/toOrder で絞り込み、limit/offset でページ送りできます。 |
| GET /books/{id}/character-events | 登場人物の章ごとの出来事。characterSlug、fromOrder/toOrder で絞り込めます。 |
| GET /books/{id}/idioms | 作品の慣用句・ことわざ・典故。 |
| POST /books/{id}/idioms | 慣用句の追加(sourceText, hanViet, viEquivalent, …)。 |
| DELETE /books/{id}/idioms/{idiomId} | 慣用句の削除。 |
| GET /books/{id}/conflicts | 確認待ちの重複疑い登場人物。status で絞り込めます。 |
| POST /books/{id}/conflicts/{conflictId}/resolve | 競合に対して統合 / 維持 / 却下を決定します。 |
| GET /books/{id}/lessons | 訳文の編集から得られた学び。 |
| DELETE /books/{id}/lessons/{lessonId} | 学びを却下します。 |
| GET /books/{id}/export | 設定集全体のエクスポート(novelId パラメータは任意)。 |
curl "$BASE/books/$BOOK_ID/export" \
-H "Authorization: Bearer $KEY"Webhook
翻訳が完了したかどうかを知るために /books/:id/chapters をポーリングする代わりに、エンドポイントを登録しておけば、2Novel から呼び出しが届きます。設定 → Webhookで作成します(Proプラン)。エンドポイントごとに専用の署名シークレットが発行され、作成時に一度だけ表示されます。
ワークスペースには複数のエンドポイントを登録できます。本番用とステージング用に分けたり、イベントを取りこぼさずに新しい受信先へ移行したりする場合に便利です。
イベント
| event | タイミング |
|---|---|
| book.chapters_translated | 作品の章の翻訳が完了したとき。まとめて通知されます。1,000章の一括翻訳でも呼び出しは1回で、最初の章が完了してから約30秒後に送られます。 |
| ping | 「テスト送信」を押したとき。実際の配信経路をそのまま通るため、テストが成功すれば実際の経路が動作していることを確認できます。 |
送信される内容
本文は「呼び鈴」であり、荷物ではありません。章の内容は含まれません。受信したら、上記のAPIを呼び出して訳文を取得してください。なお、内容を一緒に送ると章ごとに1リクエストが必要になり、大きな一括翻訳では数千件に膨れ上がってしまいます。
X-2Novel-Signature: 9f2b… (hex, HMAC-SHA256)
X-2Novel-Timestamp: 1787760000 (unix seconds)
Content-Type: application/json
{
"schema_version": 1,
"event": "book.chapters_translated",
"book_id": "8f14e45f-ceea-467a-9fd9-9f2b0e4f7f10",
"title": "凡人修仙传",
"pending_since": "2026-08-27T03:12:44.512Z"
}署名の検証
署名は、文字列 <timestamp>.<raw body> をエンドポイントの署名シークレットでHMAC-SHA256にかけた値です。比較は生の本文に対して行ってください。パースしてから再シリアライズするとバイト列がすべて変わり、署名は決して一致しません。リプレイ攻撃を防ぐため、タイムスタンプが5分を超えてずれているリクエストは拒否してください。
import crypto from 'node:crypto'
app.post('/2novel/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const raw = req.body.toString('utf8') // raw, not yet parsed
const ts = req.get('X-2Novel-Timestamp')
const sig = req.get('X-2Novel-Signature')
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(401)
const expected = crypto
.createHmac('sha256', process.env.LITRANS_WEBHOOK_SECRET)
.update(`${ts}.${raw}`)
.digest('hex')
const a = Buffer.from(sig, 'hex')
const b = Buffer.from(expected, 'hex')
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(401)
const { event, book_id } = JSON.parse(raw)
if (event === 'book.chapters_translated') void pullChapters(book_id)
res.sendStatus(200) // ack fast, work later
})返すべきレスポンス
| HTTP | 2Novel の解釈 |
|---|---|
| 2xx | 受信済み。ウィンドウを閉じ、再試行はしません。 |
| 409 | まさにその作品の処理中であることを示します。60秒後に再送され、失敗にはカウントされません。直前の一括翻訳をまだ処理している間は、これが適切な返答です。 |
| 4xx / 5xx / timeout | 失敗。60秒 → 120秒 → 300秒 → 600秒 → 900秒の間隔で再試行され、その後は破棄されます。再試行を使い切る失敗が5回連続するとエンドポイントは無効化され、手動で再度有効にする必要があります。 |
エンドポイントは公開サーバー上の https:// である必要があります。プライベートネットワーク内を指すアドレスは、登録時と送信時の両方で拒否されます。公開ホスト名でも後からリダイレクトされる可能性があるため、2回チェックしています。
同じイベントを2回受信することは正常です(処理済みのあとに再試行が届く場合など)。受信側は冪等にしてください。新しいウィンドウと古いウィンドウの再送を区別するには pending_since を使います。
レート制限とクォータ
APIキーには、1分あたりのリクエスト数(RPM)の制限はなく、確認すべき Retry-After ヘッダーもありません。翻訳はキューで処理されます。POST /translation/books/{id}/translate は1回の呼び出しで最大1000件の章IDを受け付け(未指定は全章)、すぐにレスポンスを返します。章はバックグラウンドで翻訳され、GET /books/{id} またはWebhookで進捗を追跡できます。
| 実行が止まる条件 | HTTP | 動作 |
|---|---|---|
| ワークスペースの残高が章の翻訳費用に足りない | 402 | out_of_funds。チャージしてから、もう一度翻訳してください。 |
| 設定したトークン予算(ワークスペースまたはプロジェクト、期間ごと)を使い切った | 429 | budget_exceeded と details: { scope, period, used, limit } を返します。実行前と、モデル呼び出しのたびに確認されます。 |
| モデルプロバイダーがスロットリングした(プロバイダー自身の429) | — | 章はキューに残り、プロバイダーが指定する待ち時間のあとに再試行されます。リクエスト自体は失敗しません。 |
エラーコード
エラーはJSON { "error": "<code>", "message"?: "..." } と、対応するHTTPステータスで返されます。
| HTTP | error | 意味 |
|---|---|---|
| 400 | invalid_body / invalid_json_body | リクエスト本文の形式が不正、または必須フィールドがありません。 |
| 401 | Unauthorized | APIキーがない、または正しくありません(Authorization ヘッダー)。 |
| 402 | out_of_funds | ワークスペースの残高が翻訳費用に足りません。 |
| 404 | book_not_found / chapter_not_found | リソースが見つかりません(またはワークスペースの範囲外です)。 |
| 409 | chapter_order_exists | orderNo が作品内にすでに存在します。 |
| 429 | budget_exceeded | ワークスペースまたはプロジェクトに設定したトークン予算を、この期間で使い切りました。 |
| 500 | internal_error | サーバーエラーです。しばらくしてからもう一度お試しください。 |