保存搜索

给这些筛选条件起个名字,方便以后再来。

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 304 spends 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 source and its source_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": ...}. en is null until it is translated. Genres and tags are named by their zh in parameters.
  • A missing fact is null, never 0 or an empty string. An empty list is [].
  • Numbers are raw (22256029, not 22.3M). Times are RFC 3339 in UTC, and days are YYYY-MM-DD in UTC.
  • Every object carries url, its page on DaoSearch. Link to it wherever you show the data.
  • view=compact on 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.state is available, removed_at_source (the source says the book is gone) or withheld_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.

  • ids is {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 id as you sent it and a status: found; moved, when the id is a second record of a book filed under another source, with canonical_id naming where it lives; or not_found, for no such book or one DaoSearch has hidden. Expect new values.
  • book is exactly what GET /books/{source}/{source_book_id} gives for canonical_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=latest is 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), its title when it has one, its content in Chinese and English, its likes and 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_count is the count the page's heading shows. Some comments are never shown and only the first 100 in each order are served, so page.total can 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. days sets how many, 90 by default and up to 365.
  • History starts on 2026-10-15 and is never pruned. first_day is 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: true means new figures were recorded that day; changed: false means the day repeats the last recorded figures. Every day is there, with no gaps.
  • metrics uses 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_count is 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
      }
    ]
  }
}

GET /search takes exactly one of two parameters.

  • q is 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.
  • author is 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's author filter.
  • source narrows 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, or pov and access on JJWXC) and the minimums on its figures differ by source. GET /sources lists them, and the OpenAPI document repeats them per source.
  • A minimum such as min_fans or min_score takes one of the source's steps, not any number. On Fanqie, min_reads filters on lifetime reads (reads_all_time), not on the current reads figure.
  • genre and tag take the zh of a genre or tag. subgenre needs its genre.
  • title takes 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.

  • depth is how many places a board can hold: 100 on most, 200 on Zongheng, 500 on Qidian's monthly votes. page.total is 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_by names the metric key the board is ordered by, and that figure is first in each book's metrics with "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's weekly_reads or Qimao's heat: GET /sources describes 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_books and the source's popularity minimum take the values in /sources. contains=qidian:1010868264 finds the lists that hold a book.
  • On one list, sort reorders its books by one of the source's figures, and status, min_words, dropped and has_note filter them. min_words takes one of the source's steps (booklists.min_words_steps in /sources). has_note exists 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.last says 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:

  1. Hold your ids. Keep each book as {source}:{source_book_id}. When a batch answers moved, store its canonical_id instead.
  2. Batch-fetch them. Ask for 50 at a time with GET /books?ids=. A thousand books is 20 requests and 100 units.
  3. Send the ETags back. Keep each batch's ETag and send it as If-None-Match next time. An unchanged batch is a free 304. Keep your batches in the same order and grouping, or the tag will not match.
  4. 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.
  5. Read history rather than keeping it. For how a book's figures moved, GET /books/{source}/{source_book_id}/history already has every day for 1 unit.
  6. Use the rankings for what is moving. A board's entries say new, up, down or flat, so fetching the boards you care about once a day finds the books worth adding to your set without walking a catalogue.
  7. Watch availability.state for a book the source has removed, and treat not_found as 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 ETag it is a free 304.
  • 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 (after Retry-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.

  1. 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.
  2. Show a link to the book's url wherever you show its data.
  3. 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.
  4. 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.
  5. 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.
  6. Keep your key out of public pages and repositories. A key that is published will be revoked.
  7. 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.