API

Tài liệu API

Tự động hoá 2Novel bằng REST: tạo tác phẩm, thêm chương, dịch và lấy kết quả — không cần mở trình duyệt. Mọi yêu cầu xác thực bằng khoá API.

Xác thực

Tất cả endpoint yêu cầu một API key. Tạo key trong Cài đặt → API Keys — key chỉ hiện một lần sau khi tạo, hãy lưu ngay. Gắn key vào header Authorization ở mọi request:

Base URL
https://2novel.app/api
Header xác thực
Authorization: Bearer ltr_xxxxxxxxxxxx
Tác phẩm, chương và API key đều thuộc về workspace của bạn. Mỗi lần dịch sẽ trừ vào số dư theo số chữ nguồn — đảm bảo workspace còn đủ số dư trước khi gọi. Các ví dụ dưới đây dùng biến shell $KEY:
export KEY="ltr_xxxxxxxxxxxx"
export BASE="https://2novel.app/api"

Quy trình

Một vòng đời dịch hoàn chỉnh gồm 5 bước. Bước 3 chỉ xếp hàng công việc (trả về ngay 202); bạn dùng bước 5 để chờ tới khi chương chuyển sang done, rồi đọc bản dịch ở bước 4.

  1. 1

    Tạo tác phẩm

    POST /books — nhận về bookId

  2. 2

    Thêm chương

    POST /books/{id}/chapters — nhận về chapterId

  3. 3

    Dịch chương

    POST /translation/books/{id}/translate — xếp hàng

  4. 4

    Lấy bản dịch

    GET /books/{id}/chapters/{chapterId} — đọc translatedText

  5. 5

    Trạng thái dịch

    GET /books/{id} — theo dõi chapters[].status

POST/books

1 · Tạo tác phẩm

Tạo một tác phẩm mới trong workspace. Lưu lại book.id để dùng cho các bước sau.
TrườngKiểuMô tả
title*stringTên tác phẩm (1–300 ký tự).
sourceLangstringNgôn ngữ nguồn (mặc định 'auto').
targetLangstringNgôn ngữ đích (mặc định 'vi').
autoApprovebooleanTự động phân tích (lập Book Bible) rồi áp dụng ngay trước khi dịch từng chương chưa phân tích. Mặc định true — bỏ trống là bật. Gửi false để dịch thẳng, không phân tích.
Yêu cầu
curl -X POST "$BASE/books" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Phàm Nhân Tu Tiên",
    "sourceLang": "zh",
    "targetLang": "vi",
    "autoApprove": true
  }'
Phản hồi
201 Created
{
  "book": {
    "id": "b1a2c3d4-...",
    "title": "Phàm Nhân Tu Tiên",
    "sourceLang": "zh",
    "targetLang": "vi",
    "autoApprove": true,
    "createdAt": "2026-06-30T08:00:00.000Z"
  }
}
POST/books/{id}/chapters

2 · Thêm chương

Thêm một chương bằng cách dán văn bản (JSON như trên). Endpoint cũng nhận multipart/form-data để upload nhiều file (PDF/DOCX/TXT) — gửi trường files và startOrderNo; mỗi file thành một chương, đánh số theo thứ tự tên file.
TrườngKiểuMô tả
orderNo*numberSố thứ tự chương (nguyên dương, duy nhất trong tác phẩm).
titlestringTiêu đề chương (tối đa 300 ký tự, có thể để trống).
sourceText*stringVăn bản gốc cần dịch (1–500.000 ký tự).
Yêu cầu
curl -X POST "$BASE/books/$BOOK_ID/chapters" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "orderNo": 1,
    "title": "Chương 1: Lên núi",
    "sourceText": "韩立坐在这座普普通通的茅屋之中..."
  }'
Phản hồi
201 Created
{
  "chapters": [
    {
      "id": "c9f8e7d6-...",
      "bookId": "b1a2c3d4-...",
      "orderNo": 1,
      "title": "第一章 上山",
      "sourceText": "韩立坐在...",
      "translatedText": null,
      "translatedTitle": null,
      "status": "idle",
      "errorMsg": null,
      "prepassAt": null
    }
  ]
}
POST/translation/books/{id}/translate

3 · Dịch chương

Xếp hàng dịch các chương đã chọn và trả về ngay (bất đồng bộ). queued là số chương vừa được đưa vào hàng đợi. Theo dõi tiến độ ở bước 5, lấy kết quả ở bước 4.
TrườngKiểuMô tả
chapterIdsstring[]Danh sách chương cần dịch (tối đa 1000). Bỏ trống = dịch tất cả chương trong tác phẩm.
modelstringTên model (1–64 ký tự). Bỏ trống dùng model mặc định của workspace.
useBiblebooleanBật Book Bible (đồng nhất thuật ngữ/nhân vật). Mặc định true.
passphrasestringCụm mật khẩu giải mã key BYOK (chỉ gói Pro dùng key riêng). Để trống nếu dùng key hệ thống.
Yêu cầu
curl -X POST "$BASE/translation/books/$BOOK_ID/translate" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "chapterIds": ["c9f8e7d6-..."],
    "useBible": true
  }'
