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:
https://2novel.app/apiAuthorization: Bearer ltr_xxxxxxxxxxxx$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
Tạo tác phẩm
POST /books — nhận về bookId
- 2
Thêm chương
POST /books/{id}/chapters — nhận về chapterId
- 3
Dịch chương
POST /translation/books/{id}/translate — xếp hàng
- 4
Lấy bản dịch
GET /books/{id}/chapters/{chapterId} — đọc translatedText
- 5
Trạng thái dịch
GET /books/{id} — theo dõi chapters[].status
/books1 · Tạo tác phẩm
book.id để dùng cho các bước sau.| Trường | Kiểu | Mô tả |
|---|---|---|
| title* | string | Tên tác phẩm (1–300 ký tự). |
| sourceLang | string | Ngôn ngữ nguồn (mặc định 'auto'). |
| targetLang | string | Ngôn ngữ đích (mặc định 'vi'). |
| autoApprove | boolean | Tự độ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. |
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
}'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"
}
}/books/{id}/chapters2 · Thêm chương
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ường | Kiểu | Mô tả |
|---|---|---|
| orderNo* | number | Số thứ tự chương (nguyên dương, duy nhất trong tác phẩm). |
| title | string | Tiêu đề chương (tối đa 300 ký tự, có thể để trống). |
| sourceText* | string | Văn bản gốc cần dịch (1–500.000 ký tự). |
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": "韩立坐在这座普普通通的茅屋之中..."
}'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 · Dịch chương
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ường | Kiểu | Mô tả |
|---|---|---|
| chapterIds | string[] | 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. |
| model | string | Tên model (1–64 ký tự). Bỏ trống dùng model mặc định của workspace. |
| useBible | boolean | Bật Book Bible (đồng nhất thuật ngữ/nhân vật). Mặc định true. |
| passphrase | string | Cụ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. |
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 · Lấy danh mục chương
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.curl "$BASE/books/$BOOK_ID" \
-H "Authorization: Bearer $KEY"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"
}
]
}/books/{id}/chapters/{chapterId}4b · Lấy nội dung một chương
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).curl "$BASE/books/$BOOK_ID/chapters/$CHAPTER_ID" \
-H "Authorization: Bearer $KEY"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"
}
}/books/{id}5 · Kiểm tra trạng thái dịch
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ý.# 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"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.
| Endpoint | Tác dụng |
|---|---|
| GET /books/{id}/glossary | Danh sách thuật ngữ của tác phẩm. |
| POST /books/{id}/glossary | Thê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}/characters | Danh sách nhân vật. |
| POST /books/{id}/characters | Thê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}/relations | Quan hệ giữa nhân vật — lọc theo fromSlug/toSlug/fromOrder/toOrder, phân trang limit/offset. |
| GET /books/{id}/character-events | Sự kiện theo chương của nhân vật — lọc characterSlug, fromOrder/toOrder. |
| GET /books/{id}/idioms | Thành ngữ / tục ngữ / điển cố của tác phẩm. |
| POST /books/{id}/idioms | Thêm thành ngữ (sourceText, hanViet, viEquivalent, …). |
| DELETE /books/{id}/idioms/{idiomId} | Xoá một thành ngữ. |
| GET /books/{id}/conflicts | Nhân vật nghi trùng chờ duyệt — lọc theo status. |
| POST /books/{id}/conflicts/{conflictId}/resolve | Quyết định merge / keep / dismiss một xung đột. |
| GET /books/{id}/lessons | Bà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}/export | Xuất toàn bộ Book Bible (tham số tuỳ chọn novelId). |
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
| event | Khi nào |
|---|---|
| book.chapters_translated | Mộ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. |
| ping | Bạ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.
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.
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ì
| HTTP | 2Novel hiểu là |
|---|---|
| 2xx | Đã nhận. Đóng cửa sổ, không gửi lại. |
| 409 | Bạ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 / timeout | Lỗ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.
| HTTP | error | Ý nghĩa |
|---|---|---|
| 400 | invalid_body / invalid_json_body | Body sai định dạng hoặc thiếu trường bắt buộc. |
| 401 | Unauthorized | Thiếu hoặc sai API key (header Authorization). |
| 402 | out_of_funds | Workspace không đủ số dư để dịch. |
| 404 | book_not_found / chapter_not_found | Không tìm thấy tài nguyên (hoặc không thuộc workspace của bạn). |
| 409 | chapter_order_exists | orderNo đã tồn tại trong tác phẩm. |
| 500 | internal_error | Lỗi máy chủ — thử lại sau. |