Building sources

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 22 August 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 tries to construct a source with no arguments, in this order:

  1. new module.exports.Source()
  2. new module.exports['<source-id>']() where the id is the folder name in the repository
  3. new Source() as a global

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() An in-memory { retrieve, store, keychain } store. Not persisted across app launches.
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 A host-installed shim. See the overview for the caveat.

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.

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 or id string Required. Must be a string. A numeric id is dropped, so stringify it.
title, or titles[0] string Falls back to "Untitled".
image string Cover URL. Must be absolute. Not coverURL.
author string
summary or description string In list results only. Details use desc, see below.
tags or genres array of strings Bare strings here, not objects.
webURL string The series page on the source site, used by the in-app browser button.
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
views number Whole numbers.
chapters number Chapter count for display. Whole numbers.
completed boolean Defaults to false.
publisher string
releaseDate string Kept and displayed as a string, not parsed.
publishingStatus string Free-form label, separate from status.

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',
    webURL: 'https://example.com/series/slug',
  },
};

Every field from the manga table above applies here, with two differences:

  • The summary is read from desc first, then description. summary is not read on this path, which is a common source of an empty detail page on a source whose list results look fine.
  • status is read only here. The value is uppercased and matched exactly.
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 or chapterId string Required. Entries without one are dropped silently.
name or title string Display title.
number or chapNum number Defaults to 0. Used for sorting and for the "Chapter n" label.
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 and exists for hotlink protection: some page-image CDNs return 403 unless the request carries a Referer matching the source's own site. Set it to the site root and the app attaches that header to every page image request for the chapter. Leave it out entirely when the CDN 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.