Phản hồi
202 Accepted
{
  "queued": 1
}
GET/books/{id}

4 · Lấy danh mục chương

Trả về tác phẩm kèm danh mục chương nhẹ — không mang sourceText/translatedText, để một bộ nghìn chương vẫn là một response nhỏ và poll được liên tục. hasTranslation cho biết chương đã có bản dịch; tiêu đề đã dịch nằm ở translatedTitle (fallback về title gốc khi còn trống). Nội dung đầy đủ của từng chương lấy ở endpoint ngay dưới.
Yêu cầu
curl "$BASE/books/$BOOK_ID" \
  -H "Authorization: Bearer $KEY"
Phản hồi
200 OK
{
  "book": { "id": "b1a2c3d4-...", "title": "Phàm Nhân Tu Tiên", ... },
  "chapters": [
    {
      "id": "c9f8e7d6-...",
      "orderNo": 1,
      "title": "第一章 上山",
      "translatedTitle": "Chương 1: Lên núi",
      "status": "done",
      "hasTranslation": true,
      "sourceCharCount": 2431,
      "errorMsg": null,
      "prepassAt": "2026-06-30T08:05:00.000Z"
    }
  ]
}
GET/books/{id}/chapters/{chapterId}

4b · Lấy nội dung một chương

Trả về một chương với đầy đủ sourceText và translatedText — đây là endpoint để đọc hay tải bản dịch. translatedText là null cho tới khi chương dịch xong (status: "done"). Chỉnh sửa bản dịch bằng PATCH cùng đường dẫn (trường title, translatedText).
Yêu cầu
curl "$BASE/books/$BOOK_ID/chapters/$CHAPTER_ID" \
  -H "Authorization: Bearer $KEY"
Phản hồi
200 OK
{
  "chapter": {
    "id": "c9f8e7d6-...",
    "bookId": "b1a2c3d4-...",
    "orderNo": 1,
    "title": "第一章 上山",
    "translatedTitle": "Chương 1: Lên núi",
    "sourceText": "韩立坐在这座普普通通的茅屋之中...",
    "translatedText": "Hàn Lập ngồi trong căn nhà tranh bình thường này...",
    "status": "done",
    "errorMsg": null,
    "prepassAt": "2026-06-30T08:05:00.000Z"
  }
}
GET/books/{id}

5 · Kiểm tra trạng thái dịch

Mỗi chương có trường status: idle → queued → running → done (hoặc failed, kèm errorMsg). Poll cách vài giây cho tới khi mọi chương done. Endpoint /jobs cho biết chi tiết hàng đợi đang xử lý.
Yêu cầu
# Cách 1 — trạng thái từng chương (dùng chung endpoint bước 4)
curl "$BASE/books/$BOOK_ID" \
  -H "Authorization: Bearer $KEY" | jq '.chapters[] | {orderNo, status}'

# Cách 2 — chi tiết job đang chạy của tác phẩm
curl "$BASE/translation/books/$BOOK_ID/jobs" \
  -H "Authorization: Bearer $KEY"
Phản hồi
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

Book Bible là lớp đồng nhất của từng tác phẩm — từ điển thuật ngữ, nhân vật, quan hệ và thành ngữ mà bộ dịch áp dụng cho mọi chương. Các endpoint dưới đây đọc và sửa nó; mọi đường dẫn đều nằm dưới /books/{id} và yêu cầu cùng API key.

