Extension API reference

Every method the Boundless host calls on a source, with the exact argument and return field names it reads.

5 min read Updated 2 October 2026

This is the complete contract between the app and an extension, written against the code that actually performs the conversion. The host reads a fixed set of field names off the plain objects you return and ignores everything else, so a misspelled key produces a blank value in the UI rather than an error. Where two names are listed for one field, the host checks them in the order given and takes the first that is present.

Loading and export

The host evaluates your file, then constructs your source with new module.exports.Source(). That is the only shape supported; there is no fallback to an id-keyed export or a global Source.

module and exports are pre-defined globals, so module.exports = { Source }; at the end of the file is the normal shape. Methods are invoked on the instance, so this works and helper methods on the class are available.

Host globals

Global What it is
App.createRequest(o) Returns o unchanged. Use it for readability, or pass a plain object literal.
App.createRequestManager(opts) Returns { schedule(request) }. opts is ignored.
App.createSourceStateManager() Returns { store(key, value), retrieve(key) }, both async. Backed by on-device storage, namespaced per source, and persists across app launches until the source is removed.
App.createSourceManga, createMangaInfo, createChapter, createChapterDetails, createPagedResults, createPartialSourceManga Identity functions, present for compatibility. They return their input.
console.log/info/warn/error/debug Writes to the device log, tagged [ext:<source-id>].
URLSearchParams Not available and not shimmed. Build query strings by hand (see the overview's qs() helper).

Networking

const manager = App.createRequestManager({});
const response = await manager.schedule(
  App.createRequest({
    url: 'https://example.com/api?page=1',   // required, http or https only
    method: 'GET',                            // defaults to GET
    headers: { 'User-Agent': UA },            // string values only
    data: undefined,                          // request body, as a string
  })
);

schedule resolves with:

Field Type Notes
status number HTTP status code. Non-2xx resolves normally, it does not reject.
headers object Response headers as strings.
data string The response body. Never parsed for you.

Transport failures (DNS, TLS, timeout) reject with a plain string message, not an Error object. A second argument to schedule is accepted and ignored, so any retry logic must be your own.

Every request goes out from the user's own device and connection. Send them only to sites you are authorised to read, keep the volume low, and identify your source honestly. Headers are for talking to a site correctly, not for getting past an access control.

getSourceFeeds()

Optional. Synchronous, called once at load time on the JavaScript queue, so it must return immediately: no await, no network.

getSourceFeeds() {
  return [
    { id: 'popular', name: 'Popular' },
    { id: 'latest', name: 'Latest' },
  ];
}

Each entry needs both id and name as strings, and entries missing either are dropped. If the method is absent or the list ends up empty, the app falls back to one feed: { id: 'popular', name: 'Popular' }. The selected feed id arrives as request.feed in getSearchResults.

getSearchTags()

Optional and async. Populates the genre picker.

async getSearchTags() {
  return [{ id: 'action', label: 'Action' }];
}

id is required and must be a string. The display name is read from label, then name, and falls back to the id. If the method is not defined, the app uses an empty list and hides the picker. Selected tags arrive as request.includedTags.

getSearchResults(request, metadata)

Async. Powers both Browse and Search.

The request object

Field Type Notes
title string The search query. '' when browsing.
feed string The selected getSourceFeeds() id. Present only when browsing, absent during search.
includedTags array [{ id: 'action' }]. Always present, often empty. Objects, not bare strings.
excludedTags array Always present, always empty today.
medium string 'comics' or 'novel', present only when the app's medium filter is set.

The metadata argument

The host builds this itself from its own page counter. It is null on the first page and { page: n } for page n where n >= 2. It does not echo back the object you returned last call, so extra fields you put in your returned metadata will not come back to you. Track nothing but the page number.

The return shape

return { results: [ /* partial manga */ ], metadata: { page: page + 1 } };

A bare array is also accepted, but it can never advertise a next page. The host offers another page only when the returned object has a metadata property that is not undefined and results is non-empty. Omit metadata on the last page. Returning it forever makes Browse re-fetch the final page indefinitely.

Manga fields

Read from each entry in results:

Field Type Notes
mangaId string Required. Must be a string; id is not read as a fallback.
title string Falls back to "Untitled"; titles[0] is not read.
image string Cover URL. Must be absolute. Not coverURL.
author string
summary string Read from desc, then summary. description is never read, in a list result or in details.
tags array of strings Bare strings here, not objects. genres is not read.
medium string 'novel' selects the prose reader. Anything else is treated as comics. Overridden by the app's medium filter when one is set.
rating number
chapters number Chapter count for display. Whole numbers.
completed boolean Defaults to false.
publisher string

getMangaDetails(mangaId)

Async. Returns one object. Display fields may be nested under mangaInfo or placed flat at the top level, and both are read the same way:

return {
  mangaInfo: {
    title: 'Title',
    image: 'https://example.com/cover.jpg',
    author: 'Author',
    desc: 'Long summary text.',
    tags: ['Action'],
    status: 'ONGOING',
    medium: 'comics',
  },
};

Every field from the manga table above applies here too, including status. That table leaves status out, but the host reads it from search results as well, to drive the ongoing or completed badge on the Browse grid. Here it is uppercased and matched exactly against the table below.

Returned status Shown as
ONGOING Ongoing
COMPLETED, ENDED Completed
HIATUS Hiatus
ABANDONED, CANCELLED Cancelled
anything else, or absent Unknown

The id is not read from your response. The host uses the mangaId it passed in.

getChapters(mangaId)

Async. Returns a flat array. The host sorts it by number descending, so ordering on your side is not required, but the numbers must be right.

Field Type Notes
id string Required. Entries without one are dropped silently; chapterId is not read.
name string Display title, default "Chapter"; title is not read.
number number Defaults to 0. Used for sorting and the "Chapter n" label; chapNum is not read.
group string Scanlator or translator credit.
time number Upload date as epoch milliseconds. Not a Date, not an ISO string.

time must be numeric. The host treats a value above 1e11 as milliseconds and anything smaller as seconds, so a modern date in seconds still works, but milliseconds is the contract. A Date object or a string is ignored and the chapter shows no date. volume and pageCount are not read on this path.

Return the whole list. There is no paging on this method.

getChapterDetails(mangaId, chapterId)

Async. One method serves both comics and prose, and the app calls only the branch it needs based on the series medium.

Comics:

return {
  id: chapterId,
  mangaId,
  pages: [
    'https://cdn.example.com/1.jpg',
    'https://cdn.example.com/2.jpg',
  ],
  referer: 'https://example.com/',
};

pages must be an array of absolute URL strings. Objects with a url property are not accepted, and any entry that does not parse as a URL is dropped. Page order is the array order.

referer is optional. Some page-image hosts return 403 unless the request carries a Referer from their own site. If the site you are authorised to read needs that, set it to the site root and the app attaches that header to every page image request for the chapter. A host that blocks hotlinking to keep other apps out is applying an access control, so do not set referer to get around it. Leave it out entirely when the host does not need it, and never set it to an empty string.

Prose:

return { id: chapterId, mangaId, pages: [], text: chapterBody };

The chapter body is read from text, then body. Plain text with newlines is what the prose reader expects. Return pages: [] alongside it so an image reader never receives URLs it cannot display.

An empty string in text is treated as absent, and so is a missing property, so a chapter you cannot resolve should return no text rather than a placeholder.