DaoSearch
API
一个只读的 JSON API,数据与本站相同:来自六个中文网络小说平台的作品、搜索、书库、排行榜和书单。免费,使用账号里的密钥。
本指南为英文。
What it is
The API gives a program the same things the site shows a reader: a book and its figures, what readers say about it, how its figures moved day by day, search, each source's catalogue with its filters, the ranking boards and the booklists. It covers all six sources: Qidian, Fanqie, JJWXC, Faloo, Qimao and Zongheng.
It is read-only JSON over GET, it is free, and it adds nothing the pages do not have. It is meant for personal projects and tools: a Discord bot, a userscript, a reading tracker, a ranking watcher, an agent that looks a novel up. It is not a way to copy the catalogue, and the listings are capped in depth so it cannot be one.
Everything lives under https://daosearch.io/api/v1. A machine-readable description of every endpoint, parameter and field is at /api/v1/openapi.json (OpenAPI 3.1, no key needed). It is generated from the same code that answers the requests, so it cannot say something the API does not do.
Get a key
Every request needs a key, and every account can have one once it is a day old. Sign in, open your account page, tick the box to accept the terms and press "Create key".
- The key is shown once, when you create it. Store it then: only a hash of it is kept, so nobody can show it to you again.
- An account has one key. "Regenerate" replaces it with a new one, and the old key stops working at once. "Revoke" stops it and leaves the account without one until you create another.
- Keep it out of public code and repositories. Nothing watches for a leaked key, so if yours is exposed, regenerate it yourself.
- The account page shows how many of today's units you have spent.
We keep the day your key was last used and today's count against the limits. We do not keep what a key asked for.
Quickstart
Send the key in the Authorization header, and a User-Agent that names your tool, so we can tell your requests apart from everyone else's and reach you if something goes wrong.
curl -i -H "Authorization: Bearer $DAOSEARCH_KEY" \
-H "User-Agent: my-shelf-bot/1.0" \
"https://daosearch.io/api/v1/books/qidian/1010868264"
The answer is the book as {"data": {...}}, with these headers among the others:
HTTP/2 200
content-type: application/json; charset=utf-8
cache-control: private, max-age=300
etag: "9b2c41f0e6a8d3c57f1e0a4b6d2c8e91"
ratelimit-limit: 50000
ratelimit-remaining: 49999
ratelimit-reset: 41234
The same call in Python, with the requests library:
import os
import requests
API = "https://daosearch.io/api/v1"
session = requests.Session()
session.headers.update({
"Authorization": "Bearer " + os.environ["DAOSEARCH_KEY"],
"User-Agent": "my-shelf-bot/1.0",
})
r = session.get(f"{API}/books/qidian/1010868264")
r.raise_for_status()
book = r.json()["data"]
print(book["title"]["en"], book["url"])
print("units left today:", r.headers["RateLimit-Remaining"])
And in JavaScript, with fetch in Node 18 or later:
const API = "https://daosearch.io/api/v1";
const res = await fetch(`${API}/books/qidian/1010868264`, {
headers: {
Authorization: `Bearer ${process.env.DAOSEARCH_KEY}`,
"User-Agent": "my-shelf-bot/1.0",
},
});
const body = await res.json();
if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
console.log(body.data.title.en, body.data.url);
A good first session: call GET /sources to learn what each source's figures mean, search for a title you know, then fetch that book.
Two things are refused on purpose. A key in the URL (?key= or ?api_key=) is answered 400, because URLs end up in logs. And the API cannot be called from a web page on another site, since a key in a page is a published key. Call it from a server, a script, a browser extension or a userscript.
Limits
| limit | default | when you pass it |
|---|---|---|
| requests a minute, per account | 120 | 429 rate_limited, and Retry-After is the seconds left in the minute |
| units a day, per account | 50,000 | 429 quota_exceeded, and Retry-After is the seconds until 00:00 UTC |
| units a day, the whole API | 1,000,000 | 429 quota_exceeded, with a message that says it is the API's day, not yours |
The day starts again at 00:00 UTC. The API's own day is shared by every account, so on a very busy day a request can be turned away before your own units run out.
A single record costs 1 unit. A page of a listing (up to 50 books or lists), a page of comments, a batch of up to 50 books and a search by author, which reads every source, cost 5. A request turned away by the limits, or for a parameter that is wrong, costs nothing. A book that is not there is still an answer, and costs its unit.
| call | units | refreshed |
|---|---|---|
GET /sources |
1 | every 9 hours |
GET /books/{source}/{source_book_id} |
1 | every 6 hours |
GET /books |
5 | every request |
GET /books/{source}/{source_book_id}/comments |
5 | every 6 hours |
GET /books/{source}/{source_book_id}/history |
1 | every hour |
GET /{source}/rankings |
1 | every 30 minutes |
GET /{source}/rankings/{board} |
5 | every 30 minutes |
GET /{source}/booklists |
5 | every hour |
GET /{source}/booklists/{list_id} |
5 | every hour |
GET /search |
1 | every 6 hours |
GET /{source}/genres |
1 | every 9 hours |
GET /{source}/tags |
1 | every 9 hours |
GET /{source}/books |
5 | every 15 minutes |
"Refreshed" is how long an answer is kept before it is built again. A batch of books is put together on every request, from each book's own answer, so each book in it is as fresh as GET /books/{source}/{source_book_id} would give it.
Every answer past the key check carries three headers about your day, so a tool can pace itself without a failed call:
| header | meaning |
|---|---|
RateLimit-Limit |
your account's units for a day |
RateLimit-Remaining |
units left today, after this request |
RateLimit-Reset |
seconds until the units are given back |
For scale: a Discord bot answering lookups for a busy server spends a few hundred units a day, and checking a thousand books once a day in batches spends 100.
Asking for more
If something reasonable needs more than 50,000 units a day or 120 requests a minute, email [email protected] with your username, what you are building, and the limits you need. We can give one account limits of its own; they apply from its next request, and the RateLimit-Limit header and your account page show them. Ask rather than spreading the work over several accounts.
Conditional requests
Every 200 carries an ETag, a fingerprint of that exact answer. Keep it with your copy, and send it back as If-None-Match when you ask again. If nothing changed, the answer is 304 Not Modified with no body, and it costs no units.
curl -i -H "Authorization: Bearer $DAOSEARCH_KEY" \
-H "User-Agent: my-shelf-bot/1.0" \
-H 'If-None-Match: "9b2c41f0e6a8d3c57f1e0a4b6d2c8e91"' \
"https://daosearch.io/api/v1/books/qidian/1010868264"
HTTP/2 304
etag: "9b2c41f0e6a8d3c57f1e0a4b6d2c8e91"
cache-control: private, max-age=300
ratelimit-limit: 50000
ratelimit-remaining: 49999
ratelimit-reset: 41187
In Python, with the session from the quickstart:
etags = {} # url -> ETag, kept beside your copy of the answer
copies = {} # url -> the data you last got
def fetch(url):
headers = {"If-None-Match": etags[url]} if url in etags else {}
r = session.get(url, headers=headers)
if r.status_code == 304:
return copies[url] # unchanged, and it cost nothing
r.raise_for_status()
etags[url], copies[url] = r.headers["ETag"], r.json()["data"]
return copies[url]
- A
304spends no units, but it still counts toward the minute's requests, and it is refused like any request once the day's units left would not cover the call's cost. - Every data endpoint has one, a batch of books included: its tag changes when any book in it does.
- An answer is rebuilt on the schedule in the table above, so re-checking more often than that only ever returns
304.
How answers are shaped
- A data answer is
{"data": ...}. A listing adds"page". An error is{"error": {...}}. Nothing else sits at the top level. - A book is named by its
sourceand itssource_book_id: the id in the source's own URL, sent as text because Fanqie's are 19 digits, longer than a JavaScript number holds. - Text in two languages is
{"en": ..., "zh": ...}.enis null until it is translated. Genres and tags are named by theirzhin parameters. - A missing fact is
null, never0or an empty string. An empty list is[]. - Numbers are raw (
22256029, not22.3M). Times are RFC 3339 in UTC, and days areYYYY-MM-DDin UTC. - Every object carries
url, its page on DaoSearch. Link to it wherever you show the data. view=compacton a listing drops each book's cover, synopsis excerpt and the labels of its figures: about a third of the size, for a tool that only needs to choose.- A parameter the endpoint does not have is refused with 400, not ignored. So is a value off its list.
The six sources count different things, so a book's figures come as a metrics array, each with a key, the label the site prints, a value and the period it is counted over. GET /sources describes every key a source can send. Do not compare a figure across sources.
Sources
GET /sources is the place to start. It lists the six sources and, for each, what every metric key means, which sorts and filters its catalogue takes and on which steps, and what its booklists accept. Read the allowed values from here and do not hard-code them. It changes rarely, so cache it for a day.
A metric with on_book: true is on a book's own record. One with on_book: false appears only on catalogue rows or on a ranking board's entries, such as a board's own weekly count.
curl -H "Authorization: Bearer $DAOSEARCH_KEY" \
"https://daosearch.io/api/v1/sources"
One source, cut to two of its figures:
{
"data": [
{
"source": "fanqie",
"label": "Fanqie",
"name_zh": "番茄小说",
"site_url": "https://fanqienovel.com",
"url": "https://daosearch.io/fanqie/library",
"guide_url": "https://daosearch.io/blog/fanqie-reads-vs-all-time-reads",
"has_rankings": true,
"has_booklists": true,
"score_scale": 10,
"metrics": [
{
"key": "reads",
"label": "Reads",
"zh": "在读",
"period": "current",
"on_book": true,
"meaning": "The read count Fanqie's app shows for the book now.",
"sort": "reads",
"min_param": null,
"min_steps": null
},
{
"key": "score",
"label": "Score",
"zh": "评分",
"period": null,
"on_book": true,
"meaning": "Reader score out of 10. A third of scored books sit on a 5.8 placeholder.",
"sort": "score",
"min_param": "min_score",
"min_steps": [
7,
8.5,
9,
9.5
]
}
],
"catalogue": {
"sorts": [
"reads",
"reads_all",
"updated",
"added",
"score",
"shelves",
"words"
],
"default_sort": "reads",
"length_steps": [
100000,
300000,
1000000,
2000000,
3000000
],
"facets": [
{
"param": "audience",
"values": [
"male",
"female"
]
}
]
},
"booklists": {
"sorts": [
"visits",
"books",
"recs",
"active"
],
"default_sort": "active",
"min_books_steps": [
50,
100,
200,
300,
500
],
"popularity": {
"param": "min_visits",
"steps": [
100000,
1000000,
10000000,
30000000
]
},
"member_sorts": [
"reads",
"score",
"shelves",
"words",
"updated"
],
"min_words_steps": [
100000,
300000,
1000000,
2000000,
3000000
],
"metrics": [
{
"key": "visits",
"label": "Views",
"meaning": "Visits to the list over its life. Fanqie publishes no follower count."
},
{
"key": "recommendations",
"label": "Recs",
"meaning": "Recommendations readers left on the list."
}
]
}
}
]
}
A book
GET /books/{source}/{source_book_id} is everything the book's page shows: titles and author in both languages, the whole English synopsis, genre, tags, the source's figures, the boards it ranks on now, whether the source still carries it, the same novel on other sources, its other names, and a link to an English translation when one is known. comments_url and history_url are the two calls below for the same book.
- The Chinese synopsis is never sent. Titles and tags in Chinese are there because they identify the book.
availability.stateisavailable,removed_at_source(the source says the book is gone) orwithheld_at_source(the source has it but is not showing it now, which on JJWXC often passes). Expect new values.- An id whose book is filed under another source answers 301 to that book's API URL.
- A book DaoSearch has hidden answers 404, as its page does.
curl -H "Authorization: Bearer $DAOSEARCH_KEY" \
"https://daosearch.io/api/v1/books/qidian/1010868264"
{
"data": {
"source": "qidian",
"source_book_id": "1010868264",
"url": "https://daosearch.io/qidian/book/1010868264/lord-of-the-mysteries",
"source_url": "https://www.qidian.com/book/1010868264/",
"title": {
"en": "Lord of the Mysteries",
"zh": "诡秘之主"
},
"author": {
"en": "Cuttlefish That Loves Diving",
"zh": "爱潜水的乌贼"
},
"cover_url": "https://daosearch.io/img/cover/qidian/1010868264.jpg",
"synopsis_en": "With the rising tide of steam power and machinery, who can come close to being a Beyonder?\n\nShrouded in the fog of history and darkness, who or what is the lurking evil that murmurs into our ears?",
"genre": {
"en": "Fantasy",
"zh": "玄幻"
},
"subgenre": {
"en": "Otherworld Continent",
"zh": "异世大陆"
},
"tags": [
{
"en": "Steampunk",
"zh": "蒸汽朋克"
},
{
"en": "Transmigration",
"zh": "穿越"
}
],
"facets": [
{
"key": "audience",
"label": "Audience",
"value": "male",
"value_label": "Male"
}
],
"status": "completed",
"word_count": 4465200,
"chapter_count": 1432,
"updated_at": "2020-05-01T12:00:00Z",
"refreshed_at": "2026-10-06T03:41:12Z",
"metrics": [
{
"key": "monthly_votes",
"label": "Votes",
"value": 15864,
"period": "month"
},
{
"key": "recommendation_votes",
"label": "Recs",
"value": 10203441,
"period": "all_time"
},
{
"key": "fans",
"label": "Fans",
"value": 428117,
"period": "all_time"
},
{
"key": "collections",
"label": "Collections",
"value": 6120344,
"period": "all_time"
}
],
"rankings": [
{
"board": "yuepiao",
"board_label": "Monthly Votes",
"channel": "overall",
"channel_label": null,
"position": 212,
"url": "https://daosearch.io/qidian/rankings?page=5"
}
],
"booklist_count": 5997,
"availability": {
"state": "available",
"since": null
},
"also_on": [
{
"source": "qimao",
"source_book_id": "1693271",
"url": "https://daosearch.io/qimao/book/1693271/lord-of-the-mysteries"
}
],
"other_names": [
{
"name": null,
"name_translated": "Lord of Mysteries",
"kind": "rt",
"last_seen_at": "2026-10-06T03:00:00Z"
}
],
"english_translation": {
"site": "RandomTranslator",
"url": "https://randomtranslator.com/novel/4812"
},
"translated_by": "GPT-6 Luna",
"comments_url": "https://daosearch.io/api/v1/books/qidian/1010868264/comments",
"history_url": "https://daosearch.io/api/v1/books/qidian/1010868264/history"
}
}
Several books at once
GET /books?ids= answers up to 50 books in one request for 5 units, however many you name. Use it for a list you already hold: a reader's shelf, a watch list, the books your bot has seen.
idsis{source}:{source_book_id}pairs separated by commas. An id named twice is answered once.- The answer is one item per id, in the order you asked, so you can map each one back.
- Each item has the
idas you sent it and astatus:found;moved, when the id is a second record of a book filed under another source, withcanonical_idnaming where it lives; ornot_found, for no such book or one DaoSearch has hidden. Expect new values. bookis exactly whatGET /books/{source}/{source_book_id}gives forcanonical_id, and null when the book is not found. A missing book never fails the whole request.- An entry that cannot be read (no colon, an unknown source, more than 50 ids) is a 400 naming it, and costs nothing.
curl -H "Authorization: Bearer $DAOSEARCH_KEY" \
"https://daosearch.io/api/v1/books?ids=qidian:1010868264,fanqie:7143038691944959011"
{
"data": [
{
"id": "qidian:1010868264",
"status": "found",
"canonical_id": "qidian:1010868264",
"book": {
"source": "qidian",
"source_book_id": "1010868264",
"url": "https://daosearch.io/qidian/book/1010868264/lord-of-the-mysteries",
"source_url": "https://www.qidian.com/book/1010868264/",
"title": {
"en": "Lord of the Mysteries",
"zh": "诡秘之主"
},
"author": {
"en": "Cuttlefish That Loves Diving",
"zh": "爱潜水的乌贼"
},
"cover_url": "https://daosearch.io/img/cover/qidian/1010868264.jpg",
"synopsis_en": "With the rising tide of steam power and machinery, who can come close to being a Beyonder?\n\nShrouded in the fog of history and darkness, who or what is the lurking evil that murmurs into our ears?",
"genre": {
"en": "Fantasy",
"zh": "玄幻"
},
"subgenre": {
"en": "Otherworld Continent",
"zh": "异世大陆"
},
"tags": [
{
"en": "Steampunk",
"zh": "蒸汽朋克"
},
{
"en": "Transmigration",
"zh": "穿越"
}
],
"facets": [
{
"key": "audience",
"label": "Audience",
"value": "male",
"value_label": "Male"
}
],
"status": "completed",
"word_count": 4465200,
"chapter_count": 1432,
"updated_at": "2020-05-01T12:00:00Z",
"refreshed_at": "2026-10-06T03:41:12Z",
"metrics": [
{
"key": "monthly_votes",
"label": "Votes",
"value": 15864,
"period": "month"
},
{
"key": "recommendation_votes",
"label": "Recs",
"value": 10203441,
"period": "all_time"
},
{
"key": "fans",
"label": "Fans",
"value": 428117,
"period": "all_time"
},
{
"key": "collections",
"label": "Collections",
"value": 6120344,
"period": "all_time"
}
],
"rankings": [
{
"board": "yuepiao",
"board_label": "Monthly Votes",
"channel": "overall",
"channel_label": null,
"position": 212,
"url": "https://daosearch.io/qidian/rankings?page=5"
}
],
"booklist_count": 5997,
"availability": {
"state": "available",
"since": null
},
"also_on": [
{
"source": "qimao",
"source_book_id": "1693271",
"url": "https://daosearch.io/qimao/book/1693271/lord-of-the-mysteries"
}
],
"other_names": [
{
"name": null,
"name_translated": "Lord of Mysteries",
"kind": "rt",
"last_seen_at": "2026-10-06T03:00:00Z"
}
],
"english_translation": {
"site": "RandomTranslator",
"url": "https://randomtranslator.com/novel/4812"
},
"translated_by": "GPT-6 Luna",
"comments_url": "https://daosearch.io/api/v1/books/qidian/1010868264/comments",
"history_url": "https://daosearch.io/api/v1/books/qidian/1010868264/history"
}
},
{
"id": "fanqie:7143038691944959011",
"status": "not_found",
"canonical_id": null,
"book": null
}
]
}
A book's comments
GET /books/{source}/{source_book_id}/comments is what readers say about a book: the comments its DaoSearch page shows, through the page's own filter, so a comment with nothing to read (a picture, a line of punctuation) is left out.
sort=likes(the default) is the most liked first;sort=latestis the newest first.- 20 comments a page, 5 pages deep in each order: the first 100 of each. A page costs 5 units.
- Each comment has its source's own
id(stable across pages and orders), itstitlewhen it has one, itscontentin Chinese and English, itslikesand when it was posted. - Nothing of who wrote a comment is sent: no name, no account, no picture, and not the pseudonym the page shows either. The terms ask you never to try to find out.
comment_countis the count the page's heading shows. Some comments are never shown and only the first 100 in each order are served, sopage.totalcan be lower.
curl -H "Authorization: Bearer $DAOSEARCH_KEY" \
"https://daosearch.io/api/v1/books/qidian/1010868264/comments"
{
"data": {
"source": "qidian",
"source_book_id": "1010868264",
"book_url": "https://daosearch.io/api/v1/books/qidian/1010868264",
"sort": "likes",
"comment_count": 364,
"translated_by": "Google Translate",
"comments": [
{
"id": "819204551870333012",
"source": "qidian",
"title": {
"en": "Klein is the best protagonist",
"zh": "克莱恩是最好的主角"
},
"content": {
"en": "Rereading it for the third time and the Tarot Club still gives me chills.",
"zh": "第三遍重读,塔罗会还是让我起鸡皮疙瘩。"
},
"likes": 1204,
"created_at": "2025-03-14T09:21:00Z"
}
]
},
"page": {
"number": 1,
"size": 20,
"count": 20,
"total": 100,
"total_capped": true,
"last": 5,
"next_url": "https://daosearch.io/api/v1/books/qidian/1010868264/comments?page=2"
}
}
A book's history
GET /books/{source}/{source_book_id}/history is a book's figures by day: how its fans, votes, favourites or score moved. It costs 1 unit.
- One row per UTC day, oldest first, ending today.
dayssets how many, 90 by default and up to 365. - History starts on 2026-10-15 and is never pruned.
first_dayis the first day in the answer: the start of the window, or the day the book was first recorded when that is later. - A day is recorded only when a figure changed, and the days between are filled in.
changed: truemeans new figures were recorded that day;changed: falsemeans the day repeats the last recorded figures. Every day is there, with no gaps. metricsuses the same keys as the book's own. Only the figures DaoSearch keeps a history of are there, which is not every key a source has: a figure only a listing carries is left out.chapter_countis how many chapters had been seen at the source by that day.
curl -H "Authorization: Bearer $DAOSEARCH_KEY" \
"https://daosearch.io/api/v1/books/qidian/1010868264/history?days=30"
{
"data": {
"source": "qidian",
"source_book_id": "1010868264",
"book_url": "https://daosearch.io/api/v1/books/qidian/1010868264",
"from": "2026-09-17",
"to": "2026-10-16",
"first_day": "2026-10-15",
"days": [
{
"day": "2026-10-15",
"changed": true,
"metrics": [
{
"key": "monthly_votes",
"label": "Votes",
"value": 15201,
"period": "month"
},
{
"key": "fans",
"label": "Fans",
"value": 428003,
"period": "all_time"
}
],
"chapter_count": 1432
},
{
"day": "2026-10-16",
"changed": false,
"metrics": [
{
"key": "monthly_votes",
"label": "Votes",
"value": 15201,
"period": "month"
},
{
"key": "fans",
"label": "Fans",
"value": 428003,
"period": "all_time"
}
],
"chapter_count": 1432
}
]
}
}
Search
GET /search takes exactly one of two parameters.
qis a title in English or Chinese, or a book's URL at its source. A title returns up to 5 books across all sources, in the site's own order, and forgives a typo when nothing matches exactly. A book is found by its other names too. One Chinese character is enough; a query in Latin letters needs 3. A URL returns that one book, or nothing if we do not hold it.authoris an author's name: the first 5 books by that author on each source, so up to 30. It costs 5 units. For more of one author use the catalogue'sauthorfilter.sourcenarrows either to one source.
Sending both q and author, or neither, is a 400. There is no paging and no relevance score. A result is a short card: get the book for its details.
curl -H "Authorization: Bearer $DAOSEARCH_KEY" \
"https://daosearch.io/api/v1/search?q=lord+of+the+mysteries"
{
"data": [
{
"source": "qidian",
"source_book_id": "1010868264",
"url": "https://daosearch.io/qidian/book/1010868264/lord-of-the-mysteries",
"title": {
"en": "Lord of the Mysteries",
"zh": "诡秘之主"
},
"author": {
"en": "Cuttlefish That Loves Diving",
"zh": "爱潜水的乌贼"
},
"cover_url": "https://daosearch.io/img/cover/qidian/1010868264.jpg",
"matched": "title"
}
]
}
The catalogue
GET /{source}/books is a source's catalogue with the filters of its library page: title, author, status, genre, subgenre, tag and tag_not (both repeatable), tag_match, min_words, max_words, updated, dropped, sort, order, and each source's own filters and minimums.
- Sorts, the source's own filters (
audience, orpovandaccesson JJWXC) and the minimums on its figures differ by source.GET /sourceslists them, and the OpenAPI document repeats them per source. - A minimum such as
min_fansormin_scoretakes one of the source's steps, not any number. On Fanqie,min_readsfilters on lifetime reads (reads_all_time), not on the currentreadsfigure. genreandtagtake thezhof a genre or tag.subgenreneeds itsgenre.titletakes a piece of a title. A book URL there is a 400 that points you to/search.- There is no catalogue across sources and no sort across them: their figures do not compare.
curl -H "Authorization: Bearer $DAOSEARCH_KEY" \
"https://daosearch.io/api/v1/qidian/books?status=completed&min_fans=100000"
{
"data": [
{
"source": "qidian",
"source_book_id": "1010868264",
"url": "https://daosearch.io/qidian/book/1010868264/lord-of-the-mysteries",
"title": {
"en": "Lord of the Mysteries",
"zh": "诡秘之主"
},
"author": {
"en": "Cuttlefish That Loves Diving",
"zh": "爱潜水的乌贼"
},
"cover_url": "https://daosearch.io/img/cover/qidian/1010868264.jpg",
"genre": {
"en": "Fantasy",
"zh": "玄幻"
},
"status": "completed",
"word_count": 4465200,
"synopsis_excerpt": "With the rising tide of steam power and machinery, who can come close to being a Beyonder? Shrouded in the fog of history and darkness, who or what is the lurking evil that murmurs into our ears?",
"metrics": [
{
"key": "monthly_votes",
"label": "Votes",
"value": 15864,
"period": "month"
},
{
"key": "recommendation_votes",
"label": "Recs",
"value": 10203441,
"period": "all_time"
},
{
"key": "fans",
"label": "Fans",
"value": 428117,
"period": "all_time"
}
]
}
],
"page": {
"number": 1,
"size": 50,
"count": 50,
"total": 213,
"total_capped": false,
"last": 5,
"next_url": "https://daosearch.io/api/v1/qidian/books?min_fans=100000&page=2&status=completed"
}
}
Genres and tags
GET /{source}/genres lists a source's genres with their subgenres, and GET /{source}/tags its tags with how many books carry each. These are the values the catalogue's genre, subgenre, tag and tag_not take. book_count on a genre or a subgenre is null when it holds no books. The tags are every tag on 25 or more of the source's books, up to 3,000 in one answer; q narrows them by name.
curl -H "Authorization: Bearer $DAOSEARCH_KEY" \
"https://daosearch.io/api/v1/qidian/genres"
{
"data": [
{
"zh": "玄幻",
"en": "Fantasy",
"book_count": 214530,
"url": "https://daosearch.io/qidian/library?genre=3",
"audience": "male",
"subgenres": [
{
"zh": "东方玄幻",
"en": "Eastern Fantasy",
"book_count": 96412
},
{
"zh": "异世大陆",
"en": "Otherworld Continent",
"book_count": 58107
}
]
}
]
}
curl -H "Authorization: Bearer $DAOSEARCH_KEY" \
"https://daosearch.io/api/v1/qidian/tags?q=system"
{
"data": [
{
"zh": "系统",
"en": "System",
"book_count": 88214,
"url": "https://daosearch.io/qidian/library?tag_in=412"
}
]
}
Rankings
GET /{source}/rankings lists the boards we hold for a source now: each board's code, its name, what it ranks by, how deep it goes and its channels (a genre, an audience, or the whole board). Boards come and go, so list them and do not hard-code a code.
GET /{source}/rankings/{board} is one page of a board as the source last published it. Each entry has its position, its previous_position at the last daily snapshot, and movement: new, up, down or flat. channel picks one of the board's channels.
depthis how many places a board can hold: 100 on most, 200 on Zongheng, 500 on Qidian's monthly votes.page.totalis how many the channel holds now.- Only places the source itself published are returned. On the site, Qidian's genre boards for monthly votes go on past Qidian's own last place using the votes we hold; those continued places are not in the API.
ranked_bynames the metric key the board is ordered by, and that figure is first in each book'smetricswith"ranked_by": true. It is null where the source publishes no figure for the board. Some boards carry a figure of their own, such as Faloo'sweekly_readsor Qimao'sheat:GET /sourcesdescribes each.- A book first seen on a board has a null title until we have indexed it.
- An unknown board or channel is a 404. We do not answer with a different board.
curl -H "Authorization: Bearer $DAOSEARCH_KEY" \
"https://daosearch.io/api/v1/qidian/rankings"
{
"data": [
{
"board": "yuepiao",
"label": "Monthly Votes",
"zh": "月票榜",
"description": "Qidian's monthly vote ranking: readers spend monthly tickets on the books they follow.",
"ranked_by": "monthly_votes",
"depth": 500,
"default_channel": "overall",
"channels": [
{
"channel": "overall",
"label": "All",
"zh": "全部"
},
{
"channel": "21",
"label": "Fantasy",
"zh": "玄幻"
}
],
"url": "https://daosearch.io/qidian/rankings"
}
]
}
curl -H "Authorization: Bearer $DAOSEARCH_KEY" \
"https://daosearch.io/api/v1/qidian/rankings/yuepiao?view=compact"
This one is the compact view:
{
"data": {
"source": "qidian",
"board": "yuepiao",
"label": "Monthly Votes",
"zh": "月票榜",
"channel": "overall",
"channel_label": "All",
"ranked_by": "monthly_votes",
"refreshed_at": "2026-10-08T02:10:44Z",
"url": "https://daosearch.io/qidian/rankings",
"entries": [
{
"position": 1,
"previous_position": 3,
"movement": "up",
"change": 2,
"book": {
"source": "qidian",
"source_book_id": "1010868264",
"url": "https://daosearch.io/qidian/book/1010868264/lord-of-the-mysteries",
"title": {
"en": "Lord of the Mysteries",
"zh": "诡秘之主"
},
"author": {
"en": "Cuttlefish That Loves Diving",
"zh": "爱潜水的乌贼"
},
"genre": {
"en": "Fantasy",
"zh": "玄幻"
},
"status": "completed",
"word_count": 4465200,
"metrics": [
{
"key": "monthly_votes",
"value": 15864,
"ranked_by": true
},
{
"key": "recommendation_votes",
"value": 10203441
},
{
"key": "fans",
"value": 428117
}
]
}
}
]
},
"page": {
"number": 1,
"size": 50,
"count": 50,
"total": 500,
"total_capped": false,
"last": 10,
"next_url": "https://daosearch.io/api/v1/qidian/rankings/yuepiao?page=2&view=compact"
}
}
Booklists
GET /{source}/booklists lists the reader-made lists of a source, and GET /{source}/booklists/{list_id} is one list with a page of its books. Zongheng has no booklists: both answer 404 not_supported there, and has_booklists in /sources says so in advance.
- A list is named by its id at the source (
list_id). - On the index,
sort,min_booksand the source's popularity minimum take the values in/sources.contains=qidian:1010868264finds the lists that hold a book. - On one list,
sortreorders its books by one of the source's figures, andstatus,min_words,droppedandhas_notefilter them.min_wordstakes one of the source's steps (booklists.min_words_stepsin/sources).has_noteexists only where lists carry curators' notes. - A list of more than 5,000 books cannot be sorted or filtered: that is a 400
sort_not_available. Ask for it plain. - A book on a list can belong to another source than the list. Each book says its own
source.
curl -H "Authorization: Bearer $DAOSEARCH_KEY" \
"https://daosearch.io/api/v1/qidian/booklists?min_books=30"
{
"data": [
{
"source": "qidian",
"list_id": "628204115",
"url": "https://daosearch.io/qidian/booklists/628204115/apocalypse-survival-picks",
"title": {
"en": "Apocalypse Survival Picks",
"zh": "末世求生精选"
},
"description_en": "Thirty survival stories that keep their rules straight.",
"book_count": 30,
"metrics": [
{
"key": "followers",
"label": "Follows",
"value": 1840
}
],
"updated_at": "2026-09-29T08:00:00Z"
}
],
"page": {
"number": 1,
"size": 50,
"count": 50,
"total": 2500,
"total_capped": true,
"last": 20,
"next_url": "https://daosearch.io/api/v1/qidian/booklists?min_books=30&page=2"
}
}
curl -H "Authorization: Bearer $DAOSEARCH_KEY" \
"https://daosearch.io/api/v1/qidian/booklists/628204115"
{
"data": {
"source": "qidian",
"list_id": "628204115",
"url": "https://daosearch.io/qidian/booklists/628204115/apocalypse-survival-picks",
"title": {
"en": "Apocalypse Survival Picks",
"zh": "末世求生精选"
},
"description_en": "Thirty survival stories that keep their rules straight.",
"book_count": 30,
"metrics": [
{
"key": "followers",
"label": "Follows",
"value": 1840
}
],
"updated_at": "2026-09-29T08:00:00Z",
"translated_by": "GPT-6 Luna",
"members": [
{
"position": 1,
"note": "The best rule system of the genre, and it never cheats.",
"book": {
"source": "qidian",
"source_book_id": "1010868264",
"url": "https://daosearch.io/qidian/book/1010868264/lord-of-the-mysteries",
"title": {
"en": "Lord of the Mysteries",
"zh": "诡秘之主"
},
"author": {
"en": "Cuttlefish That Loves Diving",
"zh": "爱潜水的乌贼"
},
"cover_url": "https://daosearch.io/img/cover/qidian/1010868264.jpg",
"genre": {
"en": "Fantasy",
"zh": "玄幻"
},
"status": "completed",
"word_count": 4465200,
"synopsis_excerpt": "With the rising tide of steam power and machinery, who can come close to being a Beyonder? Shrouded in the fog of history and darkness, who or what is the lurking evil that murmurs into our ears?",
"metrics": [
{
"key": "monthly_votes",
"label": "Votes",
"value": 15864,
"period": "month"
},
{
"key": "recommendation_votes",
"label": "Recs",
"value": 10203441,
"period": "all_time"
},
{
"key": "fans",
"label": "Fans",
"value": 428117,
"period": "all_time"
}
]
}
}
]
},
"page": {
"number": 1,
"size": 50,
"count": 30,
"total": 30,
"total_capped": false,
"last": 1,
"next_url": null
}
}
Paging and depth caps
A listing is paged with page, from 1. page.next_url is the next request, null on the last page, and page.last is the deepest page the view allows. Asking past it is a 400 page_out_of_range that names the last page.
Every listing is capped in depth, and the caps are the point: they are what keeps the API from being a way to copy the catalogue, which is also why the daily quota can be generous.
- The catalogue: 50 books a page, 50 pages deep with no filter, 20 with one filter and 5 with two or more. Each tag counts as a filter. Narrow the view to see more of it.
- A ranking board: as deep as the board itself.
- A book's comments: 5 pages of 20 in each order.
- Search: 5 books for a title, 5 per source for an author, no paging.
- Booklists:
page.lastsays where each view ends.
When page.total_capped is true, page.total is a floor rather than the whole count.
Keeping in sync
Most tools hold a set of books and want to know when something about them changes. This is the cheap way to do it:
- Hold your ids. Keep each book as
{source}:{source_book_id}. When a batch answersmoved, store itscanonical_idinstead. - Batch-fetch them. Ask for 50 at a time with
GET /books?ids=. A thousand books is 20 requests and 100 units. - Send the ETags back. Keep each batch's
ETagand send it asIf-None-Matchnext time. An unchanged batch is a free304. Keep your batches in the same order and grouping, or the tag will not match. - Do not ask too often. A book's answer is rebuilt on the schedule in the limits table. Once a day is plenty for most tools.
- Read history rather than keeping it. For how a book's figures moved,
GET /books/{source}/{source_book_id}/historyalready has every day for 1 unit. - Use the rankings for what is moving. A board's entries say
new,up,downorflat, so fetching the boards you care about once a day finds the books worth adding to your set without walking a catalogue. - Watch
availability.statefor a book the source has removed, and treatnot_foundas gone until it comes back.
Caching
- Data answers come with
Cache-Control: private, max-age=300: keep what you fetch for at least five minutes. No shared cache may store them, since every answer is for one key. - Each answer is built once and shared by everyone who asks for the same thing, then rebuilt on the schedule in the limits table. Asking again sooner returns the same answer, and with its
ETagit is a free304. - A batch of books is put together on each request from each book's own answer, so it is exactly as fresh as asking for the books one by one.
GET /sources, the genres and the tags change rarely: a day is a fine time to keep them.- Errors carry
Cache-Control: private, no-store: do not keep them. A request that failed can be asked again (afterRetry-After, for a 429).
Errors
An error is JSON with a stable code, the HTTP status, a message that says what to do next, and param when one parameter is at fault. Branch on code. Messages may be reworded.
{
"error": {
"code": "invalid_parameter",
"status": 400,
"param": "min_score",
"message": "min_score must be one of 7, 8.5, 9, 9.5 on fanqie.",
"docs_url": "https://daosearch.io/developers#errors"
}
}
| status | code | when |
|---|---|---|
| 400 | invalid_parameter |
A parameter's value is not one the endpoint takes. param names it and the message lists what it does take. Also answered for a key sent in the URL. |
| 400 | unknown_parameter |
The request sent a parameter the endpoint does not have. The message lists the ones it has. |
| 400 | page_out_of_range |
page is past the last page this view allows. The message names the last page. |
| 400 | sort_not_available |
A booklist too large to sort was asked for sorted or filtered. Ask for it without sort and filters. |
| 401 | missing_key |
No Authorization header. |
| 401 | invalid_key |
The key is not one DaoSearch issued, was typed wrong, or was not sent as "Authorization: Bearer ". |
| 401 | revoked_key |
The key was revoked, or regenerated: the old key stops at once. Your account page shows the key you have now. |
| 403 | account_blocked |
API access for the key's account was blocked by DaoSearch. |
| 403 | cors_not_allowed |
A browser asked, for a web page on another origin, whether it may call the API. It may not. |
| 404 | not_found |
No such book, board, channel or list, or a path that starts like an endpoint and matches none. A book DaoSearch has hidden is not_found too. Any other path under /api/v1/ was the old site's API and answers 410 in plain text. |
| 404 | unknown_source |
The {source} segment is not one of the sources. The message lists them. |
| 404 | not_supported |
The source has no such resource: Zongheng has no booklists. |
| 405 | method_not_allowed |
The API is read-only: GET and HEAD. |
| 429 | rate_limited |
The account made more requests this minute than it may. Wait Retry-After seconds. |
| 429 | quota_exceeded |
The account has used its units for today. Retry-After is the seconds until 00:00 UTC. |
| 500 | internal |
Something failed on our side. Try again in a moment. |
| 503 | api_disabled |
The API is switched off for now. Try again later. |
| 503 | query_timeout |
The answer took longer than a request may wait (3 seconds), so the request was stopped. Its units are given back. The answer is usually still being prepared: try again after Retry-After seconds. If the same request keeps answering this, narrow it with a filter. |
A 401 also carries WWW-Authenticate: Bearer. A 429 and a 503 carry Retry-After, in seconds: wait that long, then go on. For a 500, wait a moment and retry a few times with a growing pause.
Versioning
/api/v1 is the contract. These are not a new version, so write a client that tolerates them: a new field, a new endpoint, a new optional parameter, a new value in a list such as availability.state or a batch item's status, a new metric key, a looser limit. The batch, comments and history endpoints and the ETags all arrived this way, inside v1.
Removing or renaming a field, changing what one means, or making a parameter required would be a new path, /api/v2. /api/v1 would then keep working for at least 90 days and say when it ends in a Sunset header.
The limits and the terms are not part of the version. They may tighten, and this page will say so first.
Terms
By creating a key you agree to these.
- The API is free, for personal projects and tools. It may change or stop; a breaking change gets a new version path and 90 days.
- Show a link to the book's url wherever you show its data.
- Keep to your limits and cache what you fetch. The RateLimit headers tell you where you stand. If you need more, ask for it: do not spread the work over several accounts.
- Do not use it to copy the catalogue or to train on it in bulk. Titles, synopses and covers belong to their authors and the source sites.
- Reader comments are for analysis, and for short quotes shown with a link to the book's url. Do not republish them as a collection, and never try to identify who wrote one.
- Keep your key out of public pages and repositories. A key that is published will be revoked.
- Send a User-Agent that names your tool.
These are version 2 of the terms, from 22 October 2026, which added the lines on reader comments and on asking for more. A key created under the first version keeps working as it is, with nothing to accept again.
DaoSearch provides translations and links for discovery. The works belong to their authors and to the sites that publish them.
For LLM tools
The OpenAPI document gives each endpoint an operationId that reads as an intent (get_book, get_books, search_books, list_catalogue), a description that says when to use it and what it costs, and an example. Most agent tooling can turn it into tools directly. /llms.txt describes the site's public pages and the API's endpoints in a few lines.
Contact
Questions, a bug, or a limit that is too tight for something reasonable: write to [email protected] or ask in the Discord. The status page shows whether the API is up. If your key has leaked, regenerate it on your account page first: that stops the leaked one at once.