Books API

Create and update book components with ordered pages and an optional cover through the Components API in Playtest Parlor.

A book is a game component with an ordered list of pages and an optional cover image. On the table it renders as a 3D book players can read and turn page by page. See Books for the player-facing behavior.

Books are created and updated through the Components API using kind: "book", the same game-scoped endpoints as every other component. Like all components, a pushed book lives in your app's import revision for the game: it shows up in the game's entity browser, game editors place it onto tables from there, and re-exporting updates it in place by externalKey without creating duplicates.

Authentication

All requests must include an OAuth2 bearer token, the same as every other endpoint:

Authorization: Bearer pp_your_token_here

The token's user must have edit access to the game. See Authentication for the full flow.

Page keys

Pages are supplied as R2 object keys (the key value from an Assets upload), not full URLs. Each key is validated and must resolve to an object under the configured asset host. A key that points anywhere else is rejected with VALIDATION_ERROR. Upload page images first with the Assets API, then pass their keys here.

Create or Update a Book

Push a book like any other component. The first key in pages is page 1 (shown when the book is closed, unless a separate cover is set). A book may have at most 200 pages.

Request:

POST /api/v1/games/:gameId/components
Authorization: Bearer pp_your_token
Content-Type: application/json

{
  "kind": "book",
  "name": "Rulebook",
  "externalKey": "rulebook",
  "widthMm": 148,
  "heightMm": 210,
  "pages": [
    "uploads/sh/sha256-page1...",
    "uploads/sh/sha256-page2...",
    "uploads/sh/sha256-page3..."
  ],
  "cover": "uploads/sh/sha256-cover..."
}

Request Body:

FieldTypeRequiredDescription
kindstringyesMust be "book"
namestringyesDisplay name (max 256 characters)
externalKeystringyesUnique identifier for this book within the game, used for upsert deduplication
pagesarrayyes on createNon-empty array of R2 object keys in reading order (maximum 200). May be omitted on an update to leave the existing pages unchanged.
coverstringnoR2 object key for a dedicated cover image shown on the closed book. When omitted, page 1 acts as the cover.
widthMmnumbernoPage width in millimeters (default 148, A5 portrait)
heightMmnumbernoPage height in millimeters (default 210, A5 portrait)

The component-asset fields (assets, parentExternalKey, sides, shape, count, payload, bleedMode) do not apply to books and are rejected with VALIDATION_ERROR. A book's content is its pages and cover.

Books also work in the batch endpoint: include a book object in the components array alongside your other components.

Response: 201 Created

{
  "id": "j57a..."
}

Errors:

  • VALIDATION_ERROR: pages is missing on create, empty, over the 200-page limit, or contains a key that does not resolve under the asset host; a disallowed field was supplied; or the externalKey is already used by a non-book component
  • GAME_NOT_FOUND: The game does not exist
  • PERMISSION_DENIED: You do not have permission to edit this game
  • UNAUTHORIZED: Your token is invalid or missing

Update semantics

Re-pushing the same externalKey updates the book in place:

  • pages present: the push is a full content snapshot. The page list is replaced wholesale, and the cover is set to cover or cleared when cover is omitted. Send the complete desired state each time, the same way you re-export other components.
  • pages omitted: a metadata-only update. name, widthMm, and heightMm are updated; the existing pages are kept. If cover is provided, just the cover is updated.

Reading books back

GET /api/v1/games/:gameId/components lists your app's components, and books come back with kind: "book". The ordered page URLs, page count, and cover URL are in each book's payload (bookPageUrls, pageCount, bookCoverUrl).

Placement and gameplay state

Pushing a book puts it in the game's component library; it does not place it on a table. Game editors place books from the entity browser, the same as decks and tiles. The spread a placed book is currently turned to is gameplay state owned by the session: players turn pages at the table, and the position stays in sync for everyone. Re-pushing content does not touch books already placed on tables; place the updated book again to use the new content.

Units

A book's page dimensions are stored in millimeters, the canonical storage format in Playtest Parlor, the same as every other piece.

Typical Workflow

// 1. Upload each page image via the Assets API (presign -> PUT -> upload-complete).
//    Keep the returned `key` for each page.

// 2. Push the book component from the ordered page keys.
const createRes = await fetch(
  `https://playtestparlor.com/api/v1/games/${gameId}/components`,
  {
    method: "POST",
    headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" },
    body: JSON.stringify({
      kind: "book",
      name: "Rulebook",
      externalKey: "rulebook",
      pages: pageKeys,
      cover: coverKey
    })
  }
);
const { id: componentId } = await createRes.json();

// 3. Later, re-export the full desired state to update it in place.
await fetch(
  `https://playtestparlor.com/api/v1/games/${gameId}/components`,
  {
    method: "POST",
    headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" },
    body: JSON.stringify({
      kind: "book",
      name: "Rulebook",
      externalKey: "rulebook",
      pages: [...pageKeys, newPageKey],
      cover: newCoverKey
    })
  }
);