Skip to content

Bookmarks

Status: Live

Saving, unsaving, per-piece state and the Bookmarked list all ship in CSL-107.

A bookmark is a reader saving a piece to come back to. Tap the bookmark on a passage, a letter, an essay, a chapter or an article, and it appears in Bookmarked on their profile.

That is the whole feature. A bookmark carries nothing but the fact that it exists — there is no note, no folder, no ordering a reader can set, and nothing to edit. The only two things that can happen to one are being made and being removed.

What a reader can save

The three canonical content pieces: Passage, Writing (letter, essay or book chapter) and Article.

A bookmark belongs to the piece, not to the route the reader took to it. Saving a piece while reading it inside a journey saves that piece, so it appears once in Bookmarked. A writing or an article shows as saved wherever it is met again. A chapter's passage is met only in its chapter (Journey passages), and its bookmark opens it like any other. There is no separate bookmark for a journey chapter.

Whether a whole Journey can be bookmarked is an open question with design. It is not ruled out; it simply has no agreed behaviour yet, and Bookmarked shows only content pieces today.

What happens to a bookmark when content changes

The piece was… The bookmark
edited unaffected; the list shows the current title
unpublished stays in Bookmarked, marked unavailable, and cannot be opened
deleted goes with the piece

Unpublishing is reversible and often temporary, so the save survives it and the reader is told the piece cannot be opened right now. Deleting is not, and a bookmark with nothing to point at has nothing to show and nowhere to go, so it is removed rather than left as a dead row.

This is deliberately different from a Reflection, which survives its piece being deleted. A reflection is the reader's own writing and losing it would lose something they made; a bookmark is only a pointer at someone else's.

Saving is always safe to repeat

Saving the same piece twice is the same single save, and the second one does not move it to the top of the list. Unsaving something that is not saved is not an error. This is what lets the app hold a bookmark a guest tapped, sign them in, and replay it without checking anything first.

Endpoint map

What Endpoint
Save a piece PUT /api/account/bookmarks/{type}/{id}/
Unsave a piece DELETE /api/account/bookmarks/{type}/{id}/
Is this piece saved? GET /api/account/bookmarks/{type}/{id}/
Everything the reader has saved GET /api/account/bookmarks/

All four need a bearer token and none is ever cached — they live under /api/account/ for exactly that reason. The public content payloads carry no bookmarked flag; overlay it from these. That is what keeps those reads cacheable for every reader at once.

Addressing a piece

{type} is passage, writing or article — the same three values reflections use, the same words a journey chapter's content block already carries, and the same word that names the piece's detail endpoint. {id} is its row id.

There is no chapter or journey type. A journey chapter is a piece plus editorial framing, so bookmarking inside a journey targets the piece underneath: take chapter.content.type and chapter.content.id from the journey detail payload.

Note type and kind are different, and both appear in payloads. type is for routing; kind is the label — passage, letter, essay, chapter or article. A letter is type: writing, kind: letter.

Save: PUT /api/account/bookmarks/{type}/{id}/

No request body. 201 the first time, 200 on every repeat.

A repeat returns the original record with its original bookmarked_at. It does not move the piece back to the top of Bookmarked, which is what re-saving would otherwise look like.

Status When
201 Saved.
200 Already saved. The response is that record, unchanged.
404 No such piece, {type} is not one of the three, or the piece is an unpublished draft the reader has not already saved.
401 No valid bearer token.

Unsave: DELETE /api/account/bookmarks/{type}/{id}/

204 whether or not anything was saved. The reader asked for this piece to be unsaved and it is unsaved, so there is nothing to handle differently. 404 and 401 as above.

Is it saved: GET /api/account/bookmarks/{type}/{id}/

{"bookmarked": true}

The authoritative state for one piece — what the reading screen asks to draw its icon. 404 and 401 as above.

The list: GET /api/account/bookmarks/

The Profile Bookmarked list: everything the reader has saved, newest first, limit/offset paginated (API Conventions).

Ordering is stable to the row, so paging cannot repeat or skip a piece saved in the same second as another. It is a flat list — unlike Reflected, Bookmarked has no date grouping, so bookmarked_at is there for ordering rather than for display.

{
  "count": 12,
  "next": "http://…/api/account/bookmarks/?limit=10&offset=10",
  "previous": null,
  "results": [
    {
      "id": 91,
      "bookmarked_at": "2026-07-21T09:12:44Z",
      "is_available": true,
      "content": {
        "type": "passage",
        "kind": "passage",
        "id": 42,
        "slug": "the-more-we-get-what-we-now-call-ourselves",
        "title": "The more we get what we now call ourselves",
        "supporting_line": "Mere Christianity",
        "image": null,
        "read_time_minutes": null,
        "theme_id": 4
      }
    }
  ]
}

content is the shared content card, plus type — the same card every other mixed-kind list serves, so one parser handles them all.

The Bookmarked row draws three of its fields: image, kind and title. supporting_line, read_time_minutes and theme_id come along because the card is shared; ignore them here.

Two are worth calling out for passages specifically. A passage has no image of its own, and a read time under a two-line quote is noise, so both come back null. A passage therefore arrives with no thumbnail — what the row should show in its place is an open question with design, so treat a null image as a case to handle rather than an error.

is_available is false once the piece is unpublished. The bookmark stays in the list — a save silently vanishing would look to a reader like it had been lost — so render the row and disable opening it. A piece deleted outright takes its bookmark with it and simply is not in the list.

Overlaying state on cached content

There is no bulk state endpoint. To show bookmark state on a page of public cards, hold the reader's list and match on content.type + content.id. Fine while a reader's library is small; if walking every page becomes the wrong shape, that is the point to ask for a bulk check.