This is an unofficial API client for accessing data from Archive of Our Own (AO3). Written in TypeScript and designed for Node.js.
Inspired by ao3_api.
- Installation
- Quick Start
- Request Options and Pagination
- Export Public Bookmarks
- API Reference
- License
Node.js 20.18.1 or later is required. This package uses ESM imports.
npm i ao3-api-nodejsHere is how to quickly fetch the details of a work:
import { getWork } from 'ao3-api-nodejs';
// Use an async IIFE (Immediately Invoked Function Expression) to use await
(async () => {
try {
const workId = '35961484';
const work = await getWork(workId);
console.log(`Title: ${work.title}`);
console.log(`Author: ${work.author}`);
console.log(`Words: ${work.stats.words}`);
} catch (error) {
console.error(error);
}
})();This is an unofficial API and is not affiliated with the Organization for Transformative Works. Please respect the AO3's Terms of Service. To avoid being IP-banned, please do not make an excessive number of requests. It is recommended to introduce a delay between requests.
The package only fetches publicly available data. It does not support authentication, cookies, or authentication-restricted works. Requests for restricted works throw AuthenticationRequiredError.
All request functions accept RequestOptions:
| Option | Purpose |
|---|---|
proxyUrl |
Optional HTTP proxy URL. Keep credentials outside source code. |
timeoutMs |
Timeout in milliseconds for each request attempt. |
signal |
An AbortSignal for cancelling an in-flight request. |
import { getWork, WorkNotFoundError, AuthenticationRequiredError, AO3Error } from 'ao3-api-nodejs'
try {
const work = await getWork('35961484', {
timeoutMs: 30_000,
signal: AbortSignal.timeout(60_000),
proxyUrl: process.env.AO3_PROXY_URL
})
console.log(work.title)
} catch (error) {
if (error instanceof WorkNotFoundError) console.error('Work not found.')
else if (error instanceof AuthenticationRequiredError) console.error('This work requires login.')
else if (error instanceof AO3Error) console.error(error.message, error.statusCode)
else throw error // Transport, timeout and cancellation errors retain their original types.
}iteratePages(fetchPage, { startPage?, maxPages? }) fetches pages sequentially and stops at the last page or the supplied page limit. It does not impose a request interval. Add a delay inside the callback when reading multiple pages:
import { setTimeout as delay } from 'node:timers/promises'
import { getUserWorks, iteratePages } from 'ao3-api-nodejs'
const signal = AbortSignal.timeout(120_000)
for await (const result of iteratePages(async page => {
if (page > 1) await delay(3000, undefined, { signal })
return getUserWorks('TheHomelyBadger', page, { timeoutMs: 30_000, signal })
}, { maxPages: 5 })) {
console.log(result.works)
}The delay is an example, not a guaranteed safe AO3 request rate. The request layer retries selected transient HTTP failures up to twice. Avoid repeatedly restarting a failed export or running multiple exports concurrently.
From a checkout of this repository:
pnpm install
pnpm run build
node examples/export-bookmarks.mjs TheHomelyBadger bookmarks.json 10 3000
# Or use bookmarks.csv as the output filename.The export example exports public bookmarks of AO3 works. The last two arguments set the page limit (default 10) and delay between pages in milliseconds (default 3000). Press Ctrl+C to cancel. Existing output files are never overwritten, and fetching must succeed before an output file is written.
JSON includes the available bookmark and work fields, page counts, and a truncated flag. CSV includes bookmark ID, work ID, title, first author, work URL, bookmark date, notes and bookmarker's tags; cells that could be read as formulas are prefixed with an apostrophe. Both formats report when the page limit produces a partial export and how many bookmarks were skipped. Series, external works and unavailable items are skipped because the current model does not provide an AO3 work ID for them. Private bookmarks are not accessible.
Get the full meta data for a single work.
author contains the first author, while authors contains all authors in page order.
series contains each series Id, title and position of current work. It is empty if the work doesn't belong to any series.
collections contains each collection's unique name and display title. It is empty if the work doesn't belong to any collection.
Signature: getWork(workId: string, requestOptions?: RequestOptions): Promise<Work>
Example:
import { getWork } from 'ao3-api-nodejs'
const work = await getWork(workId)
console.log(work.tags.rating); // 'Teen And Up Audiences'Get the list of chapters info for a work. If the work has only one chapter, it returns a single row representing the work itself, with id equal to the work ID. Pass that ID unchanged to getChapterContent.
Signature: getChapters(workId: string, requestOptions?: RequestOptions): Promise<Chapter[]>
Example:
import { getChapters } from 'ao3-api-nodejs'
const chaptersList = await getChapters(workId)
console.log(chaptersList[0]) // { id: '89650822', title: 'Chapter 1' }Get the meta data and content for a single chapter.
For standalone works, pass the ID returned by getChapters; the result contains the work title, summary, notes, body and end notes. Content and notes are HTML strings.
Signature: getChapterContent(workId: string, chapterId: string, requestOptions?: RequestOptions): Promise<ChapterContent>
Example:
import { getChapterContent } from 'ao3-api-nodejs'
const content = await getChapterContent(workId, chapterId)
console.log(content.notes) // '<p>Probably not the sequel you were expecting, sorry :)</p>'The same flow works for standalone and chaptered works:
import { getChapters, getChapterContent } from 'ao3-api-nodejs'
const workId = '57038482'
const [chapter] = await getChapters(workId)
const content = await getChapterContent(workId, chapter.id)
console.log(content.title)Get the available download links for a work. URLs are absolute and include AO3's updated_at query parameter.
Signature: getWorkDownloadLinks(workId: string, requestOptions?: RequestOptions): Promise<WorkDownloadLink[]>
Example:
import { getWorkDownloadLinks } from 'ao3-api-nodejs'
const links = await getWorkDownloadLinks(workId)
console.log(links[0]) // { format: 'AZW3', url: 'https://archiveofourown.org/downloads/...' }Accept the same query parameters as the AO3 website.
Signature: search(options: SearchOptions, requestOptions?: RequestOptions): Promise<SearchResults>
Example:
import { search } from 'ao3-api-nodejs';
const results = await search({
page: 2,
query: 'coffee shop au',
fandoms: ['Arcane: League of Legends (Cartoon 2021)'],
rating: 'Teen And Up Audiences',
complete: true,
crossover: 'include',
sortColumn: 'Kudos'
});
console.log(`Page ${results.page} of ${results.totalPages}`);
console.log(`First result: ${results.works[0].title} by ${results.works[0].author}`);Get a paginated list of works for a specific tag.
Signature: getTagWorks(tag: string, page: number = 1, options?: TagWorksOptions, requestOptions?: RequestOptions): Promise<SearchResults>
Example:
import { getTagWorks } from 'ao3-api-nodejs';
const results = await getTagWorks('Top Caitlyn (League of Legends)', 1, {
complete: true,
wordsFrom: 1000,
otherTags: ['Fluff'],
ratings: ['Teen And Up Audiences'],
sortColumn: 'Kudos'
});
console.log(`Found ${results.totalResults} works across ${results.totalPages} pages.`);Get details for a series, including the description, stats, and a list of containing works.
Signature: getSeries(seriesId: string, requestOptions?: RequestOptions): Promise<Series>
Example:
import { getSeries } from 'ao3-api-nodejs';
const series = await getSeries('2662264');
console.log(series.title); // 'Roommates AU'
console.log(`This series has ${series.stats.works} works.`); // This series has 3 works.Get meta data for a collection, including the description status and work count.
Signature: getCollection(name: string, requestOptions?: RequestOptions): Promise<Collection>
Example:
import { getCollection } from 'ao3-api-nodejs';
const collection = await getCollection('CaitlynKiramman_Violet');
console.log(collection.workCount) // 8Get a paginated list of works in a collection.
Signature: getCollectionWorks(name: string, page: number = 1, requestOptions?: RequestOptions): Promise<SearchResults>
Example:
import { getCollectionWorks } from 'ao3-api-nodejs';
const searchResults = await getCollectionWorks('CaitlynKiramman_Violet');
console.log(searchResults.totalResults) // 8Get detail info for a tag, including synonyms, parents, and children.
synonymOf is the canonical tag that the current tag points to.
synonyms are synonym tags merged into the current canonical tag.
metaTags and subTags are flat arrays, not keeping the tree structure.
Relationship arrays contain only the content currently displayed on the AO3 page. childrenTruncated is true when AO3 indicates that more child tags exist.
Signature: getTag(tag: string, requestOptions?: RequestOptions): Promise<TagDetails>
Example:
import { getTag } from 'ao3-api-nodejs';
const tag = await getTag('Fluff');
console.log(tag.category) // 'Additional Tags'
console.log(tag.canonical) // trueGet the public profile info of a specific user.
Signature: getUserProfile(username: string, requestOptions?: RequestOptions): Promise<UserProfile>
Example:
import { getUserProfile } from 'ao3-api-nodejs';
const profile = await getUserProfile('TheHomelyBadger');
console.log(`${profile.username} joined on ${profile.joined}.`); // 2016-09-16Get a paginated list of works published by a user.
Signature: getUserWorks(username: string, page: number = 1, requestOptions?: RequestOptions): Promise<SearchResults>
Example:
import { getUserWorks } from 'ao3-api-nodejs';
const results = await getUserWorks('TheHomelyBadger');
console.log(`Found ${results.totalResults} works by TheHomelyBadger.`); // Found 49 works by TheHomelyBadger.Get a paginated list of a user's public bookmarks.
The embedded work.author contains the first author, while work.authors contains all authors.
bookmark.tags, bookmark.notes and bookmark.created describe the bookmarker's additions and bookmark date, rather than the work's tags or update date.
Signature: getUserBookmarks(username: string, page: number = 1, requestOptions?: RequestOptions): Promise<BookmarkResults>
import { getUserBookmarks } from 'ao3-api-nodejs'
const results = await getUserBookmarks('TheHomelyBadger')
console.log(results.bookmarks[0].bookmark.workTitle)Get the public bookmarks for a work. bookmark.id is null when AO3 does not expose an ID.
Signature: getWorkBookmarks(workId: string, page: number = 1, requestOptions?: RequestOptions): Promise<BookmarkResults>
Get paginated comments for a complete work, organized into reply threads.
Public adult works are supported. Login-required works throw AuthenticationRequiredError; unexpected pages throw AO3Error instead of returning an empty comment list.
Signature: getWorkComments(workId: string, page: number = 1, requestOptions?: RequestOptions): Promise<CommentResults>
Get paginated comments for one chapter, organized into reply threads.
Signature: getChapterComments(workId: string, chapterId: string, page: number = 1, requestOptions?: RequestOptions): Promise<CommentResults>
Get all comments for a work. Caution: This method fetches every comments page sequentially. Avoid calling it frequently for works with many comments.
Signature: getAllWorkComments(workId: string, requestOptions?: RequestOptions): Promise<AllCommentResults>
HTTP response failures use AO3Error.
Unexpected work listing, profile, series, user bookmark and comment pages also throw AO3Error. Legitimate empty result pages still return empty lists. Network, timeout and cancellation errors retain their original transport error types.
Missing resources use:
WorkNotFoundErrorUserNotFoundErrorChapterNotFoundErrorCollectionNotFoundErrorSeriesNotFoundErrorTagNotFoundError
MIT