Partner APIv1View as markdown

Novelmint Publish API — v1 Reference

Current version: v1. Last updated 2026-08-13.

Everything a business needs to send a finished catalogue to Novelmint and have it earn from readers. If you are still deciding whether to integrate, the programme rules — who owns the books, how consent works, how the revenue share is paid — are at /partners/docs.

This page is also available as plain markdown.


1. Versioning

The version is in the path.

https://novelmint.ai/api/publish/v1/...

Every response carries Novelmint-Publish-Version: 1.

What is allowed to change inside v1 — new endpoints, new optional request fields, new response fields, new reason_code values, new error codes. Your integration must ignore fields and codes it does not recognise; that requirement is the whole reason v1 can improve without breaking you.

What forces v2 — removing or renaming a field, changing a field's type, changing the meaning of an existing value, or making an optional request field required. A v2 is a new path. v1 keeps serving, and any retirement date is announced in writing with notice, never implied by a version bump.

Unversioned paths (/api/publish/books, without v1) exist from before versioning and forward to v1 permanently. They are safe to keep using, and a future v2 does not change what they serve — an unversioned caller is a v1 caller for good. New integrations should use the explicit path.


2. Authentication

A bearer token, minted in the partner dashboard under Tokens.

Authorization: Bearer pub_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

The secret is shown once, at creation. We store a hash; nobody at Novelmint can read your token back to you. Lose it and you mint a new one and revoke the old.

The token belongs to the business, not to the person who created it, so it survives staff changes.

Environments

PrefixBehaviour
pub_test_Runs the entire pipeline and stops before publishing. Nothing reaches readers. POST .../publish returns status: "draft" with reason_code: "test_environment".
pub_live_Publishes for real.

Build against a test token first. It exercises every code path you will hit in production except the last one.

Scopes

ScopeGrantsHow you get it
publish:createCreate books, add chapters, create grants, publishAlways
publish:readRead submission statusAlways
publish:updatePATCH a chapter already sentTick the box when you create the token
publish:deleteNot issued to anyone

publish:update is off by default and on in one click — a checkbox on the token form, no request and no waiting. Off by default so a token built for one-way ingestion cannot rewrite anything, which matters when the credential is sitting in someone's CI.

Scopes are fixed at creation. To change them, create a new token and revoke the old one; that is one action either way and it means a leaked token's powers never quietly grow.

publish:update only reaches books you still own. PATCH resolves ownership through the series, so the moment a writer accepts an on_behalf title it belongs to them and the endpoint stops applying to it. In practice the scope covers your own imprint and titles still held pending consent — never a writer's published book.

publish:delete is not issued and is not coming soon. As designed it would hand a partner a kill switch over a writer's catalogue after the relationship ended. Withdrawal requests go to a human at partners@novelmint.ai, deliberately.

Authentication failures

All are 401 except insufficient_scope (403) and https_required (400). The codes are deliberately distinguishable so a lapsed partnership does not look like a bad token.

CodeMeaning
unauthorizedMissing or malformed Authorization header
invalid_tokenNo such token, or the secret does not match
token_revokedYou revoked it
token_expiredPast its expiry
client_revokedThe vendor application bound to this token was revoked
not_partnerYour Platinum partnership lapsed. Every token stops at once
insufficient_scopeValid token, wrong scope

3. Conventions

Base URL https://novelmint.ai

Content type application/json on every request with a body.

Errors always take this shape, whatever went wrong:

{ "error": { "code": "title_required", "message": "A title is required." } }

Branch on code. message is written for a human reading a log and may be reworded at any time.

Idempotency, and what replayed means

Every write is safe to repeat. When a request turns out to be a duplicate, we return the original result rather than creating a second copy, and the response says so:

{ "book_id": "3fa85f64-…", "replayed": true }

"replayed": true means nothing new was created — this already existed and here it is again. false means this call is what created it. Two mechanisms decide it:

EndpointMatched on
POST /booksYour Idempotency-Key header, per partner. Same key, same book
POST .../chaptersThe content itself: same title and byte-identical prose

The practical consequence: a failed upload can simply be re-run. If your script dies after 30 of 41 chapters, run the whole book again — the 30 come back replayed: true, the missing 11 are created, and the book ends with 41 chapters rather than 71. You do not need to track how far you got.

Idempotency-Key is required on POST /books for this reason. Use something stable from your own system — your catalogue id works well — not a fresh UUID per attempt, which would defeat the point.

external_ref — your own catalogue id, up to 200 characters. Set it on create and it is echoed on every response about that book. Without it, reconciling a few hundred submissions against your system is a manual job.

