Partner documentation

Publish/Export API

Version 1

The integration contract for external platforms that receive work published from Inkroost. Implement three endpoints, and creators can send their series and chapters straight to you — no code is deployed on our side per partner.

OAuth2 authorization-code Two delivery modes JSON manifest + ZIP package

1. Overview

Inkroost creators can publish a series or a single chapter directly to your platform. The creator authorizes the link between their Inkroost account and your platform once (OAuth2, section 2); after that, each publish delivers a JSON manifest and a ZIP package of the artwork to your ingest endpoint (section 3).

To integrate, your platform implements three endpoints — an OAuth2 authorization page, an OAuth2 token endpoint, and an ingest endpoint — and registers our callback URL. On the Inkroost side, an administrator enters your endpoint URLs and OAuth client credentials, and creators can start publishing to you.

2. OAuth2 authorization

Inkroost is a standard OAuth2 client using the authorization-code grant. You issue us a client_id and client_secret for your platform.

Our callback URL — register it as the redirect URI

https://inkroost.laravel.cloud/integrations/callback

Stable and identical for every partner. Requests arriving with a missing or altered state are rejected.

  • Authorization request. We send the creator's browser to your authorization URL with response_type=code, client_id, redirect_uri, state, and optionally scope.
  • Return. After the creator consents, redirect the browser to our callback above with the code and the unchanged state.
  • Token exchange. We POST grant_type=authorization_code with the code, redirect_uri, client_id and client_secret (form-encoded) to your token URL, and expect a JSON response with access_token, and optionally refresh_token and expires_in (seconds).
  • Refresh. When the access token expires and a refresh_token was issued, we POST grant_type=refresh_token to the same token URL before publishing.
  • Revocation. Creators can disconnect at any time on their side, which deletes the stored tokens. Treat any long-unused token as revocable on your side too.

Tokens are stored encrypted at rest, per creator and per platform, and are never written to logs.

3. Receiving a publish

Your platform receives each publish in one of two delivery modes, chosen when the integration is registered. Both carry the same manifest (section 5) and the same ZIP package (section 4) — they differ only in how the package bytes travel.

Mode A — one POST default Mode B — signed upload URL
Best for Ingest endpoints that accept large streamed bodies Hosts with strict request-body limits (serverless)
Requests One multipart POST with manifest + ZIP Manifest POST → ZIP PUT → status polling
You implement The ingest endpoint The ingest endpoint + a status endpoint + signed URLs

Mode A — one multipart POST (default)

Each publish is one POST to the ingest URL you provide, authenticated with the creator's OAuth2 access token as a bearer token:

POST /your/ingest/endpoint Inkroost → your platform
Authorization: Bearer <creator access token>
Content-Type: multipart/form-data

manifest = <JSON string, the manifest described in section 5>
package  = <ZIP file, the package described in section 4>

Respond with:

Status Meaning What happens next
2xx Accepted. The JSON body may include id (your identifier for the received work) and url (a public link to it). If url is present, the creator is shown a "View on platform" link.
401 / 403 The token is invalid or revoked. The creator is told to reconnect.
Other 4xx The publish is refused. Include error, error_description or message in a JSON body. The creator sees a shortened version of your message. Not retried.
5xx / timeout Treated as transient. Retried once after a short delay, then failed.

Uploads can be large (see limits, section 6) — accept streamed multipart bodies and allow several minutes for a full-series publish.

Mode B — signed upload URL (two-phase)

For platforms whose ingest cannot accept a large request body, the package bytes bypass your ingest endpoint entirely:

  • Publish requestmanifest only
  • Package uploadone raw PUT
  • Status pollinguntil terminal

3B.1 — Publish request

A small POST to your ingest URL, bearer-authenticated, with a JSON body — no ZIP:

POST /your/ingest/endpoint Inkroost → your platform
Authorization: Bearer <creator access token>
Content-Type: application/json

{
  "source_language": "ja",            // ISO 639-1, always present; repeated inside the manifest
  "manifest": { … },                  // the manifest described in section 5
  "package": {
    "name": "my-series-123.zip",
    "size_bytes": 48211930,
    "content_type": "application/zip"
  }
}

Respond 2xx with a short-lived signed upload URL and your identifier for the publication:

200 OK your platform → Inkroost
{
  "upload_url": "https://…",          // required — https, pre-signed for one PUT of size_bytes
  "id": "your-publication-id",        // required — used to poll your status endpoint
  "status_url": "https://…"           // optional — overrides the registered status URL for this publish
}

Error responses use the same mapping as Mode A: 401/403 → reconnect, other 4xx → refused with your message, 5xx/timeout → retried once. Nothing has been uploaded at this point.

3B.2 — Package upload

The ZIP archive bytes are sent in a single raw PUT to upload_url with Content-Type: application/zip. The signed URL authenticates itself — no Authorization header is sent.

The PUT is the completed upload

There is no separate "upload complete" request — begin validating and publishing when the object lands.

3B.3 — Status polling

Validation and publication happen asynchronously on your side. The status endpoint is polled with the creator's bearer token, starting about 2 seconds after the upload and backing off to every 30 seconds, for up to 15 minutes:

GET <status_url from 3B.1> Inkroost → your platform · or <registered status URL>?id=<id>
Authorization: Bearer <creator access token>

