← Book Summary / API
Tokens

Driving Book Summary from your own code

Book Summary is a SkillSafe app, so it is reachable through the generic app API. Base URL:

https://api.skillsafe.ai/v1/app-api

The app is identified by the token you call with, not by a header. There is no X-App-Slug: a token minted for book-summary already scopes every call to this app.

The response envelope

Every endpoint returns the same two-key envelope. Exactly one of the keys is present.

{"ok": true,  "data": { ... }}
{"ok": false, "error": {"code": "INSUFFICIENT_CREDITS", "message": "..."}}
CodeHTTPWhat it means
UNAUTHORIZED401Missing, malformed or expired token.
FORBIDDEN403A guest token tried a signed-in-only call. /run, /run-stream and /search all require a personal token.
INSUFFICIENT_CREDITS402Balance below min_credits. Call /estimate first and you will never see this.
VALIDATION_ERROR400The input object is the wrong shape, or a search provider was requested that this app has not declared.
RATE_LIMITED429Too many calls, or this app has spent its daily upstream search quota.
NOT_FOUND404Unknown job id or record id.

What this app takes and returns

These fields are taken from the running app.js, not from intent. Only title is required; everything else changes the brief rather than gating it.

FieldTypeNotes
titlestringRequired. Up to 300 characters.
authorstringOptional but load-bearing: it is what makes the grounding search land on one article rather than a disambiguation page.
formenumnovel, non-fiction, poetry, song, play, short-story, memoir, essays, reference, graphic-novel, unknown. Setting poetry, song or short-story forbids quotation entirely.
readerstringWho is asking and why. Changes what the brief leads with more than any other field.
askstringWhat you want from it. Up to 1200 characters; the browser clips longer input from the middle and marks the cut.
spoilersenumavoid or ending-ok.
depthenumquick, standard, thorough. Scales structure, themes and the reading list. It never scales the quotation budget.
sourcesarrayRetrieved records, each {id, title, abstract, url} with id of the form SRC-1. See step 3.
page_factsobjectWhat the caller worked out for itself: records_retrieved, form_read_as, form_read_from, short_form.
ask_flagsarrayReproduction requests the caller's own guard spotted, each {matched, why, instead}. The model must address these in declined.
follow_upobject{of, previous, goal} for a second pass that holds the facts steady.

What it will refuse

This app takes a copyrighted work as input, and the line it holds is enforced on the server as well as in the browser — a client-only guard is decorative when the API is public. Summary, synopsis, thematic analysis, structural breakdown, context, comparison and reading guidance are all in scope and are produced generously, including when the stated reason is that you do not want to read the book. That is a normal request.

What you will not get back is the work's text: no extended quotation, no reproduced passage, page, chapter, stanza or lyric, and no chapter-by-chapter paraphrase dense enough to substitute for reading. Brief quotation carrying a critical point is allowed within a fixed budget — 3 extracts, 25 words each, 50 words total, each attributed and each with a stated purpose — and for poetry, songs and short stories the budget is zero. Asking anyway produces a declined entry and the in-scope version of the request, not an error.

1. Get a token

Easiest route: open the token page in a browser, sign in, and press Copy shell export. Or mint a guest token, which is enough for /me and /estimate but not for /search or /run.

2. Check who you are

/me returns exactly three fields: subject_type, subject_id and credits. There is no email or name to key off — the signed-in test is subject_type === "user".

3. Ground the book — the step that makes this app worth calling

Skip this and you still get a brief, but every fact in it is unchecked model recall, which is precisely where invented books, wrong authors and non-existent editions come from. This app declares one search provider, web.wikipedia. Signed-in callers only.

Records come back as {record_id, title, abstract, url} plus a top-level retrieved_at. Re-label them SRC-1, SRC-2… in the order you send them, and pass them as sources.

The part that is easy to get wrong. These are MediaWiki snippets, not article infoboxes. A snippet about the right book very often does not mention the year, the language, or the author's full name. The brief is required to cite a source only when that source's own words carry the fact, and to write model-knowledge otherwise.

If you are building your own verification on top of this, check that the cited abstract contains the claimed value — not merely that some record id was cited. Confirming only that a citation exists is satisfied by citing a real page for a claim that page never makes, which manufactures the appearance of rigour around a confabulation. That failure is worse than an uncited claim, not better, and should be surfaced more loudly.

4. Price the run — free, no job created

/estimate costs nothing and creates nothing. It returns model, model_alias, markup_bps, hold_credits, min_credits and sponsor_enabled. Compare hold_credits against the balance from /me before you submit and a 402 becomes impossible.

hold_credits is what is reserved, not what you pay: it prices the full output cap. The actual charge comes back as charged_credits and is usually far lower.

5. Run it and poll

Pass an Idempotency-Key header on every run. A retried request carrying the same key returns the original job instead of billing a second time.

Then poll the job until it reaches a terminal state:

A terminal job carries status, output.output (the model's reply as a string), charged_credits and truncated. If truncated is true the run hit the balance-derived output cap and the reply is genuinely incomplete — parse what arrived rather than discarding it.

6. Stream it instead

/run-stream is the same call over SSE. Events are job, delta and done. Long briefs are worth streaming.

7. The output contract

The reply is a single JSON object — no code fence, no prose around it. Every key is always present; unused sections are "" or [], never null and never absent.

{
  "found": true,
  "confidence": "verified | recalled | uncertain",
  "not_found": {"reason": "", "similar_works": [{"title","author","note"}], "what_would_help": ""},
  "work": {
    "title": "", "title_source": "",
    "author": "", "author_source": "",
    "first_published": "", "first_published_source": "",
    "original_language": "", "original_language_source": "",
    "form": "", "form_source": "",
    "series": "", "series_source": "",
    "length_note": ""
  },
  "orientation": "", "premise": "", "argument": "",
  "structure": [{"part": "", "role": "", "note": ""}],
  "themes": [{"theme": "", "how_it_works": "", "where_it_lives": ""}],
  "context": {"historical": "", "biographical": "", "publication": ""},
  "reception": {"on_publication": "", "since": "", "contested": ""},
  "style": "",
  "difficulty": {"band": "accessible | moderate | demanding | specialist | ", "why": "", "time_note": ""},
  "for_you": {"read_it_if": [], "skip_it_if": [], "before_you_start": ""},
  "read_next": [{"title": "", "author": "", "relation": ""}],
  "quotations": [{"text": "", "attribution": "", "supports": ""}],
  "editions_note": "", "spoiler_note": "", "limits": "",
  "declined": [{"asked": "", "why": ""}]
}

The _source fields

Each of the five volatile facts is paired with a _source holding either the id of a record you sent (SRC-1) or the literal string model-knowledge. Those five — author, year, original language, form, series — are the facts a language model invents most readily and a reader cannot check. A non-empty value beside an empty _source is a defect in the reply.

found: false

When the work cannot be identified, found is false, not_found is filled in with real similar-titled works, and every other section is empty. A reply carrying both found: false and an orientation is self-contradicting, and the orientation is the half to distrust. Handle this branch explicitly: it is the one that protects your users from a confidently invented book.

difficulty.band

Normally one of the four bands, but legitimately an empty string when found is false — rating a book that could not be identified would be exactly the kind of confident filler this app avoids. Do not validate it as a closed enum.

Rate limits worth knowing

A brief is a starting point for reading, not a reference work and not a citable scholarly source. Treat the confirmed marks as meaning "checked against a retrieved snippet", which is a real claim but a modest one.