Rate limits — keyed on the token, so a leaked credential can be cut off without stopping your other integrations.

GroupLimit
Ingestion (POST /books, .../chapters, /grants)2,000/hour
Go live (POST .../publish)500/day
Reads (GET /books/:id)600/minute

Over the limit returns 429 with Retry-After. A 200-title backlist is roughly 8,000 requests and lands inside a working day without touching a ceiling.


4. The two modes

Chosen per book at creation and fixed thereafter.

own_imprint — you own the work. It publishes under your own account and persona, immediately. No consent machinery.

On these titles you are the author, so you earn the author's 70% of reader spend rather than the partner's 10%. The two are not combined: the referral share exists to pay whoever brought someone else's book, and it is not paid to you on your own.

on_behalf — a writer owns the work and you have an agreement with them. The book is ingested and held: fully uploaded, invisible to everyone, published only once that writer consents on our own page. When they accept, ownership transfers to them, the book publishes on the terms you supplied, and you earn 10% of reader spend on it out of Novelmint's 30% share — never out of the writer's 70%.

You do not choose whether a title is held. The grant state decides.


5. The normal sequence

POST   /api/publish/v1/books                         -> book_id, status "draft"
POST   /api/publish/v1/books/{book_id}/chapters      -> repeat, one per chapter
POST   /api/publish/v1/books/{book_id}/publish       -> status "live" or "held_pending_author"
GET    /api/publish/v1/books/{book_id}                  reconcile, any time

6. Endpoints

6.1 Create a book

POST /api/publish/v1/books
Authorization: Bearer pub_live_…
Idempotency-Key: your-unique-key
Content-Type: application/json
FieldTypeRequiredNotes
titlestring1–300 characters
mode"own_imprint" | "on_behalf"Defaults to own_imprint
blurbstringBack-cover copy
languagestringISO code, defaults en
external_refstringYour catalogue id, ≤200 chars
persona_iduuidown_imprint only: publish under a persona you already own
author.user_iduuidon_behalf¹The writer's Novelmint account, if you know it
author.emailstringon_behalf¹The writer's email, if they have no account
author.pen_namestring²Name the book is attributed to before they accept. Need not be unique — see below

¹ on_behalf needs one of author.user_id or author.email. ² Required when identifying the writer by email.

201 Created when this call created it, 200 when it was already there (see replayed, §3)

{
  "submission_id": "8f14e45f-ceea-467a-9f4b-9a0b1c2d3e4f",
  "book_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "external_ref": "MER-2291",
  "status": "draft",
  "replayed": false,
  "links": {
    "chapters": "/api/publish/v1/books/3fa85f64-…/chapters",
    "publish":  "/api/publish/v1/books/3fa85f64-…/publish",
    "self":     "/api/publish/v1/books/3fa85f64-…"
  }
}

Errors

StatusCodeMeaning
400idempotency_key_requiredHeader missing
400invalid_jsonBody did not parse
400title_required / title_too_long
400invalid_modeNot one of the two modes
400author_requiredon_behalf with no author identifier
400pen_name_requiredIdentified by email, no pen name
403author_blockedThis writer has asked you not to send them anything. Do not retry
409idempotency_conflictA different request already used this key
422not_platinum_partner, cannot_grant_selfGrant could not be established

Pen names do not have to be unique. Two authors can publish here under the same name, the way two people can share a display name anywhere else. Only the web address has to be distinct, so a writer taking an existing name gets a number added to theirs. Nothing is blocked and you do not need to check first — the writer is told before accepting, and can change both the name and the address themselves afterwards.

What happens to the writer. Submitting on_behalf with an author.email sends that writer one email: what you sent, that nothing is published, and a link to accept or decline. One email per writer, not per title — a second book for the same person adds to the existing request rather than mailing them again. If you would rather bring them over from your own site instead, use the handoff flow (§6.5) and no email is sent at all.


6.2 Add a chapter

One chapter per request. There is no bulk endpoint: a 200k-word novel is over a megabyte of JSON, and a failed chapter should retry alone.

Order of arrival does not matter, provided you say where each chapter goes. Send chapter_number and the chapter lands at exactly that position, so a job queue can post concurrently and out of sequence. Omit it and chapters are appended in the order they arrive, which is only safe if you are posting them one after another.

POST /api/publish/v1/books/{book_id}/chapters
FieldTypeRequiredNotes
contentstringThe prose. 1 MB maximum, measured in UTF-8 bytes
titlestringDefaults to Chapter {n}
role"main" | "prologue" | "epilogue" | "front_matter"Defaults main
chapter_numberintegerWhere it goes. main chapters only; honoured exactly. Omit to append. Positions must be unique — a clash is 409 chapter_number_taken

