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.
- 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/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_type |
400 | A /places?type= filter value isn't a known place type; detail.available lists the valid types. |
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.9174,
"text": "Then I said to you, “Don’t dread, neither be afraid of them." },
{ "book": "HAG", "chapter": 2, "verse": 5, "reference": "Haggai 2:5", "score": 0.8967,
"text": "This is the word that I covenanted with you when you came out of Egypt, ..." },
{ "book": "1TH", "chapter": 5, "verse": 20, "reference": "1 Thessalonians 5:20", "score": 0.8952,
"text": "Don’t despise prophesies." }
]
}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.9421,
"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.9111,
"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 source, NET, is copyrighted — see notes-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" }
]
}
]
}Each note carries its canonical anchor (book/chapter/verse + a human reference), the
type (tn translator's · sn study · tc text-critical · map · or null for 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 (stable order within a verse), and the
note's own cross_references (each a target by canonical coords, to_verse_end null for a single
verse, set for a range).
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.
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. |
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." }
]
}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 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 Nave's Topical Bible
(public domain, 1897). Optionally filter by name substring (q, case-insensitive) and section
(the A–Z index letter). Ordered by name, then id.
| Param | In | Type | Default | Notes |
|---|---|---|---|---|
q |
query | string | — | Case-insensitive name substring. |
section |
query | string | — | The A–Z index letter (e.g. F). |
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 },
{ "id": "faithfulness", "name": "FAITHFULNESS", "section": "F", "see_also": null }
]
}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. Caching: immutable.
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 }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": "…" }
]
}A verse absent in the chosen translation hydrates as text: null (not an error). A redirect or
empty topic returns "total": 0, "verses": []. 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 },
{ "id": "commandments", "name": "COMMANDMENTS", "section": "C", "see_also": null },
{ "id": "prayer", "name": "PRAYER", "section": "P", "see_also": null }
]
}The deduped union across the reference's range, ordered by name 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, and the lemma/transliteration/gloss joined from the lexicon.
| 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%203:16/words'{
"reference": "John 3:16", "text_id": "SBLGNT", "total": 25,
"tokens": [
{ "position": 1, "surface_form": "οὕτως", "strongs_id": "G3779", "morph_code": "ADV",
"lemma": "οὕτως", "transliteration": "houtōs", "gloss": "thus(-ly)" },
{ "position": 3, "surface_form": "ἠγάπησεν", "strongs_id": "G25", "morph_code": "V-AAI-3S",
"lemma": "ἀγαπάω", "transliteration": "agapaō", "gloss": "to love" }
]
}A token's lemma/transliteration/gloss are null when it is untagged or its Strong's has no
lexicon entry. 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." },
{ "id": "OSHB", "name": "Open Scriptures Hebrew Bible", "language": "hbo",
"direction": "rtl", "versification": "standard", "attribution": "Hebrew Old Testament … CC BY 4.0 …" },
...
]
}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": 14, "verse_count": 412806, "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 |
| Numbered books | 1 John, 1John, 1 Jn, I John, First John |
| Separators | colon or period (3:16 ≡ 3.16) |
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.