Skip to content

Passages API guide

Status: Live

Implemented and deployed to dev (CSL-141). See API Conventions for auth, pagination, and error handling.

Orientation for the passage endpoints. Swagger (/api/docs/, /api/redoc/) and the Linear ticket hold the exact request and response schemas. This guide covers what each endpoint is and how it behaves, not every field. For the concept, see the Passages overview.

Endpoints

  • GET /api/passages/ returns the full published set, paginated (LimitOffset, the repo default).
  • GET /api/passages/todays-featured/ returns the Home hero pool.
  • GET /api/passages/{id}/ returns the reading-screen detail for one passage.

All are public, app-key-gated (X-API-Key), user-agnostic, and cacheable (Cache-Control: public).

Each passage can name a primary theme, served as theme_id. The client resolves theme_id against the cached themes set.

The hero shows one quote, but the endpoint returns a pool of up to 10. The client caches the pool and shows a different quote at random per session, with no extra request per rotation.

Behaviour that shapes the contract:

  • Order is deterministic, not random. The published set has one fixed order, newest-published first, so responses are stable and cache cleanly. The randomness the user sees is the client picking one of the 10 it holds.
  • ?exclude= is a cursor, not a filter. The client keeps only its current window (up to 10) and sends those ids as ?exclude=1,2,3. Because the order is fixed, the server advances past the furthest excluded passage and returns the next window. A client holding items 11 to 20 that sends ?exclude=11..20 gets 21 to 30, not 1 to 10. The client does not accumulate every id it has seen. The last window is enough.
  • The pool never returns empty and wraps at the end. Once the cursor passes the last passage, it wraps to the start. While at least one passage exists, the hero is never empty, and small sets rotate.

Reading detail: GET /api/passages/{id}/

The single-passage payload behind the passage reading screen. It returns 404 when the id is not a published passage, the same eligibility as the list: live, with body text. The payload is public and cacheable (Cache-Control: public + ETag).

Beyond the card fields, it adds the full reading layer:

  • interpretation is a structured object with three fields. heading is the passage title. subtitle is fixed at "Seeing this through Lewis's lens", the same for every piece. body is the "What This Means" text as Markdown. body can be empty, because interpretations are authored later with AI help, so a live passage can ship without one.
  • reflection_prompt is the editorial reflect question. It can be empty.
  • related_quotes holds up to 4 related quotes in the editor's stored order. Each is a lean card of id, slug, and quote only. The rail renders just the quote, and a tap fetches that passage's full detail by id, so the card omits source, audio, and theme_id. This rail is display-only. The CMS curates its membership and order, and the runtime never ranks or computes it. The server drops non-published picks and the passage itself, and returns [] when none remain, so the FE omits the section.
  • Two computed onward rails power the footer carousels (CSL-207). more_on_topic returns Lewis's own Writings that share the passage's source book or theme. related_content returns contributor Articles matched by shared tags, with a theme fallback when the passage has no tags. Each card's supporting_line is the "From {author}" byline. Both are content cards, live-only, self-excluded, and capped at 10, and each is [] when nothing matches. Both rails are computed, unlike the curated related_quotes rail above.

Card notes

  • The list and hero cards carry no top-level kind. Both responses return passages only, so each card omits the type discriminator. The detail payload does carry kind: "passage", because it follows the shared content-detail contract, where kind names the piece type across content kinds.
  • source is polymorphic, so it does carry a kind. A passage's origin is a Book (kind: "book", whose cover is an image rendition set) or a Writing (kind is letter, essay, or chapter). It is null when the passage cites neither.
  • source is fuller on the detail payload than in the list. The reading screen's "Discover About the Book" block shows the book's publication_year, synopsis, and buy_cta, so the detail returns the full book card, the same shape the writing detail returns. The list and Home hero render only "from {title}" with a small cover chip, so their source stays lean (kind, id, slug, title, cover) and omits those three fields. A Writing source is a bare reference (kind, id, slug, title) on both, because there is no book block to fill.
  • Eligibility. Only live passages with body text appear.

See Swagger for the precise field list, pagination envelope, and status codes.