201 Created when this call created it, 200 when it was already there (see replayed, §3)

{ "chapter_id": "…", "chapter_number": 12, "role": "main", "replayed": false }

Errors

StatusCodeMeaning
400content_required / chapter_empty
400invalid_role
404submission_not_foundBook not found, or not yours
409chapter_number_takenA book holds one prologue
413chapter_too_largeOver 1 MB

A book you do not own returns 404, not 403, so partners cannot probe each other's ids.


6.3 Correct a chapter

Requires publish:update, which you enable with a checkbox when creating the token (§2).

PATCH /api/publish/v1/books/{book_id}/chapters/{chapter_number}

Send content, title, or both. Addressed by chapter number — what your catalogue knows — and it never moves the chapter: fixing a typo in chapter 12 must not reshuffle the book.

200 OK

{ "chapter_id": "…", "chapter_number": 12, "content_changed": true }

6.4 Publish

POST /api/publish/v1/books/{book_id}/publish
FieldTypeNotes
genrestringDefaults uncategorized
cover_urlstringhttps only. We fetch and host it. Required unless a cover is already attached
blurbstringOmit to leave the saved blurb alone
credits_per_chapterinteger1–10, defaults 5

200 OK — live

{
  "submission_id": "…", "book_id": "…", "external_ref": "MER-2291",
  "status": "live",
  "book_url": "https://novelmint.ai/read/jo-ashworth/the-salt-road"
}

202 Accepted — held (on_behalf, writer has not consented yet)

{
  "status": "held_pending_author",
  "reason_code": "awaiting_author_consent",
  "message": "Ingested and held. This title publishes when the writer accepts."
}

Nothing further is required from you. When the writer accepts, the book publishes on the terms in this call — genre, blurb, price and cover are remembered.

Idempotent: publishing an already-live title returns its current state with "replayed": true.

422 — rejected. reason_code is the gate's own code:

reason_codeMeaning
manuscript_onlyAdult-rated. Cannot be sold here, whatever your contracts say
fan_fiction_manuscript_onlyThe characters are not yours to license
derivative_unresolvedContent review flagged possible third-party characters; the writer must resolve it
no_coverNo cover attached and none supplied
no_proseNo chapter has any content
cover_url_not_httpsCover URL must be https
cover_url_blockedResolves to an address we will not fetch
cover_too_largeOver 10 MB
cover_not_an_imageNot a JPEG, PNG or WebP — decided by content, not by the header you sent
cover_unreachableWe could not fetch it

The first three are permanent for that title. Retrying will not change the answer; they are policy, not transient failures.


The strongest and fastest way to get a writer's consent, and the one that involves no email from us at all.

POST /api/publish/v1/grants
{
  "author": { "email": "jo@example.com", "pen_name": "Jo Ashworth" },
  "return_url": "https://your-site.example/novelmint/done"
}

Redirect the writer's browser to the handoff_url you get back. They confirm in their own Novelmint session and we send them to return_url.

What the writer sees, in order. The page loads before any sign-in: who sent their books, the exact titles, that nothing is published, and what the terms are. Signing in is asked for only at the point of deciding, and creating an account is free.

That order is deliberate. A demand for credentials arriving out of the blue, about books someone did not know had been submitted, is indistinguishable from a phishing attempt — and the list of titles is the one thing that lets a writer tell a real submission from an impersonation, so it has to be readable before they commit anything.

A writer with no account signs up at that point and is returned to this page, then to your return_url, without losing their place.

201 Created

{
  "grant_id": "…",
  "state": "pending",
  "handoff_url": "https://novelmint.ai/publish/claim/gr_…?return_to=https%3A%2F%2Fyour-site.example%2Fdone",
  "expires_in": 3600
}

Send the writer to handoff_url as given. Your return_url is already encoded into it; do not rebuild it or strip the query string.

  • return_url is validated by exact origin against the website on your approved application. An unregistered origin is refused (422 return_url_not_allowed).
  • The handoff token is single-use and lives one hour. Long enough for a writer with no account to sign up, find a magic link in their inbox and come back; short enough that a URL sitting in browser history or a Referer log stops being useful the same day. If it does lapse, call this endpoint again for a fresh one.
  • Already-consented writer: 200 with "state": "active" and handoff_url: null. Nothing to confirm — just submit titles.
  • A request is already open: 200 with a fresh handoff_url and "reissued": true. Call this again whenever you need to: the writer closing the tab is the ordinary reason, and the token may well have lapsed anyway. Re-issuing invalidates the previous link, so there is never more than one live claim credential for a writer.
  • 403 author_blocked — the writer has blocked you. Do not retry.
  • 409 grant_not_pending — they already answered.

