Build a source.

How to write a Kiyume source, test it against the real site, and publish it in a repository of your own.

In short

  • A source is one JSON file that describes four requests — search, series details, chapters, pages — and how to read their answers. It can’t run code.
  • A repository is a signed list of sources, hosted as static files on any HTTPS site you control.
  • One tool creates the project, tests it against the real site, signs everything and builds the files to upload.

Before you start

You’ll need:

  • a Mac with macOS 14 or later, and the Kiyume source tool (6 MB, Apple Silicon and Intel). No Xcode needed. Unzip it and install it from Terminal:
    cd ~/Downloads/kiyume-source-tool
    xattr -d com.apple.quarantine source-validator
    sudo mkdir -p /usr/local/bin
    sudo mv source-validator /usr/local/bin/
    The xattr line is there because the tool isn’t notarised by Apple yet: without it, macOS refuses to open a program downloaded from the web.
  • static hosting with HTTPS for your repository: any web host, or a service such as GitHub Pages;
  • the address of the site you want to read, and the right to publish an adapter for it. Read its terms first.

A source contains no manga. It doesn’t add screens to Kiyume and can’t reach the network, files or the device on its own: Kiyume does the requests for it, only to the addresses it declares, over HTTPS.

Pick the simplest kind

  1. The site has a JSON APIUse a JSON source. It’s the most stable kind, because APIs change less often than page layouts.
  2. The site only returns web pagesUse an HTML source, which reads the pages with CSS selectors.
  3. It’s your own server at homeDon’t publish a repository for it. Connect it directly in Kiyume instead: it supports Komga.

Create the project

Start from the site’s address. For a site that returns web pages:

source-validator init https://examplescans.com my-source --name "Example Scans" --developer "Your name" --language en

For a site with a JSON API, add --format json. The command creates a folder with:

FileWhat it is
manifest.jsonWho made the source, what it reads, and what it’s allowed to reach.
source.jsonThe definition: the four requests and how to read them.
kiyume-publish.jsonSettings for publishing your repository.
private/Two private keys, one for the source and one for the repository. Already excluded by .gitignore.
README.mdNext steps, including a ready-made brief for an AI coding assistant.

It never writes into a folder that already has files in it. The generated definition is a valid starting shape, not a working source: no tool can tell from an address alone which selectors or fields a site uses.

The manifest

{
  "manifestVersion": 1,
  "id": "en.examplescans",
  "name": "Example Scans",
  "developer": "Your name",
  "version": "0.1.0",
  "apiVersion": "1.0.0",
  "minAppVersion": "1.0.0",
  "languages": ["en"],
  "content": { "rating": "teen", "minimumAge": 13 },
  "allowedDomains": ["examplescans.com"],
  "capabilities": ["network"],
  "implementation": "declarative",
  "homepageURL": "https://examplescans.com",
  "sourceURL": "https://examplescans.com"
}
  • id — language, then site: en.examplescans, it.esempio, multi.example. Lowercase letters, numbers, dots and dashes.
  • version — raise it for every update you publish. A published version can never be replaced.
  • languages — the languages of the catalogue, as language codes.
  • content — rating is everyone, teen, mature or adult, with an optional minimumAge and descriptors such as violence or nudity. An adult catalogue must be rated adult and 18+. Review the starting value: it’s teen only because a manifest can’t publish an unknown rating.
  • allowedDomains — every host the source will contact, including the one serving images. Kiyume refuses any other address.
  • minAppVersion — the oldest Kiyume that can read your definition. Raise it when you use a newer feature, or older versions will install the source and fail.

The signing fields, integrityHash and signingIdentity, are filled in by the tool when it signs the package. Don’t edit them by hand.

The definition

A definition describes where to send four requests and how to read the answers. It can’t loop, branch, or run code: everything in it is data Kiyume interprets.

{
  "schemaVersion": 1,
  "search": {
    "endpoint": { "url": "https://examplescans.com/search", "query": { "q": "{query}" } },
    "format": "html", "records": "article.manga",
    "manga": {
      "id": { "attribute": "data-id" },
      "title": { "path": "h2" },
      "coverURL": { "path": "img", "attribute": "src" }
    },
    "nextPage": "a.next"
  },
  "manga": {
    "endpoint": { "url": "https://examplescans.com/manga/{id}" },
    "format": "html", "record": "main.manga",
    "manga": {
      "id": { "attribute": "data-id" },
      "title": { "path": "h1" },
      "authors": { "path": ".author" },
      "description": { "path": ".description" },
      "status": { "path": ".status" }
    }
  },
  "chapters": {
    "endpoint": { "url": "https://examplescans.com/manga/{mangaID}/chapters" },
    "format": "html", "records": "li.chapter",
    "chapter": {
      "id": { "attribute": "data-id" },
      "title": { "path": ".title" },
      "chapterNumber": { "attribute": "data-number" },
      "language": { "constant": "en" }
    }
  },
  "pages": {
    "endpoint": { "url": "https://examplescans.com/chapter/{chapterID}" },
    "format": "html", "records": "img.page",
    "page": { "imageURL": { "attribute": "src" } }
  }
}

Each request can use these values in {braces}:

