Partner documentation
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.
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.
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.
response_type=code, client_id,
redirect_uri, state, and optionally scope.
code and the unchanged state.
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_token
was issued, we POST grant_type=refresh_token to the same token URL before
publishing.
Tokens are stored encrypted at rest, per creator and per platform, and are never written to logs.
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 |
Each publish is one POST to the ingest URL you provide, authenticated with the
creator's OAuth2 access token as a bearer token:
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.
For platforms whose ingest cannot accept a large request body, the package bytes bypass your ingest endpoint entirely:
A small POST to your ingest URL, bearer-authenticated, with a JSON body — no ZIP:
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:
{
"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.
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.
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:
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. |
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.
package is a ZIP archive. Entry names are chosen so a plain lexical sort
reproduces reading order:
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
reading_mode in the manifest (paged or
vertical) says which presentation the creator designed for.chapters[].folder values), never file timestamps.pages_missing; the numbering of the remaining
entries stays gapless.
The manifest form field and the manifest.json archive entry are the
same JSON document:
{
"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.
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."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.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.