6.6 Read a submission

GET /api/publish/v1/books/{book_id}
{
  "submission_id": "…", "book_id": "…", "external_ref": "MER-2291",
  "status": "live", "reason_code": null, "mode": "on_behalf",
  "title": "The Salt Road", "chapter_count": 41, "has_cover": true,
  "content_rating": "T", "rating_status": "complete",
  "chapters_classified": 41, "chapters_needing_review": 0,
  "book_url": "https://novelmint.ai/read/jo-ashworth/the-salt-road",
  "created_at": "2026-08-13T09:12:04Z", "updated_at": "2026-08-13T11:40:22Z"
}

chapter_count counts every chapter including prologues, which is what you mean when you ask whether all forty arrived.

Poll this to reconcile. There are no webhooks in v1.


6.7 Content rating

We rate every chapter you send. You do not declare a rating, and we do not take one.

Each chapter is classified by us after the title goes live. The result decides two things: the book's rating, and whether that chapter is readable at all.

  • Confidently rated → the chapter is published and readable.
  • Not confidently rated → the chapter is held private and its owner is asked to confirm. We never publish prose on a guess about what is in it.

Classification is asynchronous, so POST .../publish always returns "rating_status": "pending". Poll GET /books/{book_id}:

FieldMeaning
content_ratingThe book's derived rating. null until chapters are classified
rating_statuspendingin_progresscomplete, or needs_review
chapters_classifiedHow many are done
chapters_needing_reviewHeld private, waiting on a human

needs_review outranks complete: a book with one unrated chapter is not finished, and it is the state worth chasing.

A book that classifies as Adult becomes manuscript-only and is unlisted — that is manuscript_only in §6.4, arrived at from the prose rather than from anything you told us. If your catalogue contains adult titles, they will be refused here whatever your contracts say.


7. Statuses

statusMeaning
draftCreated, accumulating chapters. Invisible to everyone but you
publishingTransient, during the publish call
held_pending_authorIngested, waiting on the writer. Invisible to readers
livePublished. book_url is populated
rejectedRefused. reason_code says why

A held title becomes rejected with reason_code: "grant_expired" if the writer never answers within 30 days. Your content is not deleted — start a fresh request and the chapters are still there.


8. Costs

Ingestion is free. Creating books and posting chapters debits nothing: no beats are extracted from finished prose, so none of the generation pipeline runs.

Per-chapter content classification (§6.7) runs after publish and is absorbed by Novelmint. It is the only model cost on this path and it is never billed to you.

There is no fee to join, per book, or per writer.


9. Worked example

A 41-chapter novel by a writer you represent, from nothing to live.

BASE=https://novelmint.ai/api/publish/v1
TOKEN=pub_test_…

# 1. Consent first, so the book is never held.
curl -sX POST "$BASE/grants" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"author":{"email":"jo@example.com","pen_name":"Jo Ashworth"},
       "return_url":"https://your-site.example/done"}'
# -> redirect the writer's browser to handoff_url

# 2. The book shell.
BOOK=$(curl -sX POST "$BASE/books" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -H "Idempotency-Key: MER-2291-create" \
  -d '{"mode":"on_behalf","title":"The Salt Road","external_ref":"MER-2291",
       "author":{"email":"jo@example.com","pen_name":"Jo Ashworth"}}' \
  | jq -r .book_id)

# 3. Chapters, one at a time, in order.
curl -sX POST "$BASE/books/$BOOK/chapters" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"title":"Before the Rain","role":"prologue","content":"..."}'
# ... repeat for each chapter ...

# 4. Go live.
curl -sX POST "$BASE/books/$BOOK/publish" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"genre":"fantasy","credits_per_chapter":5,
       "cover_url":"https://your-cdn.example/salt-road.jpg"}'

# 5. Reconcile.
curl -s "$BASE/books/$BOOK" -H "Authorization: Bearer $TOKEN"

10. Not in v1

Stated so you can plan around them rather than discover them.

  • Webhooks. Poll GET /books/{id}.
  • File upload / EPUB. Send structured chapters; you know where your chapter boundaries are and a parser only guesses.
  • Deleting a published book. Withdrawal requests go to a human.
  • Partial consent. A writer accepts everything you have sent them, or nothing.
  • Bulk endpoints. One book per request, one chapter per request.

11. Support

partners@novelmint.ai for integration questions. Include your external_ref and submission_id; both appear on every response for exactly this reason.

Not a partner yet? Apply here — approval is by hand, and tokens are minted from your dashboard once you are through.