RequestValues
search{query}, {page}, {offset}, {filter_<id>}
latest (optional){page}, {offset}
manga{id}
chapters{mangaID}
pages{chapterID}

{offset} exists only when the request declares pageSize, and equals (page − 1) × pageSize: many APIs count records rather than pages.

Declare in schemaVersion the version of the features you use: 4 for path filters, alternatives and the newer transforms, 5 for collections, 6 for chapter lists spread over several pages. A feature used under a lower version is rejected.

Reading the response

HTML is read with CSS selectors. JSON is read with a dotted path, with a few extra forms:

PathMeans
attributes.title.enPlain keys, and array positions by number.
relationships[type=author].attributes.nameEvery array element whose field has that value.
attributes.tags[].attributes.name.enEvery element of an array.
attributes.title.*Any one value of an object — useful when titles are keyed by language.

Each field reads from exactly one of path (the record), constant (a fixed text), valueName (a value from an earlier request), parts (several rules joined) or alternatives (the first rule that finds something).

Then transforms run in order: trim, lowercase, uppercase, prefix, suffix, replaceLiteral, split, join, first, last, extractNumber, mapValue, mapAny. extractNumber turns “Chapter 12.5” into 12.5; mapValue turns the site’s words into Kiyume’s, like “Completed” into completed.

When a page list needs something from another page first — a host name, a chapter key — use up to three preflight requests before the final one, and read their values with valueName.

Fields worth the effort

  • chapter.externalURL — for licensed series the site links to rather than hosts. Kiyume then opens the official release instead of showing a chapter that can’t load.
  • chapter.publishedAt with publishedAtDate — date formats tried in order: iso8601, unix, or a date pattern with its locale, since month names change with the language.
  • manga.viewerHint — rightToLeft, leftToRight or verticalContinuous. Kiyume trusts it over guessing from tags.
  • manga.publicationType — manga, manhwa, manhua, webtoon, oneShot and more. Map it in search results too: the reader needs it before the first chapter opens.
  • filters — text, single choice or on/off controls, offered in Kiyume before a search. Their values arrive as {filter_<id>}.
  • collections — up to eight shelves such as “Popular” or “Recently updated”, each one the search seen through fixed filter values. Declare a shelf only if the site can really answer it.

Test it

source-validator definition source.json
source-validator manifest manifest.json
source-validator definition-live-health manifest.json source.json "a title to search"

The first two commands check the files. The third runs the source against the real site, with the same limits Kiyume applies, and checks search, series details, chapters, pages and the first image.

Valid isn’t the same as workingA definition can pass every check and still read nothing. Only the live test shows a renamed field or a moved list, and running it again a few hours later catches fields that are only sometimes there.

Publish a repository

A repository can hold as many sources as you like. It’s only a signed list: readers add it once, see your sources with their logos, and choose which to install.

If you started with init, your keys and kiyume-publish.json already exist. Otherwise, create two different keys once, and keep private/ out of version control:

mkdir -p private
source-validator key-generate private/source.key
source-validator key-generate private/repository.key

Then fill in kiyume-publish.json — above all your public HTTPS address and a contact for reports — and add a square icon.png for the logo:

{
  "schemaVersion": 1,
  "publicBaseURL": "https://sources.example.com/",
  "outputDirectory": "public",
  "repository": {
    "id": "example.sources",
    "operatorName": "Example Sources",
    "keyID": "example-repository-key",
    "privateKey": "private/repository.key",
    "allowedDomains": ["sources.example.com"],
    "feedbackURL": "mailto:sources@example.com"
  },
  "source": {
    "manifest": "manifest.json",
    "payload": "source.json",
    "privateKey": "private/source.key",
    "icon": "icon.png",
    "filters": null
  }
}
source-validator publish kiyume-publish.json

The command checks both files, signs and verifies the source package, creates or updates the signed list without dropping earlier versions, and builds a small web page for your repository. It refuses to overwrite a different package with the same name and version.

Upload only the folder named in outputDirectory. Never upload, commit or share the private keys. To publish an update, raise version in the manifest and run the same command again.

Share it

The generated page has two buttons, Add repository to Kiyume and Install source. On a device with Kiyume, they open a preview with your name, the addresses your sources may contact and your key’s fingerprint. Nothing is ever installed without the reader confirming.

Publish your repository key’s fingerprint somewhere readers already trust — your site, your profile, your project page — so they can check it matches what Kiyume shows.

Limits

  • HTTPS only, and only the domains in the manifest, whatever the definition asks for.
  • No loops, no conditions, no arithmetic beyond {offset}.
  • Paths up to 32 steps, 8 transforms per field, 3 preflight requests per operation.
  • A chapter list is one request, unless it declares pageSize under schema 6: then up to 20 pages are read.
  • Packages up to 10 MB, and responses up to 8 MB.

Ground rules

A repository should host only its signed list, the signed sources, their logos, an information page and a way to reach you — never pages, chapters, copied covers, credentials or mirrors of a site. Rate content honestly, answer reports and remove a contested source promptly. The full list is on the Sources page.

Signing and Kiyume’s limits stop a source from misbehaving technically. They don’t decide whether an adapter is allowed by a site’s terms or by the law: that’s up to you. This guide isn’t legal advice.

Questions about the format? Write to support@kiyume.app.