Respond 2xx with the publication's state:

Response Terminal? What happens next
{ "status": "processing" } No Any non-terminal state keeps polling.
{ "status": "published", "id": "…", "url": "https://…" } Yes The creator sees the public url; id may repeat or refine the id from 3B.1.
{ "status": "failed", "message": "Why it was refused" } Yes The creator sees a shortened version of your message (or error / error_description). The package is not re-uploaded.
  • Any other status value, a 5xx, or an unreachable endpoint is treated as still in progress and polled again. A publish that never reaches a terminal state within the polling window is reported to the creator as unconfirmed.
  • 401 / 403 — the creator is told to reconnect.

4. The package

package is a ZIP archive. Entry names are chosen so a plain lexical sort reproduces reading order:

Archive layout
manifest.json                 — the same JSON as the manifest form field
cover.png | cover.jpg | cover.webp   — series cover artwork, when one exists
chapter-001/page-001.png      — page artwork, zero-padded, in reading order
chapter-001/page-002.png
chapter-010-5/page-001.png    — fractional chapter numbers (10.5) use a dash

Asset technical specifications

  • Page images: PNG or JPEG (the original files as created — the extension always matches the bytes). RGB/sRGB, 8-bit per channel, no embedded color profiles required.
  • Page dimensions: width is typically 800–2048 px. Height is unbounded — vertical-scroll (webtoon) pages can be many times taller than wide, so never assume a fixed aspect ratio. Traditional pages are commonly 2:3 portrait. The series' reading_mode in the manifest (paged or vertical) says which presentation the creator designed for.
  • Cover: PNG, JPEG or WebP, portrait, typically 600–1200 px wide.
  • Ordering: trust the entry names (or the manifest's chapters[].folder values), never file timestamps.
  • Missing pages: a page whose artwork could not be read is skipped and counted in the manifest's pages_missing; the numbering of the remaining entries stays gapless.

5. The manifest

The manifest form field and the manifest.json archive entry are the same JSON document:

manifest.json
{
  "generator": "Inkroost",
  "manifest_version": 1,
  "published_at": "2026-08-30T12:00:00+00:00",
  "scope": "series",                  // or "chapter"
  "series": {
    "id": 123,                        // stable Inkroost series id — attach future chapters to it
    "title": "…",
    "slug": "…",
    "source_language": "ja",          // ISO 639-1 source language; always present, "en" when the creator never chose one
    "language": {                     // the same language in every code format a partner might require
      "code": "ja",
      "iso_639_1": "ja",
      "iso_639_2": "jpn",
      "bcp47": "ja-JP",
      "name": "Japanese"
    },
    "synopsis": "…",
    "reading_direction": "ltr",       // "ltr" or "rtl" (manga)
    "reading_mode": "paged",          // "paged" or "vertical" (webtoon)
    "maturity_rating": "all_ages",    // "all_ages", "teen" or "mature"
    "genres": ["Fantasy", "Action"],
    "content_origin": "…",            // "human" (no AI), "ai_assisted" (AI draws all images) or "mixed" (some pages human, some AI)
    "cover": "cover.png",             // archive entry name, or null
    "source_url": "https://…"         // canonical URL on Inkroost, or null
  },
  "creator": {
    "name": "…",                      // the creator's public name — attribute the work to it
    "profile_url": "https://…"        // or null
  },
  "chapters": [
    {
      "id": 456,                      // stable Inkroost chapter id — dedupe repeat submissions with it
      "number": 1,
      "title": "…",
      "folder": "chapter-001",        // where this chapter's pages live in the ZIP
      "page_count": 24,               // pages actually in the ZIP
      "pages_missing": 0,
      "published": true,              // false = still a draft on Inkroost
      "access_mode": "free",          // or "supporter_only"
      "source_url": "https://…"
    }
  ],
  "chapter_count": 1,
  "page_count": 24,
  "pages_missing": 0
}

Fields may be added in future versions; unknown fields must be ignored. A breaking change increments manifest_version. series.id and chapters[].id are stable across publishes — use them to attach new chapters to an existing series and to recognise a re-publish of a chapter you already carry.

6. Limits

  • At most 500 pages per publish. A larger series is published chapter by chapter.
  • One in-flight publish per creator, per destination, per work — duplicates are coalesced on our side.
  • Publishes per creator are rate-limited on our side; you may apply your own limits too and refuse with a 4xx.

7. Security expectations

  • All registered endpoint URLs must be https; http endpoints are rejected at configuration time. In Mode B the upload_url and status_url you return must be https too — anything else is treated as absent — and signed URLs are never written to logs on our side.
  • Only content the publishing creator owns is ever packaged — ownership is checked when the publish is requested and again when it is sent.
  • Chapters with "published": false or "access_mode": "supporter_only" are included because the owner chose to send them — honor the flags when deciding what to show publicly on your side.
  • OAuth tokens and client secrets are stored encrypted and never logged, on either side of a correct implementation.

8. Becoming a partner

Implement the three endpoints, then send your authorization URL, token URL, ingest URL, scopes (if any), OAuth client credentials, and your preferred delivery mode (Mode A by default; for Mode B, include your status URL) to hello@inkroost.com. We register the integration and it appears on every creator's Integrations page.