EndpointTác dụng
GET /books/{id}/glossaryDanh sách thuật ngữ của tác phẩm.
POST /books/{id}/glossaryThêm/sửa một thuật ngữ (sourceTerm, targetTerm, category, note, locked).
DELETE /books/{id}/glossary/{termId}Xoá một thuật ngữ.
GET /books/{id}/charactersDanh sách nhân vật.
POST /books/{id}/charactersThêm nhân vật (nameVi, nameCn, aliases, role, faction, realm, …).
GET /books/{id}/characters/{slug}Chi tiết một nhân vật theo slug.
PATCH /books/{id}/characters/{slug}Sửa nhân vật — tên, quan hệ, khả năng, trang bị, timeline.
DELETE /books/{id}/characters/{slug}Xoá nhân vật.
GET /books/{id}/relationsQuan hệ giữa nhân vật — lọc theo fromSlug/toSlug/fromOrder/toOrder, phân trang limit/offset.
GET /books/{id}/character-eventsSự kiện theo chương của nhân vật — lọc characterSlug, fromOrder/toOrder.
GET /books/{id}/idiomsThành ngữ / tục ngữ / điển cố của tác phẩm.
POST /books/{id}/idiomsThêm thành ngữ (sourceText, hanViet, viEquivalent, …).
DELETE /books/{id}/idioms/{idiomId}Xoá một thành ngữ.
GET /books/{id}/conflictsNhân vật nghi trùng chờ duyệt — lọc theo status.
POST /books/{id}/conflicts/{conflictId}/resolveQuyết định merge / keep / dismiss một xung đột.
GET /books/{id}/lessonsBài học rút ra từ các lượt sửa bản dịch.
DELETE /books/{id}/lessons/{lessonId}Bỏ qua một bài học.
GET /books/{id}/exportXuất toàn bộ Book Bible (tham số tuỳ chọn novelId).
Ví dụ — xuất Book Bible
curl "$BASE/books/$BOOK_ID/export" \
  -H "Authorization: Bearer $KEY"

Webhook

Thay vì hỏi /books/:id/chapters liên tục để biết đã dịch xong chưa, hãy đăng ký một endpoint và để 2Novel gọi về. Tạo trong Cài đặt → Webhook (gói Pro); mỗi endpoint có khoá ký riêng và khoá chỉ hiện một lần lúc tạo.

Một workspace đăng ký được nhiều endpoint — thường là một cho production và một cho staging, hoặc để chuyển sang hệ thống nhận mới mà không bị hụt sự kiện nào.

Sự kiện

eventKhi nào
book.chapters_translatedMột tác phẩm vừa có chương dịch xong. Gộp lại: một lô 1.000 chương chỉ sinh MỘT lần gọi, sau khoảng 30 giây kể từ chương đầu tiên xong.
pingBạn bấm “Gửi thử”. Đi qua đúng đường gửi thật, nên test xanh là bằng chứng về đường thật.

Nội dung gửi đến

Body là một cái chuông, không phải gói hàng — nó không mang nội dung chương. Nhận xong, hãy gọi API ở trên để lấy bản dịch. Chủ ý: gửi kèm nội dung nghĩa là mỗi chương một request, và một lô lớn sẽ thành hàng nghìn.

POST https://api.cua-ban.com/2novel/webhook
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": "Phàm Nhân Tu Tiên",
  "pending_since": "2026-08-27T03:12:44.512Z"
}

Xác thực chữ ký

Chữ ký là HMAC-SHA256 của chuỗi <timestamp>.<raw body> với khoá ký của endpoint. So sánh trên raw body — parse rồi serialize lại sẽ đổi từng byte và chữ ký không bao giờ khớp. Từ chối request có timestamp lệch quá 5 phút để chặn phát lại.

Node.js
import crypto from 'node:crypto'

app.post('/2novel/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const raw = req.body.toString('utf8')          // raw, chưa parse
  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)                             // trả nhanh, làm việc sau
})

Bạn trả về gì

HTTP2Novel hiểu là
2xxĐã nhận. Đóng cửa sổ, không gửi lại.
409Bạn đang bận với chính tác phẩm này. Quay lại sau 60 giây và KHÔNG tính là lỗi — đây là câu trả lời đúng khi bạn còn đang xử lý lô trước.
4xx / 5xx / timeoutLỗi. Thử lại theo 60s → 120s → 300s → 600s → 900s rồi bỏ. Sau 5 lần cạn lượt liên tiếp, endpoint bị tắt và bạn phải bật lại tay.

Endpoint phải là https:// trên một máy chủ công khai. Địa chỉ trỏ vào mạng nội bộ bị từ chối cả lúc đăng ký lẫn lúc gửi — kiểm tra hai lần vì một tên miền công khai vẫn có thể bị đổi hướng sau đó.

Gọi lại cùng một sự kiện là chuyện bình thường (một lần thử lại sau khi bạn đã xử lý xong). Hãy làm phía nhận idempotent; dùng pending_since để phân biệt cửa sổ mới với lần gửi lại của cửa sổ cũ.

Mã lỗi

Lỗi trả về dạng JSON { "error": "<code>", "message"?: "..." } kèm mã HTTP tương ứng.

HTTPerrorÝ nghĩa
400invalid_body / invalid_json_bodyBody sai định dạng hoặc thiếu trường bắt buộc.
401UnauthorizedThiếu hoặc sai API key (header Authorization).
402out_of_fundsWorkspace không đủ số dư để dịch.
404book_not_found / chapter_not_foundKhông tìm thấy tài nguyên (hoặc không thuộc workspace của bạn).
409chapter_order_existsorderNo đã tồn tại trong tác phẩm.
500internal_errorLỗi máy chủ — thử lại sau.