# TorahLM Reference

The long-form companion to [torahlm.org/skill.md](https://torahlm.org/skill.md).
`skill.md` is the quick reference — the endpoints, URL formats and aliases an
agent needs to read a text. This file holds the details that would only be
noise there: response shapes, field-by-field payloads, and the rules the other
representations follow.

Everything here is optional reading. Markdown over a simple URL remains the
default contract, and nothing below is needed to use it.

## Three representations of one API

Every API endpoint has one path and three renderings of it. Put a prefix in
front of the path to choose:

| Prefix | For | Content type |
|--------|-----|--------------|
| *(none)* | agents — the default | `text/markdown` |
| `/json/` | programs | `application/json` |
| `/web/` | people | `text/html` |

So `/Genesis/r/1:1`, `/json/Genesis/r/1:1` and `/web/Genesis/r/1:1` are the same
resource. The same holds for `/search?q=shofar`, `/links/Genesis.1.1`,
`/library`, `/halachipedia/{page}`, `/shiurim/{podcastId}` and the rest.

Two things have no `/json` twin:

- **Raw corpus files** under `/download/...` — full Markdown downloads, often
  several MB; request them directly. (`/json/download/tree.json` is available:
  it's the index of downloadable files, not a file.)
- **Human surfaces** — the home page, the notebook builder, and the short links
  out to NotebookLM/GitHub are for people, and answer with a 404 error object.

(`POST /mcp` isn't in this scheme at all: it's a JSON-RPC 2.0 protocol endpoint
for agents, not a resource with representations. Call it directly.)

## The JSON envelope

Every success is:

```json
{ "kind": "…", "apiVersion": "0.1-beta", "url": "https://torahlm.org/…", "data": { } }
```

`url` is the `torahlm.org` URL to cite: always absolute, always with your
`?src=` tag removed, and with the book's real name after alias resolution
(`/json/בראשית/r/1:1` reports `https://torahlm.org/Genesis/r/1:1`).

It identifies the resource's canonical citation, which is not always a byte-for-byte
reproduction URL. Book text and Halachipedia normally omit presentation filters
such as `lang`; a text byte-window retains non-default `lang` when language
affected which segments fit. Binding `limit`, `max_bytes`, `offset`, and other
controls needed to distinguish a partial body are retained, while no-op size
controls are dropped. `/links` also retains `category`, `text=0`, and
non-default `lang`, because its JSON twin contains the rendered Markdown.
Search differs in one
detail: it rebuilds its `url` from the normalized parameters, so it always
carries an explicit `limit` and may not match what you sent. Everything else
echoes the path you requested, query included.

Either way, key your cache on the URL you requested rather than on this one.

`kind` says what `data` holds:

| `kind` | `data` |
|--------|--------|
| `text` | a passage — see *Book text* below |
| `book` | a book overview (bare `/json/{Book}`): `book`, `heTitle`, `toc`, versions, word counts |
| `search` | typed result sections — see *Search* below |
| `markdown` | `{ "markdown": "…" }` — the endpoint's Markdown body |
| `plaintext` | `{ "text": "…" }` — a plain-text surface (`robots.txt`, `llms.txt`) |
| `json` | the endpoint's own JSON payload, e.g. `/json/download/tree.json` |

Every failure is:

```json
{ "kind": "error", "apiVersion": "0.1-beta", "status": 404, "message": "…", "usage": "…" }
```

(`usage` only where there's a usage line to give.) A route that emits JSON of
its own is still wrapped in this envelope, so you can always read `kind`. Where
an endpoint has a `/json/` twin its status matches the Markdown call's.
Redirects to a TorahLM path keep the prefix, so following `Location` leaves you
on a `/json/` URL.

### Versioning

`apiVersion` is on every JSON response, errors included. It is **`0.1-beta`**
today: the JSON representation is new, and a `0.x` version means a change to
these shapes can be breaking. Markdown is the stable surface; if you need a
contract that won't move under you, read Markdown.

Two kinds of change to expect:

- **Additive** — a new field, or an endpoint whose `kind` graduates from
  `markdown` to a typed payload (book text and `search` are typed today; the
  rest carry their Markdown in `data.markdown`). A route's URL and its envelope
  don't change when this happens, so branch on `kind` rather than assuming it,
  and ignore fields you don't recognize.
- **Breaking** — a renamed or restructured field. This bumps `apiVersion`.

If you pin, pin on the major-minor (`0.1`) and treat an unfamiliar version as a
reason to re-read this page rather than to fail hard.

## Book text

`kind: "text"`. `segments` is one entry per verse/segment, in order, each with
its own `ref` and shareable `url`:

```json
{ "kind": "text", "apiVersion": "0.1-beta", "url": "https://torahlm.org/Genesis/r/5",
  "data": {
    "book": "Genesis", "ref": "5", "sectionPath": null,
    "sectionNames": ["Chapter", "Verse"],
    "versions": { "he": ["Miqra according to the Masorah"],
                  "en": ["Metsudah Chumash…", "The Koren Jerusalem Bible", "…"] },
    "segments": [
      { "ref": "5:1", "url": "https://torahapp.org/share/book/Genesis/r/5:1",
        "he": ["זֶ֣ה סֵ֔פֶר תּוֹלְדֹ֖ת אָדָ֑ם…"],
        "en": ["This is the book of the generations of man…", "…", "…"] }
    ] } }
```

- A segment's `he` / `en` are **index-aligned** with the `versions` list of the
  same language: `segment.en[i]` is the translation named by `versions.en[i]`.
  An empty string means that version has no text for that segment.
- `?lang=he` or `?lang=en` drops the other language from `versions` and from
  every segment. The default (`?lang=bi`) returns both.
- `sectionPath` is `null` for ordinary books, and the section path for books
  addressed that way (`/Siddur Ashkenaz/s/Weekday,_Shacharit,…/r/1`).
- `sectionNames` names the levels of the ref, outermost first — `["Chapter",
  "Verse"]`, `["Daf", "Line"]`, `["Siman", "Seif"]`.
- `ref` is what was actually served. It echoes the ref you asked for; where you
  asked for none it's the ref of the section that answered, which for a
  single-section work is `"all"`.
- Requesting a whole section returns all its segments; requesting one ref
  returns that segment, plus any sub-segments beneath it (a commentary's several
  comments on one verse).
- `?limit=N` caps how many segments come back, `?max_bytes=N` greedily packs
  complete rendered segments within a soft UTF-8 content budget, `?offset=N`
  starts at a zero-based segment position, and `?footnotes=0` drops the
  footnote block from each one. None has an HTTP default — a request that sends
  none gets everything, as before. When a window starts after zero or withholds more, a
  `truncated` object appears alongside `sectionNames`:
  `"truncated": { "shown": 2, "total": 4, "offset": 0, "nextOffset": 2 }`.
  `nextOffset` exists only when more remains; a final nonzero-offset window
  still reports its range without that field. An offset outside the result set
  is a 400. Offsets are stable only within one corpus generation.
- Byte selection is measured on the Markdown rendering, so Markdown and JSON
  select the same segment refs even though JSON has different serialization
  overhead and may contain more version bodies. Fixed headers and continuation
  furniture are outside the content budget.
- A segment larger than `max_bytes` is never split or refused. It is returned
  complete; Markdown carries a size notice and JSON adds `oversized` with
  `renderedBytes`, `approximateTokens`, and the same message.

A bare `/json/{Book}` returns `kind: "book"` instead: the book's overview —
Hebrew title, description, structure, the sections that exist, version names
with licenses, and word counts. (A book whose overview hasn't been generated
falls back to `kind: "text"` for its first section.)

## MCP response windows

MCP materializes ordinary HTTP parameters before re-entering the same worker:
`get_text` defaults to `max_bytes=32768`; `get_links` defaults to that budget
plus `limit=10`; and `lookup` applies whichever defaults match its destination.
32 KiB is about 10,900 tokens in measured TorahLM Markdown (~330 tokens/KB).
An explicit `maxBytes` replaces the default, and `offset` continues the same
ordered result. These are soft content budgets: complete segments and rendered
link blocks are indivisible, and an oversized first item is delivered whole.

Links are paged in their flattened grouped display order. A window beginning
inside a category repeats its `## Category` header, while `Found N` and
`Categories:` continue to describe the full approved result set. Link offsets
are stable only within the current Sefaria cache snapshot. `/json/links/...`
still uses `kind: "markdown"`, so its continuation remains prose inside
`data.markdown` rather than a native `truncated` object.

Whole Halachipedia articles are not byte-windowed and References stay on by
default. They can be large; use the existing `section` argument when a focused
heading is sufficient.

## Search

`kind: "search"`. `query` echoes the parameters actually applied after
normalization, and `data` has one key per corpus — `books`, `titles`,
`halachipedia`, `series`, `shiurim` — each exactly one of:

- `null` — you didn't select this corpus, so it wasn't searched
- results — an array (`books` is `{ total, results }`); an **empty** array means
  searched, nothing matched
- `{ "error": "…" }` — you selected it, but it couldn't answer. Not the same as
  no matches: never report it as an empty result. Two causes, and the message
  distinguishes them — an outage (index paused or unavailable), which is worth
  retrying later, or a bad `book=` scope, which needs a corrected request.

The parameters themselves (`q`, `type`, `limit`, `book`, `teacher`, `parsha`,
`daf`, `category`) are documented in
[skill.md](https://torahlm.org/skill.md) and work identically in all three
representations.

## Identifying your client

Please add `?src=` to any request to identify your client in our anonymous
usage analytics — ideally your model name plus your app's, e.g.
`?src=claude-fable-5+myapp`. It never affects the response, and it's stripped
from the canonical `url` the JSON envelope reports back.
