Every endpoint, with real request/response examples. All examples were captured from a
running instance loaded with the 13 bundled public-domain English translations plus the
original-language texts — the SBL Greek New Testament (SBLGNT) and the Hebrew Old Testament
(OSHB, right-to-left).
- Base URL:
http://<host>:<port>(defaulthttp://localhost:8000) - Versioning: data endpoints live under
/v1. That prefix is a stability contract. - Machine-readable schema: the full OpenAPI spec is committed at
docs/openapi.json(also served live at/openapi.json), versioned with the release and CI-checked against the code — build clients against it with confidence. - Responses: JSON (
application/json). Verse text is returned exactly as stored — Unicode, editorial brackets ([is]), and punctuation are preserved untouched. The one exception is a translation's images, which come back as their own bytes (assets). - A study Bible you own (v8): the notes'
label,title,text_format,passagesandimagefields, a translation's images and documents, and a second topical source beside Nave's are served only once an operator bakes their own data in under the gitignoreddata/private/(emb-ingest). The published image carries none of it, so there they answer empty: everynote_countanddocument_countis0, notes and documents are200with an empty list, an image name is a404, and/v1/topicslists Nave's alone. Examples for a translation's notes, images and documents come from such a private build (NET,EMB), with the book's own words left out (…).
- Conventions
- Errors
GET /v1/verses/{ref}GET /v1/chapters/{book}/{chapter}GET /v1/searchGET /v1/semantic-searchGET /v1/cross-references/{ref}GET /v1/placesGET /v1/places/{id}GET /v1/places/{id}/versesGET /v1/verses/{ref}/placesGET /v1/journeysGET /v1/journeys/{id}GET /v1/places/{id}/journeysGET /v1/translations/{translation}/notes/{book}/{chapter}GET /v1/translations/{translation}/assets/{name}GET /v1/translations/{translation}/documentsGET /v1/translations/{translation}/documents/{slug}GET /v1/notes/searchGET /v1/topicsGET /v1/topics/{id}GET /v1/topics/{id}/versesGET /v1/verses/{ref}/topicsGET /v1/strongsGET /v1/strongs/{id}GET /v1/strongs/{id}/versesGET /v1/verses/{ref}/wordsGET /v1/randomGET /v1/booksGET /v1/translationsGET /healthz- Reference grammar
Translation IDs are case-insensitive on input (kjv, KJV, Kjv all work) and
returned upper-cased. When a translation parameter is omitted it defaults to
CONCORD_DEFAULT_TRANSLATION (default KJV; see the README → Configuration).
Books in filters and paths resolve from a USFM id (JHN) or an alias (john, jn,
jhn). Aliases are normalized (lowercased, punctuation stripped, leading ordinal folded:
I John → 1 John).
Caching. Scripture is immutable, so every endpoint except /random and /healthz
sends a strong ETag and Cache-Control: public, max-age=31536000, immutable, and honors
If-None-Match with a 304 Not Modified. /random is explicitly not cached (see its
section). /healthz carries no caching headers.
# ETag round-trip
$ curl -sD- -o /dev/null 'localhost:8000/v1/verses/John%203:16?translations=kjv' | grep -i etag
etag: "0da1ee58348725b2badc17a751303b8e"
$ curl -s -o /dev/null -w '%{http_code}\n' \
-H 'If-None-Match: "0da1ee58348725b2badc17a751303b8e"' \
'localhost:8000/v1/verses/John%203:16?translations=kjv'
304Every error uses one envelope:
{ "error": { "code": "unparseable_reference", "message": "...", "detail": {} } }| Code | Status | When |
|---|---|---|
unparseable_reference |
400 | A reference doesn't match the grammar (e.g. foo bar). |
unknown_book |
404 (path) / 400 (filter) | An unrecognized book. 404 when it's the resource in a path (/verses/Hezekiah 1:1); 400 when it's a query-param filter (/search?book=hezekiah). |
unknown_translation |
404 | A requested translation isn't loaded. |
no_verses_found |
404 | A well-formed reference matches no verse in any requested translation (e.g. Genesis 999:1). |
no_match |
404 | /random filters match nothing (e.g. book=GEN&testament=NT). |
unknown_place |
404 | A place id in a path resolves to no place (/places/nope). detail.place_id echoes it. |
unknown_journey |
404 | A journey id in a path resolves to no journey (/journeys/nope). detail.journey_id echoes it. |
unknown_topic |
404 | A topic id in a path resolves to no topic (/topics/nope). detail.topic_id echoes it. |
unknown_strongs |
404 | A Strong's number in a path resolves to no lexicon entry (/strongs/G99999). detail.strongs_id echoes it. |
unknown_asset |
404 | A loaded translation has no image by that name (/translations/EMB/assets/nope.jpg). detail echoes translation and name. |
unknown_document |
404 | A loaded translation has no document by that slug (/translations/EMB/documents/nope). detail echoes translation and slug. |
unknown_type |
400 | A /places?type= filter value isn't a known place type; detail.available lists the valid types. |
unknown_kind |
400 | A /translations/{translation}/documents?kind= value isn't one of front-matter / reading-plan / book-introduction / about; detail.available lists them. |
unknown_source |
400 | A /topics?source= value isn't a loaded topical source; detail.available lists them (as /topics's sources does). |
unknown_status |
400 | A /places?status= filter value isn't one of identified / disputed / unknown / symbolic / multiple. |
invalid_search_query |
400 | Malformed FTS5 syntax; the SQLite message is in detail.fts5_error. |
invalid_parameter |
422 | A query/path parameter fails validation (bad format, limit out of range, min_votes < 0, non-integer chapter). |
$ curl -s 'localhost:8000/v1/verses/foo%20bar'
{"error":{"code":"unparseable_reference","message":"'foo bar' is missing a chapter/verse — a reference needs at least a chapter number","detail":{}}}Fetch the verses named by {ref} across one or more translations.
| Param | In | Type | Default | Notes |
|---|---|---|---|---|
ref |
path | string | — | A reference per the grammar (URL-encode spaces). |
translations |
query | CSV | default translation | e.g. kjv,web,ylt. |
format |
query | parallel | grouped |
parallel |
Response shape. |
Parallel (default) — one object per verse, each translation's text nested under it; a
translation that omits a verse (a critical-text gap like Matthew 17:21) shows null:
$ curl -s 'localhost:8000/v1/verses/John%203:16?translations=kjv,web'{
"reference": "John 3:16",
"translations": ["KJV", "WEB"],
"verses": [
{
"book": "JHN", "chapter": 3, "verse": 16,
"reference": "John 3:16",
"text": {
"KJV": "For God so loved the world, that he gave his only begotten Son, that whosoever believeth in him should not perish, but have everlasting life.",
"WEB": "For God so loved the world, that he gave his one and only Son, that whoever believes in him should not perish, but have eternal life."
}
}
]
}Grouped (?format=grouped) — verses bucketed by translation:
{
"reference": "John 3:16-17",
"translations": {
"KJV": [
{ "book": "JHN", "chapter": 3, "verse": 16, "text": "For God so loved the world, ..." },
{ "book": "JHN", "chapter": 3, "verse": 17, "text": "For God sent not his Son ..." }
]
}
}Errors: 400 unparseable_reference · 404 unknown_book (e.g. Hezekiah 1:1) ·
404 no_verses_found (well-formed but no such verse, e.g. Genesis 999:1) ·
404 unknown_translation · 422 invalid_parameter (bad format). Caching: immutable.
Note: the parser does not bounds-check chapter/verse numbers — John 3:999 parses
fine; you get 404 no_verses_found only because no translation has that verse.
A whole chapter, multi-translation aware. {book} is a USFM id or alias; {chapter} is a
positive integer. ?translations= and ?format= work exactly as for /verses.
$ curl -s 'localhost:8000/v1/chapters/john/1?translations=kjv'{
"reference": "John 1",
"translations": ["KJV"],
"verses": [
{ "book": "JHN", "chapter": 1, "verse": 1, "reference": "John 1:1",
"text": "In the beginning was the Word, and the Word was with God, and the Word was God." },
...
]
}Errors: 404 unknown_book · 422 invalid_parameter (chapter < 1 or non-integer) ·
404 no_verses_found (no such chapter). Caching: immutable.
Full-text search, backed by SQLite FTS5. Searches one translation by default; add
?translations= to search several at once, deduped by canonical verse (see
Multi-translation search below).
| Param | Type | Default | Notes |
|---|---|---|---|
q |
string | — (required) | FTS5 query; see syntax below. |
translation |
string | default translation | Single translation (single-translation mode). |
translations |
string | — | Multi-translation mode. Comma-separated ids (KJV,WEB,ASV), or * for all loaded. When present it takes precedence over translation; absent/blank keeps single-translation mode. |
book |
string | — | Optional filter; USFM id or alias. |
limit |
int | 20 |
1–100. |
offset |
int | 0 |
≥ 0. |
$ curl -s 'localhost:8000/v1/search?q=lamp%20unto%20my%20feet&translation=KJV&limit=2'{
"query": "lamp unto my feet", "translation": "KJV", "book": null,
"limit": 2, "offset": 0, "total": 1,
"hits": [
{ "book": "PSA", "chapter": 119, "verse": 105, "reference": "Psalms 119:105",
"snippet": "NUN. Thy word [is] a <mark>lamp</mark> <mark>unto</mark> <mark>my</mark> <mark>feet</mark>, and a light <mark>unto</mark> <mark>my</mark> path." }
]
}Matched terms are wrapped in <mark>…</mark>. Results are relevance-ranked (FTS5 rank)
with a canonical tiebreak, so limit/offset pages don't overlap. total is the full
match count, independent of the page.
Pass ?translations= (a comma-separated list, or * for all loaded) to search several
translations at once. Results are deduped by canonical verse: one hit per verse that matched in
at least one of the requested translations, ranked by its best (max) relevance across them, with
the same canonical tiebreak. total counts distinct matching verses, not (verse, translation)
pairs.
Each hit gains a matches map — { "<TRANSLATION>": "<marked snippet>", … } — carrying every
translation that matched and its snippet. The response also echoes the searched set as
translations.
$ curl -s 'localhost:8000/v1/search?q=lovingkindness&translations=KJV,ASV&limit=1'{
"query": "lovingkindness", "translation": "KJV", "book": null,
"limit": 1, "offset": 0, "total": 1,
"hits": [
{ "book": "PSA", "chapter": 63, "verse": 3, "reference": "Psalms 63:3",
"snippet": "Because thy <mark>lovingkindness</mark> [is] better than life, ...",
"matches": {
"KJV": "Because thy <mark>lovingkindness</mark> [is] better than life, ...",
"ASV": "Because thy <mark>lovingkindness</mark> is better than life, ..."
}
}
],
"translations": ["KJV", "ASV"]
}A few shape notes (the rationale is recorded in ADR-0003):
- The
matchesmap is authoritative — it's the full per-translation detail. - The flat top-level
snippeton each hit echoes that hit's top-ranked translation's snippet, so a client that reads onlysnippetstill gets something sensible (it may name a different translation per hit). - The result-level
translationis the primary — the first id you requested (sotranslationstays a single non-null id in both modes). The searched set is intranslations.
Additive and backward-compatible. This is a purely additive widening: with translations absent
the response is byte-for-byte the single-translation shape above — no matches, no
translations field. Existing single-translation clients are unaffected.
FTS5 query syntax (passed through to SQLite):
| Form | Example | Meaning |
|---|---|---|
| terms | lamp feet |
implicit AND — both must appear |
| phrase | "lamp unto my feet" |
exact adjacent sequence |
| prefix | lov* |
love, loved, loveth, … |
| boolean | god OR lord, god NOT wrath |
explicit operators (uppercase), parentheses |
| near | NEAR(faith hope, 5) |
within N tokens |
Empty results return 200 with "total": 0 and "hits": [] — never a 404.
Errors: 422 invalid_parameter (missing/empty q, limit out of 1–100) ·
400 invalid_search_query (malformed FTS5, e.g. an unbalanced quote — detail.fts5_error
carries the SQLite message) · 404 unknown_translation (an unknown id in translation or any
id in translations) · 400 unknown_book (filter). Caching: immutable.
Meaning-based search: find verses by idea, not keyword. The query is embedded with a local model and compared against precomputed verse vectors by cosine similarity; the closest verses come back ranked. Runs fully offline — the model is baked into the image.
Search runs over one embedded translation, the World English Bible (WEB), in meaning-space.
The matches are verse references, so ?translation= controls which translation's text is
returned without changing the ranking (see "search in WEB, read in any translation" below).
| Param | Type | Default | Notes |
|---|---|---|---|
q |
string | — (required) | Natural-language query, e.g. verses about anxiety. |
limit |
int | 20 |
1–100. Number of results. |
translation |
string | WEB |
Which translation's text to return. Search always runs in WEB space. |
min_score |
float | — | Optional cosine floor in [-1, 1]; drops weaker matches. |
include_text |
bool | true |
When false, results carry refs + scores and text is null. |
$ curl -s 'localhost:8000/v1/semantic-search?q=do+not+be+anxious&limit=3'{
"query": "do not be anxious", "translation": "WEB", "count": 3,
"results": [
{ "book": "DEU", "chapter": 1, "verse": 29, "reference": "Deuteronomy 1:29", "score": 0.915,
"text": "Then I said to you, “Don’t dread, neither be afraid of them." },
{ "book": "1TH", "chapter": 5, "verse": 20, "reference": "1 Thessalonians 5:20", "score": 0.8945,
"text": "Don’t despise prophesies." },
{ "book": "JOB", "chapter": 6, "verse": 21, "reference": "Job 6:21", "score": 0.8895,
"text": "For now you are nothing. You see a terror, and are afraid." }
]
}score is cosine similarity in [-1, 1] (higher is closer), rounded to 4 places; results are
ranked descending.
Search in WEB, read in any translation. The matched references are hydrated in the
requested translation. A verse absent there (a versification gap) comes back with
text: null — the match still ranks; only its text in that translation is missing. Searching
the good shepherd matches John 10 in WEB space and renders it in the KJV:
$ curl -s 'localhost:8000/v1/semantic-search?q=the+good+shepherd&translation=KJV&limit=2'{
"query": "the good shepherd", "translation": "KJV", "count": 2,
"results": [
{ "book": "JHN", "chapter": 10, "verse": 11, "reference": "John 10:11", "score": 0.9419,
"text": "I am the good shepherd: the good shepherd giveth his life for the sheep." },
{ "book": "JHN", "chapter": 10, "verse": 14, "reference": "John 10:14", "score": 0.917,
"text": "I am the good shepherd, and know my [sheep], and am known of mine." }
]
}Empty results return 200 with "count": 0 and "results": [] — never a 404.
Errors: 422 invalid_parameter (missing/empty q, limit out of 1–100, min_score
outside [-1, 1]) · 404 unknown_translation. Caching: immutable (body-hash ETag, like
/v1/search).
Cross-references whose source falls within {ref}, ordered by community votes
(descending) with a canonical tiebreak.
| Param | Type | Default | Notes |
|---|---|---|---|
ref |
path | — | A reference per the grammar. |
include_text |
bool | false |
Hydrate each target's text. |
translation |
string | default translation | Only consulted when include_text=true. |
min_votes |
int | 0 |
≥ 0. Filters weak/disputed links. |
limit |
int | 20 |
1–100. |
offset |
int | 0 |
≥ 0. |
$ curl -s 'localhost:8000/v1/cross-references/John%203:16?include_text=true&translation=KJV&limit=2'{
"reference": "John 3:16", "translation": "KJV", "min_votes": 0,
"limit": 2, "offset": 0, "total": 23,
"cross_references": [
{
"from": { "book": "JHN", "chapter": 3, "verse": 16, "reference": "John 3:16" },
"to": { "book": "ROM", "chapter": 5, "verse_start": 8, "verse_end": null, "reference": "Romans 5:8" },
"votes": 968,
"text": "But God commendeth his love toward us, in that, while we were yet sinners, Christ died for us."
},
{
"from": { "book": "JHN", "chapter": 3, "verse": 16, "reference": "John 3:16" },
"to": { "book": "1JN", "chapter": 4, "verse_start": 9, "verse_end": 10, "reference": "1 John 4:9-10" },
"votes": 684,
"text": "In this was manifested the love of God toward us, ..."
}
]
}When include_text=false (the default), translation is null and each entry's text is
null. When include_text=true, text is the target's start verse in the chosen
translation, or null if that verse is missing there.
Target ranges. A target can span verses (verse_end set, as in 1 John 4:9-10). The
dataset has a few hundred targets whose range crosses a chapter or book boundary; the schema
stores a single to_chapter, so those 655 of 344,799 are clamped to their start verse
(verse_end: null) — the cross-reference is preserved, pointed at the correct first verse.
Empty results return 200 with "total": 0. A source verse that exists but has no
cross-references is not a 404; only an out-of-range source is.
Errors: 400 unparseable_reference · 404 unknown_book · 404 no_verses_found
(out-of-range source) · 404 unknown_translation (with include_text=true) ·
422 invalid_parameter (min_votes < 0, limit out of range). Caching: immutable.
Browse and filter the geography dataset (1,340 places), ordered by name.
| Param | Type | Default | Notes |
|---|---|---|---|
type |
string | — | Filter by place type (settlement, region, mountain, river, …). Unknown → 400 unknown_type. |
status |
string | — | Filter by status: identified, disputed, unknown, symbolic, multiple. |
q |
string | — | Case-insensitive substring match on the display name. |
limit |
int | 50 |
1–200. |
offset |
int | 0 |
≥ 0. |
$ curl -s 'localhost:8000/v1/places?type=settlement&limit=2'{
"type": "settlement", "status": null, "q": null,
"limit": 2, "offset": 0, "total": 843,
"places": [
{ "id": "a72a1ff", "friendly_id": "Abdon", "name": "Abdon", "type": "settlement",
"latitude": 33.047692, "longitude": 35.161916,
"confidence": "high", "confidence_score": 826, "status": "identified" },
{ "id": "abffcaa", "friendly_id": "Abel-beth-maacah", "name": "Abel-beth-maacah", "type": "settlement",
"latitude": 33.258051, "longitude": 35.581007,
"confidence": "high", "confidence_score": 756, "status": "identified" }
]
}Each place carries named latitude/longitude fields (never a bare ordered pair), a
confidence (high/medium/low, or null), the raw confidence_score, and a status
(see GET /v1/places/{id} for what each status means). Results are ordered
by name then id, so limit/offset pages don't overlap; total is the full filtered count.
Empty results return 200 with "total": 0 and "places": [].
Errors: 400 unknown_type (with detail.available) · 400 unknown_status ·
422 invalid_parameter (limit out of 1–200, negative offset). Caching: immutable.
One place's full detail, by its stable id, plus how many verses mention it.
| Param | In | Type | Notes |
|---|---|---|---|
id |
path | string | The OpenBible place id (e.g. a15257a). |
$ curl -s 'localhost:8000/v1/places/a15257a'{
"id": "a15257a", "friendly_id": "Jerusalem", "name": "Jerusalem", "url_slug": "jerusalem",
"type": "settlement", "preceding_article": "",
"latitude": 31.776667, "longitude": 35.234167,
"confidence": "high", "confidence_score": 1000, "status": "identified",
"modern_name": "Jerusalem", "verse_count": 955
}The honesty model. status is how confidently the place is located, and Concord never
fabricates coordinates:
status |
Meaning | Coordinates |
|---|---|---|
identified |
A confident location. | present |
disputed |
Scholars place it differently; a best guess is given but flagged. | present (hedged) |
unknown |
The location is genuinely lost to history. | null |
symbolic |
A name used non-literally (prophetic/figurative). | null |
multiple |
Itinerant — refers to several places (e.g. the tabernacle). | null |
An unknown place is honest about it — the land of Nod returns null coordinates rather than a
fabricated pin:
$ curl -s 'localhost:8000/v1/places/a1ad8e1'{
"id": "a1ad8e1", "friendly_id": "Nod", "name": "Nod", "url_slug": "nod",
"type": "region", "preceding_article": "",
"latitude": null, "longitude": null,
"confidence": null, "confidence_score": null, "status": "unknown",
"modern_name": null, "verse_count": 1
}Distinct places that share a name are distinct entries with distinct ids — the several Antiochs
and Bethlehems each have their own id and friendly_id (Antioch 1, Antioch 2).
Errors: 404 unknown_place (detail.place_id echoes the id). Caching: immutable.
The verses that mention a place, in canonical order, optionally with text. This is one direction
of the bi-directional link; the inverse is GET /v1/verses/{ref}/places.
| Param | Type | Default | Notes |
|---|---|---|---|
id |
path · string | — | The place id. |
translation |
string | default translation | Which translation's text to hydrate. Only consulted when include_text=true. |
include_text |
bool | true |
When false, translation is null and each text is null. |
limit |
int | 50 |
1–200. |
offset |
int | 0 |
≥ 0. |
$ curl -s 'localhost:8000/v1/places/a15257a/verses?translation=KJV&limit=2'{
"id": "a15257a", "translation": "KJV", "include_text": true,
"limit": 2, "offset": 0, "total": 955,
"verses": [
{ "book": "JOS", "chapter": 10, "verse": 1, "reference": "Joshua 10:1",
"text": "Now it came to pass, when Adonizedek king of Jerusalem had heard how Joshua had taken Ai ..." },
{ "book": "JOS", "chapter": 10, "verse": 2, "reference": "Joshua 10:2",
"text": "That they feared greatly, because Gibeon [was] a great city ..." }
]
}A verse absent in the chosen translation comes back with text: null. With include_text=false,
the response carries just the references — translation is null and every text is null.
total is the place's full verse count, independent of the page.
Errors: 404 unknown_place · 404 unknown_translation (with include_text=true) ·
422 invalid_parameter (limit out of 1–200). Caching: immutable.
The inverse lookup: the places named anywhere in {ref} — a verse, a range, or a whole chapter.
| Param | In | Type | Notes |
|---|---|---|---|
ref |
path | string | A reference per the grammar (URL-encode spaces). |
$ curl -s 'localhost:8000/v1/verses/Acts%2017/places'{
"reference": "Acts 17", "total": 6,
"places": [
{ "id": "a4bdea7", "friendly_id": "Amphipolis", "name": "Amphipolis", "type": "settlement",
"latitude": 40.820159, "longitude": 23.847209,
"confidence": "high", "confidence_score": 1000, "status": "identified" },
{ "id": "ab20df9", "friendly_id": "Apollonia", "name": "Apollonia", "type": "settlement",
"latitude": 40.623703, "longitude": 23.469685,
"confidence": "high", "confidence_score": 1000, "status": "identified" }
]
}The result is the deduped union across the reference's range — a place named in several
verses of the passage appears once — ordered by name then id. A reference that names no place
returns 200 with "total": 0 and "places": [] (never a 404).
Errors: 400 unparseable_reference · 404 unknown_book. Caching: immutable.
Browse the curated set of biblical journeys — ordered sequences of existing places (Paul's missionary journeys, the Exodus), ordered by id.
| Param | Type | Default | Notes |
|---|---|---|---|
limit |
int | 50 |
1–200. |
offset |
int | 0 |
≥ 0. |
$ curl -s 'localhost:8000/v1/journeys'{
"limit": 50, "offset": 0, "total": 5,
"journeys": [
{ "id": "exodus", "name": "The Exodus from Egypt",
"scripture": "Exodus 12 – Numbers 33", "dating": "13th–15th century BC (debated)",
"stop_count": 15 },
{ "id": "paul-first", "name": "Paul's First Missionary Journey",
"scripture": "Acts 13–14", "dating": "c. AD 46–48 (conventional)", "stop_count": 15 }
]
}Each summary carries its scripture range, an approximate dating (null when genuinely
debated), and a stop_count. Caching: immutable.
One journey's full detail: its metadata and its ordered stops, each resolved to a real place.
| Param | In | Type | Notes |
|---|---|---|---|
id |
path | string | The journey slug (e.g. paul-first). |
$ curl -s 'localhost:8000/v1/journeys/paul-first'{
"id": "paul-first", "name": "Paul's First Missionary Journey",
"scripture": "Acts 13–14", "dating": "c. AD 46–48 (conventional)",
"source": "Itinerary derived from the narrative of Acts 13–14; place identifications and coordinates from OpenBible.info (data/geography).",
"note": "One commonly proposed reconstruction following the sequence of Acts. Alternative reconstructions and segment-level routing are not modeled; some legs (e.g. sea crossings) are drawn as direct lines between named stops.",
"stops": [
{ "ordinal": 1, "place_id": "ae41ab4", "name": "Antioch", "friendly_id": "Antioch 1",
"latitude": 36.226691, "longitude": 36.171743,
"confidence": "high", "status": "identified", "reference": "Acts 13:1" },
{ "ordinal": 6, "place_id": "a6c704a", "name": "Antioch", "friendly_id": "Antioch 2",
"latitude": 38.306667, "longitude": 31.189444,
"confidence": "high", "status": "identified", "reference": "Acts 13:14" }
]
}The honesty model. A journey is one commonly proposed reconstruction — source cites where
the route comes from and note says so plainly; competing routes and segment-level dating are not
modeled. Each stop inherits its place's honesty: a stop on a place with no confident location
carries null coordinates (with its status), exactly as /v1/places/{id}.
A revisited place appears once per stop (the ordinal order is the itinerary).
Errors: 404 unknown_journey. Caching: immutable.
The inverse lookup: the journeys that pass through a place.
| Param | In | Type | Notes |
|---|---|---|---|
id |
path | string | The OpenBible place id (e.g. ae41ab4). |
$ curl -s 'localhost:8000/v1/places/a6c704a/journeys'{
"id": "a6c704a", "total": 1,
"journeys": [
{ "id": "paul-first", "name": "Paul's First Missionary Journey",
"scripture": "Acts 13–14", "dating": "c. AD 46–48 (conventional)", "stop_count": 15 }
]
}A place a journey revisits appears once (deduped); a real place in no journey returns 200 with
"total": 0 and "journeys": [] (never a 404). Unknown place id → 404 unknown_place.
Caching: immutable.
Translator's notes for a passage in one translation — study / translator's / text-critical notes
anchored to a point in the verse text, each with its own cross-references. Ordered by verse,
then ordinal.
Notes are user-supplied and never shipped. The published image contains zero notes (the richest sources, NET's notes and a study Bible's, are copyrighted — see notes-ingest and emb-ingest), so on a stock image this endpoint returns
200with an empty list for every translation. A note set appears only after a user bakes their own legally-obtained notes intobible.dblocally.To supply your own: drop a
<TRANSLATION>.jsonfile into the gitignoreddata/private/notes/directory and rebuild (make build-db); the loader picks it up automatically, and the file never enters the public repo or a shared image. Seeexamples/notes-sample.jsonfor a minimal, runnable example of the file shape, and notes-ingest for the full contract (field rules, aliases, validation).
| Param | In | Type | Default | Notes |
|---|---|---|---|---|
translation |
path | string | — | A loaded translation id (case-insensitive). Unknown → 404. |
book |
path | string | — | A book id or alias per the grammar. Unknown → 404. |
chapter |
path | int | — | ≥ 1. |
verse |
query | int | — | ≥ 1. Narrows to a single verse; omit for the whole chapter. |
$ curl -s 'localhost:8000/v1/translations/NET/notes/John/3?verse=16'{
"translation": "NET", "book": "JHN", "chapter": 3, "verse": 16, "total": 1,
"notes": [
{
"book": "JHN", "chapter": 3, "verse": 16, "reference": "John 3:16",
"type": "tn", "text": "Or 'this is how much God loved the world.'",
"char_offset": 8, "marker": "23", "ordinal": 1,
"cross_references": [
{ "to_book": "ROM", "to_chapter": 5, "to_verse_start": 8, "to_verse_end": null,
"reference": "Romans 5:8" }
],
"label": null, "title": null, "text_format": null, "passages": [], "image": null
}
]
}Each note carries:
- its canonical anchor (
book/chapter/verse, plus a humanreference); - the
type:tntranslator's ·snstudy ·tctext-critical ·map·article(a study Bible's feature series) ·chart· ornullfor a plain footnote; - the
text; - the
char_offset: a point where the marker attaches in the verse text, not a span; - the source
marker; - the
ordinal, its stable order within a verse; - the note's own
cross_references. Each is a target by canonical coordinates, withto_verse_endnull for a single verse and set for a range.
The v8 fields (ADR-0011) come last. They're null (or [])
when a source doesn't use them, as for NET:
| Field | Meaning |
|---|---|
label |
The source's own name for the kind of note, for display ("Study Note", "Chart", …). type stays the coarse class. |
title |
The item's heading. |
text_format |
"markdown" when text is Markdown; null means plain text. |
passages |
The ranges the note covers beyond its anchor verse, in the same book: start_chapter, start_verse, end_chapter, end_verse and a reference ("Genesis 12:10-20", "Genesis 12:10-13:4"). |
image |
The name of one of the translation's images, for a chart: fetch it from /v1/translations/{translation}/assets/{name} (ADR-0012). |
ref: links. Markdown text marks a reference a client can jump to as an inline link,
[display text](ref:TARGET). TARGET is a USFM book code, then .C (chapter), .C-C (chapter
range), .C.V (verse), .C.V-V (range) or .C.V-C.V (cross-chapter range).
To read a target with this API, replace the first . with a space and a chapter–verse . with
:: ref:JHN.3.16-4.2 becomes GET /v1/verses/JHN 3:16-4:2, and ref:GEN.12-14 becomes
/v1/verses/GEN 12-14.
Empty results return 200 with "total": 0 and "notes": [] — a translation with no notes
loaded (every translation on the public image) is a normal state, not a 404. Likewise a valid
book + chapter (or ?verse) that simply has no notes returns empty.
Errors: 404 unknown_translation · 404 unknown_book · 422 invalid_parameter
(chapter/verse < 1). Caching: immutable.
One of a translation's images: a chart a note names in image
(ADR-0012). The response is the image itself, exactly as
it was loaded, not JSON.
Images are user-supplied and never shipped, like notes. The published image holds zero, so on a stock image every name is a
404. They appear only after you bake your own: put them in the gitignoreddata/private/assets/<TRANSLATION>/and rebuild (make build-db); see notes-ingest for the rules (JPEG or PNG, lower-case names, at most 2 MiB).
| Param | In | Type | Notes |
|---|---|---|---|
translation |
path | string | A loaded translation id (case-insensitive). Unknown → 404 unknown_translation. |
name |
path | string | The image's name, exactly as a note's image gives it (case-sensitive). Unknown → 404 unknown_asset. |
$ curl -sD- -o chart-01.jpg 'localhost:8000/v1/translations/EMB/assets/chart-01.jpg'
HTTP/1.1 200 OK
content-type: image/jpeg
content-length: 97847
etag: "…"
cache-control: public, max-age=31536000, immutable
vary: Origin
x-content-type-options: nosniffcontent-type is image/jpeg or image/png. The ETag is derived from the bytes, so it's the
same on every request; If-None-Match returns 304.
Errors: 404 unknown_translation · 404 unknown_asset (detail.translation,
detail.name). Any name the translation lacks is a 404, whatever its shape. Caching: immutable.
A translation's documents: what its source prints that is tied to a whole book or to no verse — book introductions, front matter, a reading plan, notes about the edition (ADR-0012). This lists them; the next endpoint reads one.
Documents are user-supplied and never shipped, like notes and images. The published image holds zero, so on a stock image every translation's list is empty. They appear only after you bake your own: put a
<TRANSLATION>.jsonin the gitignoreddata/private/documents/and rebuild (make build-db); see documents-ingest.
| Param | In | Type | Default | Notes |
|---|---|---|---|---|
translation |
path | string | — | A loaded translation id (case-insensitive). Unknown → 404 unknown_translation. |
book |
query | string | — | Only that book's introduction (USFM id or any alias). Unknown → 400 unknown_book. |
kind |
query | string | — | Only that kind: front-matter, reading-plan, book-introduction or about. Unknown → 400 unknown_kind. |
The two filters combine. The list is ordered by kind — front matter, reading plan, book
introductions, about, the order a study Bible prints them — then by ordinal (each document's
place among its kind), then slug.
$ curl -s 'localhost:8000/v1/translations/EMB/documents?kind=book-introduction'{
"translation": "EMB",
"book": null,
"kind": "book-introduction",
"total": 66,
"documents": [
{ "slug": "introduction-gen", "kind": "book-introduction", "title": "…", "book": "GEN",
"ordinal": 1 },
...
]
}book and kind echo the filters (null when not given). Each summary's book is the USFM id
of a book introduction's book, null for the other kinds.
Empty results return 200 with "total": 0 and "documents": []: a translation with no
documents (every translation on the public image), or filters that match none.
Errors: 404 unknown_translation · 400 unknown_book · 400 unknown_kind
(detail.available). Caching: immutable.
One document in full.
| Param | In | Type | Notes |
|---|---|---|---|
translation |
path | string | A loaded translation id (case-insensitive). Unknown → 404 unknown_translation. |
slug |
path | string | The document's slug, exactly as the list gives it (case-sensitive). Unknown → 404 unknown_document. |
$ curl -s 'localhost:8000/v1/translations/EMB/documents/introduction-gen'{
"translation": "EMB",
"slug": "introduction-gen",
"kind": "book-introduction",
"title": "…",
"book": "GEN",
"ordinal": 1,
"text": "## …\n\n- [Chapters 1–3](ref:GEN.1-3): …\n\n\n\n…",
"images": [
{ "name": "reading-time-gen.jpg", "media_type": "image/jpeg", "width": 1024, "height": 187 }
]
}text is always Markdown (CommonMark):
ref:links —[Chapters 1–3](ref:GEN.1-3), the same grammar as notes' (ADR-0011); each target maps onto a reference/v1/verses/{ref}accepts.- Images —
places one of the translation's images where the source prints it. A client fetches it from/v1/translations/{translation}/assets/{NAME}; a renderer that doesn't resolveasset:shows the alt text.
images lists the images the text places, in order of first use, with each one's media type and
pixel size, so a client can lay a figure out before fetching its bytes.
Errors: 404 unknown_translation · 404 unknown_document (detail.translation,
detail.slug). Caching: immutable.
Full-text keyword search over translator-note bodies (the notes_fts FTS5 mirror), across all
loaded note translations by default. The counterpart to /v1/search for notes; the
read endpoint above fetches notes by passage, this one finds them by text.
Notes are user-supplied and never shipped. The published image contains zero notes (the richest source, NET, is copyrighted — see notes-ingest), so on a stock image this endpoint returns
200with an empty list for every query. A populated example like the one below requires a user to bake their own legally-obtained notes intobible.dblocally: drop a<TRANSLATION>.jsoninto the gitignoreddata/private/notes/directory and rebuild (make build-db). Seeexamples/notes-sample.jsonand notes-ingest for the file shape and contract.
| Param | Type | Default | Notes |
|---|---|---|---|
q |
string | — (required) | FTS5 query; same syntax as /v1/search. |
translation |
string | — (all) | Optional filter to one notes translation (e.g. NET). Case-insensitive. Omitted ⇒ all loaded. |
type |
string | — (all) | Optional filter: tn (translator's) · sn (study) · tc (text-critical) · map · other · article · chart. |
book |
string | — | Optional filter; USFM id or alias. |
limit |
int | 20 |
1–100. |
offset |
int | 0 |
≥ 0. |
$ curl -s 'localhost:8000/v1/notes/search?q=Greek&translation=NET&type=tn&limit=1'{
"query": "Greek", "translation": "NET", "type": "tn", "book": null,
"limit": 1, "offset": 0, "total": 1,
"hits": [
{ "book": "JHN", "chapter": 3, "verse": 16, "reference": "John 3:16",
"translation": "NET", "type": "tn", "char_offset": 8, "marker": "23", "ordinal": 1,
"snippet": "The <mark>Greek</mark> construction here indicates result, not purpose.",
"label": null, "title": null, "text_format": null, "passages": [], "image": null }
]
}Each hit carries the note's canonical anchor (book/chapter/verse + a human reference),
the owning translation, the type (or null for a plain footnote), the char_offset, source
marker, ordinal, and a <mark>-tagged snippet of the note body.
The v8 fields (label, title, text_format, passages, image) follow, exactly as on the
passage read. For a Markdown note (text_format: "markdown"), the snippet is cut from the raw
Markdown, so it can contain markup and link syntax. The note's own
cross_references are omitted here for leanness — fetch the full note (with its cross-references)
via the passage read above. Results are
relevance-ranked (FTS5 rank) with a canonical tiebreak (verse → ordinal → id).
Empty results return 200 with "total": 0 and "hits": [] — never a 404. This is the normal
state on the public image (no notes loaded) and for any query with no matches.
Errors: 404 unknown_translation · 400 unknown_type (unknown type; detail.available lists
the valid types) · 400 unknown_book (filter) · 400 invalid_search_query (malformed FTS5 —
detail.fts5_error) · 422 invalid_parameter (missing/empty q, limit out of 1–100).
Caching: immutable.
Browse topical-Bible subjects from every loaded topical source: Nave's Topical
Bible (public domain, 1897), which ships, plus any
source an operator loads privately (ADR-0013; user
flow: docs/v8/topics-ingest.md). Optionally filter by name substring
(q, case-insensitive), section (the A–Z index letter) and source. One A–Z list across
every source: ordered by name ignoring case, then id, so the sources interleave, names equal
but for case keep a stable order, and paging is stable. (Until v1.3.0 the order was binary, which
put all-capitals names before mixed-case ones within a letter. Nave's own order moved in one place:
ANGEL (a spirit) now comes before ANGEL (Holy Trinity).)
| Param | In | Type | Default | Notes |
|---|---|---|---|---|
q |
query | string | — | Case-insensitive name substring. |
section |
query | string | — | The A–Z index letter (e.g. F). |
source |
query | string | — | Only this source, by its exact name as sources lists it (e.g. Nave's Topical Bible). Unknown → 400 unknown_source. |
limit |
query | int | 50 |
1–200. |
offset |
query | int | 0 |
≥ 0. |
$ curl -s 'localhost:8000/v1/topics?q=faith&limit=2'{
"q": "faith", "section": null, "limit": 2, "offset": 0, "total": 4,
"topics": [
{ "id": "faith", "name": "FAITH", "section": "F", "see_also": null,
"source": "Nave's Topical Bible" },
{ "id": "faithfulness", "name": "FAITHFULNESS", "section": "F", "see_also": null,
"source": "Nave's Topical Bible" }
],
"source": null,
"sources": [ { "source": "Nave's Topical Bible", "total": 4 } ]
}see_also is the id of another topic when this one is a "See X" redirect (Nave's points
ANXIETY at CARE); such topics carry no verses of their own. source (each topic's, appended)
names its topical source. The page echoes the source filter and appends sources: every loaded
source with how many of its topics match q and section (ignoring source; 0 included),
ordered by name — the names ?source= accepts. total counts the page's own filters. Take names
from sources rather than hardcoding them (the match is exact). Errors: 400 unknown_source
(detail.source, detail.available). Caching: immutable — note that unfiltered pages and
sources change when an operator rebuilds with a private source, and a client may hold a body
cached before source existed: treat source/sources as optional.
One topic's detail, including its verse_count (0 for a redirect).
| Param | In | Type | Notes |
|---|---|---|---|
id |
path | string | A topic id (slug). Unknown → 404 unknown_topic. |
$ curl -s 'localhost:8000/v1/topics/care'{ "id": "care", "name": "CARE", "section": "C", "see_also": null, "verse_count": 53,
"source": "Nave's Topical Bible" }Errors: 404 unknown_topic (detail.topic_id). Caching: immutable.
The verses curated under a topic, in canonical order, optionally hydrated with text.
| Param | In | Type | Default | Notes |
|---|---|---|---|---|
id |
path | string | — | A topic id. Unknown → 404 unknown_topic. |
translation |
query | string | default translation | Used only when include_text=true. |
include_text |
query | bool | true |
When false, text is null and translation is echoed as null. |
limit |
query | int | 50 |
1–200. |
offset |
query | int | 0 |
≥ 0. |
$ curl -s 'localhost:8000/v1/topics/care/verses?translation=KJV&limit=2'{
"id": "care", "translation": "KJV", "include_text": true, "limit": 2, "offset": 0, "total": 53,
"verses": [
{ "book": "PSA", "chapter": 37, "verse": 5, "reference": "Psalms 37:5",
"text": "Commit thy way unto the LORD; trust also in him; and he shall bring it to pass." },
{ "book": "PSA", "chapter": 39, "verse": 6, "reference": "Psalms 39:6", "text": "…" }
],
"source": "Nave's Topical Bible"
}A verse absent in the chosen translation hydrates as text: null (not an error). A redirect or
empty topic returns "total": 0, "verses": []. source is the topic's source. Errors: 404 unknown_topic.
Caching: immutable.
The inverse lookup: the topics that cite any verse in {ref} — a verse, a range, or a chapter.
| Param | In | Type | Notes |
|---|---|---|---|
ref |
path | string | A reference per the grammar (URL-encode spaces). |
$ curl -s 'localhost:8000/v1/verses/Philippians%204:6/topics'{
"reference": "Philippians 4:6", "total": 5,
"topics": [
{ "id": "care", "name": "CARE", "section": "C", "see_also": null,
"source": "Nave's Topical Bible" },
{ "id": "commandments", "name": "COMMANDMENTS", "section": "C", "see_also": null,
"source": "Nave's Topical Bible" },
{ "id": "prayer", "name": "PRAYER", "section": "P", "see_also": null,
"source": "Nave's Topical Bible" }
]
}The deduped union across the reference's range and across every loaded source (each topic
with its source), in the browse's order: name ignoring case, then id. A reference citing
no topic returns 200 with "total": 0, "topics": []. Errors: 400 unparseable_reference ·
404 unknown_book. Caching: immutable.
Browse the Strong's lexicon (the Greek lexicon from STEPBible,
CC BY 4.0). Optionally filter by q (a case-insensitive substring of the lemma, transliteration,
or gloss) and language (grc for Greek). Ordered by Strong's number within language.
| Param | In | Type | Default | Notes |
|---|---|---|---|---|
q |
query | string | — | Substring of lemma, transliteration, or gloss. |
language |
query | string | — | ISO 639-3 code (grc). |
limit |
query | int | 50 |
1–200. |
offset |
query | int | 0 |
≥ 0. |
$ curl -s 'localhost:8000/v1/strongs?q=love&language=grc&limit=2'{
"q": "love", "language": "grc", "limit": 2, "offset": 0, "total": 18,
"entries": [
{ "strongs_id": "G25", "language": "grc", "lemma": "ἀγαπάω", "transliteration": "agapaō", "gloss": "to love" },
{ "strongs_id": "G26", "language": "grc", "lemma": "ἀγάπη", "transliteration": "agapē", "gloss": "love" }
]
}Caching: immutable.
One lexicon entry in full, including the definition. The id is normalized — the leading letter is
upper-cased and any zero-padding dropped, so g0026, g26, and G26 all resolve to G26.
| Param | In | Type | Notes |
|---|---|---|---|
id |
path | string | A Strong's number (e.g. G26). Unknown → 404 unknown_strongs. |
$ curl -s 'localhost:8000/v1/strongs/G26'{
"strongs_id": "G26", "language": "grc", "lemma": "ἀγάπη", "transliteration": "agapē",
"gloss": "love", "definition": "ἀγάπη, -ης, ἡ … love, goodwill, esteem. …",
"source": "STEP Bible (Tyndale House)"
}Errors: 404 unknown_strongs (detail.strongs_id). Caching: immutable.
The verses where a Strong's number occurs (a concordance), in canonical order, optionally hydrated with an English translation's text.
| Param | In | Type | Default | Notes |
|---|---|---|---|---|
id |
path | string | — | A Strong's number (normalized; e.g. G26). Unknown → 404 unknown_strongs. |
text |
query | string | by id | The tagged text to search. Defaults by the id's language — OSHB for H…, SBLGNT for G…. Unknown → 404. |
translation |
query | string | default translation | Hydrates each verse's text; used only when include_text=true. |
include_text |
query | bool | true |
When false, text is null and translation is echoed as null. |
limit |
query | int | 50 |
1–200. |
offset |
query | int | 0 |
≥ 0. |
$ curl -s 'localhost:8000/v1/strongs/G26/verses?limit=2'{
"strongs_id": "G26", "text_id": "SBLGNT", "translation": "KJV", "include_text": true,
"limit": 2, "offset": 0, "total": 106,
"verses": [
{ "book": "MAT", "chapter": 24, "verse": 12, "reference": "Matthew 24:12", "text": "…" },
{ "book": "LUK", "chapter": 11, "verse": 42, "reference": "Luke 11:42", "text": "…" }
]
}text_id is the tagged text searched (the SBL Greek NT); translation is the English text used to
hydrate each verse (the default translation unless overridden). A verse absent in the chosen
translation hydrates as text: null. Errors: 404 unknown_strongs; 404 unknown text.
Caching: immutable.
The tagged original-language tokens of a reference — the word-study verse view. Each token carries its surface form, Strong's number, morphology code, the lemma/transliteration/gloss joined from the lexicon, and the verse it belongs to.
| Param | In | Type | Default | Notes |
|---|---|---|---|---|
ref |
path | string | — | A reference per the grammar (URL-encode spaces). |
text |
query | string | by testament | The tagged text. Defaults by the reference's testament — OSHB for OT, SBLGNT for NT. Unknown → 404. |
$ curl -s 'localhost:8000/v1/verses/John%2011:35-36/words'{
"reference": "John 11:35-36", "text_id": "SBLGNT", "total": 11,
"tokens": [
{ "position": 3, "surface_form": "Ἰησοῦς.", "strongs_id": "G2424", "morph_code": "N-NSM-P",
"lemma": "Ἰησοῦς", "transliteration": "Iēsous", "gloss": "Jesus",
"book": "JHN", "chapter": 11, "verse": 35, "reference": "John 11:35" },
{ "position": 1, "surface_form": "ἔλεγον", "strongs_id": "G3004", "morph_code": "V-IAI-3P",
"lemma": "λέγω", "transliteration": "legō", "gloss": "to say",
"book": "JHN", "chapter": 11, "verse": 36, "reference": "John 11:36" }
]
}position is per-verse, not a running index — it restarts at 1 in every verse, as it does
between the two tokens above. The unique key for a token is book + chapter + verse +
position. Group by (chapter, verse) to split a multi-verse response into verse blocks; never
infer the boundaries from position resetting, which silently misreads a verse the tagged text
doesn't cover.
Note the two reference fields mean different things: the top-level one echoes the request (your
input order preserved), while a token's names that one token's verse. Tokens always come back in
canonical chapter, verse, position order, whatever order the request listed.
A token's lemma/transliteration/gloss are null when it is untagged or its Strong's has no
lexicon entry; book/chapter/verse/reference are always present. A valid reference with no
tokens (e.g. an OT verse for the NT-only SBLGNT) returns 200 with "total": 0, "tokens": [].
Errors: 400 unparseable_reference · 404 unknown_book · 404 unknown text.
Caching: immutable.
One random verse, optionally constrained. Handy for verse-of-the-day / projection.
| Param | Type | Default | Notes |
|---|---|---|---|
translation |
string | default translation | Single translation. |
book |
string | — | Optional; USFM id or alias. |
testament |
string | — | Optional; OT or NT, case-insensitive. |
$ curl -s 'localhost:8000/v1/random?translation=KJV&testament=OT'{
"translation": "KJV", "book": null, "testament": "OT",
"verse": {
"book": "EZK", "chapter": 34, "verse": 25, "reference": "Ezekiel 34:25",
"text": "And I will make with them a covenant of peace, ..."
}
}Not cached. /random returns Cache-Control: no-store and no ETag — every call
is meant to differ. Don't build If-None-Match / retry logic against it.
Errors: 404 unknown_translation · 400 unknown_book (filter) · 422 invalid_parameter
(testament not ot/nt) · 404 no_match (filters intersect to nothing, e.g.
book=GEN&testament=NT).
The 66-book catalog, in canonical order.
$ curl -s 'localhost:8000/v1/books'{
"books": [
{ "id": "GEN", "name": "Genesis", "testament": "OT", "chapter_count": 50, "canonical_order": 1 },
{ "id": "EXO", "name": "Exodus", "testament": "OT", "chapter_count": 40, "canonical_order": 2 },
...
]
}chapter_count is computed from the loaded verse data. Caching: immutable.
The loaded translations, ordered by id.
$ curl -s 'localhost:8000/v1/translations'{
"translations": [
{ "id": "AKJV", "name": "American King James Version", "language": "en",
"direction": "ltr", "versification": "standard", "attribution": "The American King James Version is in the public domain.",
"note_count": 0, "document_count": 0 },
{ "id": "OSHB", "name": "Open Scriptures Hebrew Bible", "language": "hbo",
"direction": "rtl", "versification": "standard", "attribution": "Hebrew Old Testament … CC BY 4.0 …",
"note_count": 0, "document_count": 0 },
...
]
}note_count is the number of notes loaded for the translation. It's 0 for every translation on
the public image, and higher only once you've baked your own notes in
(notes-ingest). A client offers any translation with note_count > 0 as a
notes source. document_count is the number of its
documents — likewise 0 on the public image.
direction is ltr for everything except the Hebrew OT (OSHB), which is rtl. The
original-language texts (SBLGNT, OSHB) are ordinary translations — usable as ?translation= on
/v1/verses and as ?text= on the word-study endpoints. Caching: immutable.
Liveness plus row counts. Not under /v1; no caching headers.
$ curl -s 'localhost:8000/healthz'{
"status": "ok",
"translation_count": 15, "verse_count": 435951, "cross_ref_count": 344799, "book_count": 66,
"place_count": 1340,
"semantic": {
"enabled": true, "translation": "WEB", "embedding_count": 31054,
"model": "ibm-granite/granite-embedding-311m-multilingual-r2", "dim": 768
}
}The semantic block reports semantic-search readiness: the embedded translation, the vector
count, and the model. When semantic search is disabled (CONCORD_SEMANTIC_SEARCH=0) it is
{ "enabled": false }. The Docker healthcheck treats the container as healthy when this
returns 200 with translation_count > 0 and semantic search ready.
{ref} in /verses and /cross-references accepts these forms (URL-encode spaces):
| Form | Example |
|---|---|
| Single verse | John 3:16 |
| Verse range | John 3:16-18 |
| Verse list | John 3:16,18,20 |
| Whole chapter | John 3 |
| Chapter range | John 3-4 |
| Cross-chapter range | John 3:16-4:2 |
| Chapter through chapter:verse | Judges 13-14:11 (⇒ Judges 13:1-14:11) |
| Numbered books | 1 John, 1John, 1 Jn, I John, First John |
| Separators | colon or period (3:16 ≡ 3.16) |
A bare chapter before the - starts at verse 1, so Judges 13-14:11 and
Judges 13:1-14:11 are the same request — reference echoes the second spelling for both.
The mirror form does not work that way: in John 3:16-4 the bare bound after a : is a
verse in the same chapter, so it reads as 16→4 and is rejected as descending. "Through the
end of chapter 4" would need that chapter's verse count, which the parser deliberately does
not know (ADR-0010).
Two deliberate disambiguations: bare jud → Jude, while Judges is jdg/judg/jg.
Multi-reference strings joined by ; are out of scope for v1. Malformed input →
400 unparseable_reference; an unknown book token → 404 unknown_book.