# Fellowverse developers - Documentation Context > SYSTEM INFO: This document is optimized for LLM/AI agent ingestion. > It contains the complete documentation in a structured, context-dense format. > Navigation chrome, styling, and interactive elements have been stripped. > GENERATED: 2026-11-02T07:45:57.720Z --- ## PATH: Api > Search (Source: api/01-search.md) Search by words or a reference ============================== POST /v1/search Takes a few words, a feeling or a reference, and returns passages of two to six verses each, with a context line saying where they sit and who is speaking. A reference such as "Psalm 23" or "John 3:16" returns that passage and the rest of its chapter, with kind set to reference. Anything else is a search by meaning and wording, with kind set to search. Billed per request. Limited to 120 a minute per key. Authentication ============== Send Authorization: Bearer $KEY from your server. Create or rotate keys (/start/authentication). Example request =============== curl https://play.fellowverse.app/v1/search \ -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \ -d '{"query":"Psalm 23","limit":3}' Example IDs are placeholders. Use the upload or job ID returned by your own request. Request body ============ Accepted content types: application/json. | Field | Type | Required | Description and constraints | | --- | --- | --- | --- | | query | string | Yes | Minimum length: 1. Maximum length: 300. | | limit | integer | No | Default: 20. Minimum: 1. Maximum: 50. | Unknown request fields are not accepted. Responses ========= | Status | Meaning | | --- | --- | | 200 | The passages found, best first. | | 400 | The request could not be used. invalid_request when a field is missing or has the wrong type; bad_request when the audio or a value could not be read. | | 401 | The key is missing, wrong, revoked, or past the end of its grace period after a roll. The code is api_unauthorized, and the reason says which. | | 403 | The account is suspended (api_suspended), or a live key was used on an account that has test keys only (api_live_locked). | | 429 | A per-key limit for the minute was reached (api_rate_limited; the reason names the limit), or a test key used up its account's daily allowance (api_test_quota; the fix says when it resets). | | 500 | Something went wrong on our side. The code is internal. | 200: application/json ===================== | Field | Type | Required | Description and constraints | | --- | --- | --- | --- | | kind | string | Yes | reference when the query named a passage, search otherwise. One of search, reference. | | title | string | No | For a reference, the chapter it names, such as "Psalm 23". Absent for a search. | | method | string | No | How a search was matched: rrf blends meaning and wording, text is wording only. Absent for a reference. One of rrf, text, vector. | | degraded | string | No | Present when a search fell back to wording only, saying why. | | results | object[] | Yes | | | results[].id | string | Yes | A stable id for the passage. | | results[].reference | string | Yes | | | results[].book | string | Yes | The book's name in English. | | results[].chapter | integer | Yes | Minimum: 1. | | results[].firstVerse | integer | Yes | Minimum: 1. | | results[].lastVerse | integer | Yes | Minimum: 1. | | results[].heroVerse | integer | Yes | The verse that carries the passage, for a short quote. Minimum: 1. | | results[].contextLine | string | Yes | Where the passage sits and who is speaking. | | results[].verses | object[] | Yes | | | results[].verses[].verse | integer | Yes | Minimum: 1. | | results[].verses[].text | string | Yes | | | results[].reason | string | Yes | Why this passage was returned. | | translation | string | Yes | Always World English Bible (public domain). | “Required” is relative to the containing object. Optional success results and failure errors depend on the job state. Errors and limits ================= JSON errors contain error.code, error.problem, error.reason and error.fix. Handle errors and retries (/guides/errors) and check test and live limits (/start/limits). --- ## PATH: Concepts > Real Recordings (Source: concepts/01-real-recordings.md) Aligning real recordings ======================== A recording of someone reading rarely matches its text exactly. A preacher says "Our reading today is from Mark" before starting. A narrator adds a closing line. A reader says "and" where the page says "but", or skips a word. Alignment that expects a clean, exact reading stops being useful the moment it meets one of these. Fellowverse alignment is built for the recordings people actually make. Send the audio and the text it reads, divided into sections with your own IDs, and you get back when each section and each word is spoken. What it takes care of ===================== - Introductions and sign-offs (#introductions-and-sign-offs) Words before or after the reading, set apart by a pause, are found and left out of your sections. - Small differences from the text (#small-differences-from-the-text) A changed word or a skipped one does not throw the timing off. - Clean cuts (#clean-cuts) Clips are cut in the quiet around each section, never in the middle of a word. - Finding the passage (#finding-the-passage) Send a Bible reading without its text and the passage is found from what is said. Introductions and sign-offs =========================== Most recordings begin and end with something that is not the text: a title, an announcement, a welcome, a "thanks for listening". When a pause sets those words apart, they are found and reported separately, in droppedBefore and droppedAfter, and your first and last sections start and end where the reading does. { "speechStartMs": 9090, "speechEndMs": 49100, "droppedBefore": "Psalm 23. A Psalm by David.", "droppedAfter": "" } You never trim the file first. Times stay in the file you uploaded, so your player can seek straight into the original. Small differences from the text =============================== Readers paraphrase, repeat a phrase, or read from a slightly different edition. Alignment matches the words it hears against the words you sent and carries on past the differences. The result reports matched, the share of your words heard as written, so you can see how closely a recording follows its text and flag the ones worth a listen. Clean cuts ========== Ask for clips and each section comes back as its own MP3, cut in the quiet around it rather than at the exact instant a word ends. A clip starts cleanly and ends without clipping the last syllable, so it plays well on its own or one after another. Finding the passage =================== For Bible readings you can leave the text out. The recording is heard, the passage is found from what is said, from any verse to any verse and across chapters, and each verse becomes a section with its reference and text. A reading recorded on a Sunday becomes verse-by-verse audio without anyone typing a reference. Where it fits ============= The sections are yours to define, so alignment works for any recording of a known text: - Sermons and talks against their outline or manuscript, so listeners can jump to a point. - Liturgies and prayers, timed line by line for following along in a service or at home. - Scripture in your own translation, sent as sections with your own verse IDs, for any edition your church reads. - Audiobooks and lessons, by chapter, paragraph or sentence, for read-along text and study tools. - Language practice, word by word, so a learner can hear any word again on its own. Try it ====== Follow Align a recording (/guides/alignment) for the requests, or Build a read-along player (/guides/read-along) to put the timings to work. Then see Record once, cut many ways (/concepts/record-once) for what one recording can become. --- ## PATH: Guides > Search (Source: guides/01-search.md) Search scripture ================ POST /v1/search takes a few words, a feeling or a reference, and returns passages: two to six verses each, with a context line saying where they sit and who is speaking. curl https://play.fellowverse.app/v1/search \ -H "Authorization: Bearer $KEY" -H 'content-type: application/json' \ -d '{"query": "something for fear", "limit": 5}' { "kind": "search", "method": "rrf", "results": [ { "id": "psg_...", "reference": "1 John 4:18-21", "book": "1 John", "chapter": 4, "firstVerse": 18, "lastVerse": 21, "heroVerse": 18, "contextLine": "...", "verses": [{ "verse": 18, "text": "There is no fear in love..." }], "reason": "..." } ], "translation": "World English Bible (public domain)" } A reference such as "Psalm 23" or "John 3:16" returns that passage and the rest of its chapter, with "kind": "reference". limit is 1 to 50, 20 by default. Billed per request. See the request and response fields (/api/search). --- ## PATH: Start > Quickstart (Source: start/01-quickstart.md) Make your first request ======================= Your first search takes a few minutes, and test keys are free. 1. Sign in to the developer console at https://play.fellowverse.app/developers with your email, and name your account. It starts with test keys. 2. Under Keys, make a test key. Copy it now: it is shown once. 3. Search: export KEY=fv_test_... curl https://play.fellowverse.app/v1/search \ -H "Authorization: Bearer $KEY" -H 'content-type: application/json' \ -d '{"query": "rest for the weary", "limit": 3}' 4. Check what you used, here or under Use in the console: curl https://play.fellowverse.app/v1/usage -H "Authorization: Bearer $KEY" Test keys call the same API as live keys, so the code you write now is the code you ship. When you are ready, see Go live (/start/go-live). Next: keep your key secure (/start/authentication) and check test allowances (/start/limits). --- ## PATH: Api > Voice Search (Source: api/02-voice-search.md) Search by a spoken request ========================== POST /v1/voice-search Takes a spoken request of up to 30 seconds and 10 MB as the request body, with its type, and returns what was heard and the same results as a search. The audio is not kept. When nothing could be heard, the response holds an empty transcript and no results. Billed per second of audio, at least one second. Limited to 60 a minute per key. Authentication ============== Send Authorization: Bearer $KEY from your server. Create or rotate keys (/start/authentication). Example request =============== curl 'https://play.fellowverse.app/v1/voice-search?limit=5' \ -H "Authorization: Bearer $KEY" -H 'Content-Type: audio/webm' \ --data-binary @request.webm Example IDs are placeholders. Use the upload or job ID returned by your own request. Parameters ========== | Name | In | Required | Type | Description and constraints | | --- | --- | --- | --- | --- | | limit | query | No | integer | How many passages to return. Default: 20. Minimum: 1. Maximum: 50. | Request body ============ The recording itself as the body, with its type in Content-Type. MP3, WAV, M4A, OGG, WebM and FLAC are read. Accepted content types: audio/mpeg, audio/mp3, audio/wav, audio/x-wav, audio/wave, audio/webm, audio/ogg, audio/mp4, audio/x-m4a, audio/aac, audio/flac, application/octet-stream. Send the raw audio bytes as the entire request body, with the matching Content-Type. Do not wrap the recording in multipart form data or JSON. Responses ========= | Status | Meaning | | --- | --- | | 200 | What was heard, and the passages found for it. | | 400 | The request could not be used. invalid_request when a field is missing or has the wrong type; bad_request when the audio or a value could not be read. | | 401 | The key is missing, wrong, revoked, or past the end of its grace period after a roll. The code is api_unauthorized, and the reason says which. | | 403 | The account is suspended (api_suspended), or a live key was used on an account that has test keys only (api_live_locked). | | 413 | The body is over the limit: 200 MB for an upload, 10 MB for voice search. The code is request_error. | | 415 | The Content-Type is not one of the audio types read. The code is request_error. | | 429 | A per-key limit for the minute was reached (api_rate_limited; the reason names the limit), or a test key used up its account's daily allowance (api_test_quota; the fix says when it resets). | | 500 | Something went wrong on our side. The code is internal. | | 503 | Voice search cannot be served right now; typed search keeps working. The code is api_stt_off. | 200: application/json ===================== | Field | Type | Required | Description and constraints | | --- | --- | --- | --- | | transcript | string | Yes | What was heard. Empty when nothing was, and then there are no results. | | kind | string | No | reference when the query named a passage, search otherwise. One of search, reference. | | title | string | No | For a reference, the chapter it names, such as "Psalm 23". Absent for a search. | | method | string | No | How a search was matched: rrf blends meaning and wording, text is wording only. Absent for a reference. One of rrf, text, vector. | | degraded | string | No | Present when a search fell back to wording only, saying why. | | results | object[] | Yes | | | results[].id | string | Yes | A stable id for the passage. | | results[].reference | string | Yes | | | results[].book | string | Yes | The book's name in English. | | results[].chapter | integer | Yes | Minimum: 1. | | results[].firstVerse | integer | Yes | Minimum: 1. | | results[].lastVerse | integer | Yes | Minimum: 1. | | results[].heroVerse | integer | Yes | The verse that carries the passage, for a short quote. Minimum: 1. | | results[].contextLine | string | Yes | Where the passage sits and who is speaking. | | results[].verses | object[] | Yes | | | results[].verses[].verse | integer | Yes | Minimum: 1. | | results[].verses[].text | string | Yes | | | results[].reason | string | Yes | Why this passage was returned. | | translation | string | No | Always World English Bible (public domain). | “Required” is relative to the containing object. Optional success results and failure errors depend on the job state. Errors and limits ================= JSON errors contain error.code, error.problem, error.reason and error.fix. Handle errors and retries (/guides/errors) and check test and live limits (/start/limits). --- ## PATH: Concepts > Record Once (Source: concepts/02-record-once.md) Record once, cut many ways ========================== Short audio is usually made one piece at a time: a clip per verse, a file per lesson, a take per line. It is slow, it costs more, and it sounds stitched together, because the rhythm a reader carries from one sentence into the next is lost at every cut. Fellowverse works the other way round. Every chapter of the Bible in Fellowverse is one continuous reading, and every passage and verse people hear is cut from it. The same approach is open to you through the API: make one long recording, and let alignment turn it into as many pieces as you need. One recording, many results =========================== Record the whole thing ====================== Read the chapter, the sermon or the lesson straight through, the way it is meant to be heard. Upload it once. Align it to your sections ========================= Send the text divided however you like: paragraphs, verses, points of a talk. Each section comes back with its start and end, and every word with its own time. Use the pieces ============== Seek your player to any section of the original file, light up each word as it is spoken, or download a clip per section to play on its own. Why it sounds better ==================== A reader who knows where a sentence is going reads it differently. Pauses fall where they belong, and the voice rises and settles naturally across the whole piece. Recorded one fragment at a time, each piece starts cold. Cut from one reading, every clip keeps the life of the whole, and a verse played alone sounds like it belongs to its chapter, because it does. Why it is simpler ================= - One session, not hundreds. A chapter or a talk is recorded in one sitting, with one microphone and one room sound. - One file to keep. Host the original and store the timings; sections are positions in that file, so nothing else needs storing unless you want clips. - New cuts without new recordings. Section timings come with word timings, so you can start or stop anywhere a word does, in your own player, without aligning again. To change the sections themselves, align the same upload again with the new text while it is kept. Cut the way your listeners listen ================================= The same recording can serve different moments: | From one recording of | You can offer | | --- | --- | | A Bible chapter | The whole chapter, passages of a few verses, or a single verse | | A sermon | The full message, each main point, or the one line worth sharing | | A lesson | The lesson, each step, or each new word with its example | | A liturgy | The service, each prayer, or a response for practice | Try it ====== Start with Align a recording (/guides/alignment), then Build a read-along player (/guides/read-along) to play sections and highlight words. See Aligning real recordings (/concepts/real-recordings) for how alignment copes with what real recordings contain, and Pacing and background sound (/concepts/pacing-and-sound) for playing the pieces differently in different moments. --- ## PATH: Guides > Voice Search (Source: guides/02-voice-search.md) Add voice search ================ POST /v1/voice-search takes a spoken request of up to 30 seconds (and 10 MB) as the request body, with its type, and returns what was heard and the same results as a search. curl https://play.fellowverse.app/v1/voice-search \ -H "Authorization: Bearer $KEY" -H 'content-type: audio/webm' \ --data-binary @request.webm The response is a search response with "transcript" added. MP3, WAV, M4A, OGG, WebM and FLAC are read. The audio is not kept. Billed per second of audio. Record and send from your app ============================= Record the request in the app, send the audio to your server, and pass it to POST /v1/voice-search as the request body with its type. On Android, MediaRecorder with OutputFormat.MPEG_4 and AudioEncoder.AAC gives audio/mp4; in a browser, MediaRecorder gives audio/webm or audio/mp4. MP3, WAV, OGG and FLAC work too. curl 'https://play.fellowverse.app/v1/voice-search?limit=5' \ -H "Authorization: Bearer $KEY" -H 'content-type: audio/mp4' \ --data-binary @request.m4a Show transcript back to the person, so they can see what was heard, with the results under it. When transcript is empty, nothing could be heard: ask them to try again closer to the microphone. Keep requests to a short spoken phrase; up to 30 seconds is read. See the voice-search endpoint (/api/voice-search). --- ## PATH: Start > Authentication (Source: start/02-authentication.md) Authentication and keys ======================= Send your key in the Authorization header: Authorization: Bearer fv_live_... A key is shown once, when it is made. Keep it on your server; do not put it in a web page or an app, where anyone could read it. Lost keys are revoked and replaced, not recovered. Test keys start fv_test_ and call the same API for free, with a daily allowance per account (see limits (/start/limits)). Make keys in the developer console at https://play.fellowverse.app/developers; every account starts with free test keys. Rolling a key makes its replacement and lets the old key keep working for up to a week, so you can switch over without a gap. Never ship a Fellowverse API key in browser JavaScript or a mobile app. Use your own backend. See the server relay guide (/guides/server-relay). --- ## PATH: Api > Uploads (Source: api/03-uploads.md) Upload a recording to align =========================== POST /v1/uploads Takes a recording of up to 200 MB as the request body, with its type. One alignment takes up to an hour of audio, or 10 minutes with a test key. Uploads are kept for 7 days (a day with a test key), long enough to align them; this is not a storage service. Limited to 30 a minute per key. Authentication ============== Send Authorization: Bearer $KEY from your server. Create or rotate keys (/start/authentication). Example request =============== curl https://play.fellowverse.app/v1/uploads \ -H "Authorization: Bearer $KEY" -H 'Content-Type: audio/mpeg' \ --data-binary @chapter.mp3 Example IDs are placeholders. Use the upload or job ID returned by your own request. Request body ============ The recording itself as the body, with its type in Content-Type. MP3, WAV, M4A, OGG, WebM and FLAC are read. Accepted content types: audio/mpeg, audio/mp3, audio/wav, audio/x-wav, audio/wave, audio/webm, audio/ogg, audio/mp4, audio/x-m4a, audio/aac, audio/flac, application/octet-stream. Send the raw audio bytes as the entire request body, with the matching Content-Type. Do not wrap the recording in multipart form data or JSON. Responses ========= | Status | Meaning | | --- | --- | | 201 | The upload was kept. Use its id to start an alignment. | | 400 | The request could not be used. invalid_request when a field is missing or has the wrong type; bad_request when the audio or a value could not be read. | | 401 | The key is missing, wrong, revoked, or past the end of its grace period after a roll. The code is api_unauthorized, and the reason says which. | | 403 | The account is suspended (api_suspended), or a live key was used on an account that has test keys only (api_live_locked). | | 413 | The body is over the limit: 200 MB for an upload, 10 MB for voice search. The code is request_error. | | 415 | The Content-Type is not one of the audio types read. The code is request_error. | | 429 | A per-key limit for the minute was reached (api_rate_limited; the reason names the limit), or a test key used up its account's daily allowance (api_test_quota; the fix says when it resets). | | 500 | Something went wrong on our side. The code is internal. | 201: application/json ===================== | Field | Type | Required | Description and constraints | | --- | --- | --- | --- | | id | string | Yes | Pattern: ^upl_[A-Za-z0-9_-]{12}$. | | bytes | integer | Yes | Minimum: 1. | “Required” is relative to the containing object. Optional success results and failure errors depend on the job state. Errors and limits ================= JSON errors contain error.code, error.problem, error.reason and error.fix. Handle errors and retries (/guides/errors) and check test and live limits (/start/limits). --- ## PATH: Concepts > Pacing And Sound (Source: concepts/03-pacing-and-sound.md) Pacing and background sound =========================== People listen to the same passage in very different moments. At a desk, a verse wants room to settle before the next one. On a run, the next one should arrive after a stretch of road. Between sets at the gym, it fills the rest. At night, the reading is slower and the gaps are short. Fellowverse does not keep a version for each moment. There is one recording of each chapter, and the moment changes how it is played. Pacing is data ============== A pacing is a small record, not a different recording. It says how long to wait between passages, how fast to play them, whether the list starts again at the end, and whether background sound plays under them. | Pacing | Between passages | Speed | Feels like | | --- | --- | --- | --- | | Work | about a minute | a touch slower | A verse, then quiet focus, then another | | Run | one to two minutes | a touch faster | A passage every so often along the way | | Gym | two to three minutes | as read | One passage for each rest between sets | | Rest | several seconds | slower | An unhurried reading to wind down | | Read | none | as read | Straight through, once, with nothing under it | Because pacing is data, it is the same for every playlist, every voice and every passage. Choosing Gym changes nothing that was recorded, and a new pacing is a new record, not a new recording session. Background sound, mixed on the server ===================================== Under the narration a listener can choose a background: room tone, rain, a low drone, soft pads, bells, open fifths, or one of a few licensed music tracks. Each sits at a fixed listening level made for speech, so the voice is always clear above it. The background is mixed into the narration on the server, not layered in the listener's device. One file plays, with one volume and one set of controls. That matters most on phones: - One stream behaves well in the background. Two sounds playing at once in a phone browser or app can drift apart, and a phone may silence one of them when the screen locks. A single stream keeps playing. - Lock-screen and headset controls stay simple. Play, pause and skip act on the one thing that is playing. - Levels are the same everywhere. Some phones ignore a page's volume settings, so a background turned down in code can still play at full volume. A mix made on the server sounds the same on every device. Each mix is made once and kept, named for the recording, the background and the version of each, so the next listener gets it straight away. The gaps are not silent ======================= The pause between passages is filled with a soft sound of its own rather than silence. A phone treats long silence as the end of listening and may stop the session, taking the lock-screen controls with it. A quiet, continuous gap keeps the session alive for the whole playlist, so a run or a workout plays through with the phone in a pocket. In your own player ================== The API gives you the pieces to do the same with your own recordings: - Sections become playable units. Align a recording (/guides/alignment) once and play its sections one at a time, from positions in the file you host. - Pacing is yours to define. Keep a small record like the table above for each moment your listeners have, and use it to set the gap, the speed and whether to repeat. - Mix ahead, not on the device. If you add background sound, mix it into your files where you host them, at a level made for speech, and cache each mix. One stream plays better than two. - Fill the gaps. Play a quiet sound between sections rather than stopping, so playback carries on with the screen off. See Record once, cut many ways (/concepts/record-once) for turning one recording into sections, and Build a read-along player (/guides/read-along) for the player itself. --- ## PATH: Guides > Alignment (Source: guides/03-alignment.md) Align a recording ================= Alignment takes a recording and the text it reads, divided into sections with your own ids (paragraphs, sentences, verses), and returns when each section and each word is spoken, and a clip per section if you ask. Announcements and closing lines set apart by a pause are found and left out of the sections; wording that differs slightly from your text is tolerated. 1. Upload the audio, up to 200 MB and an hour long, as the request body: curl https://play.fellowverse.app/v1/uploads -H "Authorization: Bearer $KEY" \ -H 'content-type: audio/mpeg' --data-binary @chapter.mp3 Returns {"id": "upl_...", "bytes": ...}. Uploads are kept for 7 days, or a day with a test key. 2. Start the alignment: curl https://play.fellowverse.app/v1/alignments -H "Authorization: Bearer $KEY" \ -H 'content-type: application/json' \ -d '{"upload": "upl_...", "clips": true, "segments": [{"id": "v1", "text": "..."}, {"id": "v2", "text": "..."}]}' The text may be up to 200,000 characters in all. Returns 202 with {"id": "job_...", "status": "queued", "url": "/v1/jobs/job_..."}. 3. Poll GET /v1/jobs/{id} until status is done or failed. A done alignment holds: { "durationMs": 52689, "matched": 0.973, "speechStartMs": 9090, "speechEndMs": 49100, "droppedBefore": "Psalm 23. A Psalm by David.", "droppedAfter": "", "segments": [{ "id": "v1", "startMs": 9480, "endMs": 12260, "clip": "1.mp3" }], "words": [{ "text": "Yahweh", "startMs": 9480, "endMs": 9900 }] } Times are milliseconds in the file you uploaded. matched is the share of your text's words heard as written; below 0.5 the job fails, because the audio does not read the text. 4. Fetch clips from GET /v1/jobs/{id}/files/{clip}. Clips are kept for 7 days after the job finishes (a day with a test key); the job and its result stay. Bible readings without text =========================== Leave out segments and the recording is taken for a reading of scripture: the passage is found from what is said, and its verses become the sections. curl https://play.fellowverse.app/v1/alignments -H "Authorization: Bearer $KEY" \ -H 'content-type: application/json' -d '{"upload": "upl_...", "clips": true}' The result adds the passage, and each section is a verse with its id, reference and text: { "passage": { "reference": "Proverbs 23:27-24:10", "book": "PRO", "bookName": "Proverbs", "firstChapter": 23, "firstVerse": 27, "lastChapter": 24, "lastVerse": 10, "translation": "World English Bible (public domain)" }, "segments": [ { "id": "PRO.23.27", "reference": "Proverbs 23:27", "text": "For a prostitute is a deep pit...", "startMs": 1000, "endMs": 4920, "clip": "1.mp3" } ] } A reading can start and end anywhere and run across chapters. A verse the recording only catches part of, at the start or end, is left out and reported in droppedBefore or droppedAfter. Only the World English Bible is held: a reading of another translation is found when enough of its wording agrees, and timed less closely. A recording that is not a reading, or that only quotes a verse, fails with the code api_passage_unknown; send its text as segments instead. A job that fails for a reason in the request (an unreadable file, a recording over an hour, text that does not match) fails at once with that reason. One that fails on our side is tried again, up to three times. Jobs of a suspended account wait, and run when it is restored. Billed per second of audio, once, when the job is done. See uploads (/api/uploads), alignment requests (/api/alignments), job results (/api/jobs) and clip downloads (/api/clips). --- ## PATH: Start > Limits (Source: start/03-limits.md) Limits and test allowances ========================== Per key, per minute: 120 searches, 60 voice searches, 30 uploads and 30 alignments. A 429 says which limit was reached. Test keys: per key, per minute, 10 searches, 5 voice searches, 5 uploads and 2 alignments; and per account, per UTC day, 500 searches, 10 minutes of voice search and 2 alignments of up to 10 minutes each. The 429 for a daily allowance says when it resets. File and text limits ==================== | Input | Limit | | --- | --- | | Search query | 1–300 characters | | Search results | 1–50; default 20 | | Voice search | 30 seconds; 10 MB | | Alignment upload | 200 MB | | Live alignment | 1 hour of audio | | Test alignment | 10 minutes of audio | | Alignment segments | 1–5,000 when supplied | | Text in one segment | 1–20,000 characters | | Text across all segments | 200,000 characters | | Segment ID | 1–100 characters | Uploads and clips are kept for 7 days for live keys or 1 day for test keys, long enough to align them and fetch the clips. Clips are kept from when the job finishes, and job results stay. Voice-search audio is not kept. See errors and retries (/guides/errors) for how to respond to a rate limit. Search is metered per request; voice search and alignment per second of audio. Pricing is on request: the account's owner asks for live access in the console, and we set up billing together. --- ## PATH: Api > Alignments (Source: api/04-alignments.md) Align an upload to the text it reads ==================================== POST /v1/alignments Starts aligning an upload to its text, given as sections with your own ids: paragraphs, sentences, verses or lines. The job runs in the background; poll GET /v1/jobs/{id} for the result. Announcements and closing lines set apart by a pause are found and left out of the sections. Wording that differs slightly from your text is tolerated. Leave out segments for a Bible reading: the passage is identified from what is said, from any verse to any verse and across chapters, and its verses become the sections. The result then holds passage. A recording that is not a reading, or only quotes a verse, fails with the code api_passage_unknown. Only the World English Bible is held, so a reading of another translation is found only when enough of its wording agrees. Billed per second of audio when the job is done. Limited to 30 a minute per key. Authentication ============== Send Authorization: Bearer $KEY from your server. Create or rotate keys (/start/authentication). Example request =============== curl https://play.fellowverse.app/v1/alignments \ -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \ -d '{"upload":"upl_Hk3vQ9xTmP2a","clips":true,"segments":[{"id":"v1","text":"Yahweh is my shepherd; I shall lack nothing."}]}' Example IDs are placeholders. Use the upload or job ID returned by your own request. Request body ============ Accepted content types: application/json. | Field | Type | Required | Description and constraints | | --- | --- | --- | --- | | upload | string | Yes | The id from POST /v1/uploads. Pattern: ^upl_[A-Za-z0-9_-]{12}$. | | clips | boolean | No | Make an MP3 clip of each section, cut in the quiet around it. Default: false. | | segments | object[] | No | The text the recording reads, in order, as sections with your own ids. Leave it out for a Bible reading whose passage is to be found. The text may be up to 200,000 characters in all, about an hour of reading. Minimum items: 1. Maximum items: 5000. | | segments[].id | string | Yes | Minimum length: 1. Maximum length: 100. | | segments[].text | string | Yes | Minimum length: 1. Maximum length: 20000. | Unknown request fields are not accepted. Responses ========= | Status | Meaning | | --- | --- | | 202 | The job was queued. | | 400 | The request could not be used. invalid_request when a field is missing or has the wrong type; bad_request when the audio or a value could not be read. | | 401 | The key is missing, wrong, revoked, or past the end of its grace period after a roll. The code is api_unauthorized, and the reason says which. | | 403 | The account is suspended (api_suspended), or a live key was used on an account that has test keys only (api_live_locked). | | 404 | The upload, job or clip does not exist, or belongs to another account. The codes are api_upload_missing, api_job_missing and api_file_missing. api_off means the request went to an address that does not serve the API; use https://play.fellowverse.app. | | 429 | A per-key limit for the minute was reached (api_rate_limited; the reason names the limit), or a test key used up its account's daily allowance (api_test_quota; the fix says when it resets). | | 500 | Something went wrong on our side. The code is internal. | 202: application/json ===================== | Field | Type | Required | Description and constraints | | --- | --- | --- | --- | | id | string | Yes | | | status | string | Yes | Always queued. | | url | string | Yes | Where to poll for the job. | “Required” is relative to the containing object. Optional success results and failure errors depend on the job state. Errors and limits ================= JSON errors contain error.code, error.problem, error.reason and error.fix. Handle errors and retries (/guides/errors) and check test and live limits (/start/limits). --- ## PATH: Concepts > Agents (Source: concepts/04-agents.md) Agents that quote scripture =========================== Ask a chat assistant for "something about fear" and it will often answer with a verse from memory. Most of the time it is close. Sometimes a word has changed, two verses have merged, or the reference points somewhere else. For scripture, close is not good enough. The fix is to stop asking the model to remember. Give it a tool that finds the passage and returns its exact words, and tell it to quote what comes back. The model still does what it is good at, understanding the person and choosing what to say. The text itself comes from the source, word for word, with its reference. A tool for any agent ==================== Most tool-calling APIs take a name, a description and a schema for the input. This one works as it is; rename input_schema to parameters where your API asks for that. { "name": "find_passage", "description": "Find Bible passages for a feeling, a topic or a reference, such as 'something for fear' or 'Psalm 23'. Returns the passage text from the World English Bible. When you use a passage, quote its text exactly as returned and say its reference; never paraphrase scripture or quote it from memory.", "input_schema": { "type": "object", "properties": { "query": { "type": "string", "description": "A feeling, a topic, a few words from the passage, or a reference." }, "limit": { "type": "integer", "minimum": 1, "maximum": 5, "description": "How many passages to return." } }, "required": ["query"] } } The description does two jobs. It tells the model when the tool helps, and it tells the model how to use what comes back. Say the same in your system prompt, so the rule holds for the whole conversation. Running the tool ================ When the model calls find_passage, your server calls search (/api/search) with its own key and hands back only what the model needs: each passage's reference, the line saying where it sits and who is speaking, and its words. export async function findPassage({ query, limit = 3 }) { const res = await fetch('https://play.fellowverse.app/v1/search', { method: 'POST', headers: { authorization: `Bearer ${process.env.FELLOWVERSE_KEY}`, 'content-type': 'application/json', }, body: JSON.stringify({ query, limit: Math.min(Math.max(limit, 1), 5) }), }); const body = await res.json(); if (!res.ok) return { error: body.error.problem, fix: body.error.fix }; return { translation: body.translation, passages: body.results.map((p) => ({ reference: p.reference, context: p.contextLine, text: p.verses.map((v) => v.text).join(' '), })), }; } A failed call goes back to the model as a plain reason and a fix, so it can tell the person what happened or try a different query, rather than filling the gap from memory. What the model sees is short and complete: { "translation": "World English Bible (public domain)", "passages": [ { "reference": "1 John 4:18-21", "context": "...", "text": "There is no fear in love; but perfect love casts out fear..." } ] } The context line matters as much as the text. It tells the model who is speaking and to whom, so an answer can say "Jesus says to his disciples" rather than guessing. Voice agents ============ A spoken conversation adds two moments worth handling. - A spoken request. When someone asks out loud, the agent's own speech recognition turns it into text, and the same tool finds the passage. If your agent takes audio directly, voice search (/guides/voice-search) takes the recording and returns what was heard with the passages. - A reading. When someone reads a passage aloud, perhaps to practise it or to ask about it, the agent can know exactly what they read. Upload the turn's audio and start an alignment (/guides/alignment) without text: the passage is found from what was said, with its reference. Run it alongside the conversation, and when it returns, the agent can answer "That was Psalm 23, verses 1 to 4" and quote it back. Beyond the Bible ================ The pattern holds for any text whose exact words matter: a liturgy, a creed, a policy, a statute, a contract. Keep the text behind a tool, let the model choose and explain, and have it quote what the tool returns. The model stays conversational and the words stay exact. For your coding assistant ========================= These docs are written to be read by assistants as well as people. - llms-index.txt (/llms-index.txt) lists every page with its size, for an assistant to choose what to read. - llms-full.txt (/llms-full.txt) is the whole site as one file, to paste into a conversation or a project. - An MCP server at /mcp on this site gives an assistant three tools, search_docs, read_page and list_pages, so it can look things up while it writes your integration. Add the address to any MCP client that takes a streamable HTTP server. See Keep your key on your server (/guides/server-relay) for where the tool's key belongs, and Handle errors and retries (/guides/errors) for the codes a failed call can return. --- ## PATH: Guides > Read Along (Source: guides/04-read-along.md) Build a read-along player ========================= Say you have a recording of Psalm 23 and want listeners to tap a verse and hear it, with each word lit as it is read. 1. Upload the recording with POST /v1/uploads and start an alignment with POST /v1/alignments. For a Bible reading you can leave out segments, and the passage and its verses are found for you. For your own text (a sermon outline, a liturgy), send it as segments with your own ids. 2. Poll GET /v1/jobs/{id} every few seconds until status is done or failed. 3. Store the result next to the recording in your own system. Each segment's startMs and endMs let your player seek to a verse in the file you already host; words gives the times to light each word. 4. If you asked for clips, download each clip from GET /v1/jobs/{id}/files/{clip} with your key and host it yourself. Clips are there for 7 days after the job finishes, so fetch them when the job is done; the timings in the result stay. Align each recording once, when it is published, and serve the timings from your own storage after that. Listeners never wait on an alignment. Fetch clips when the job is done: they are kept for 7 days for live keys or 1 day for test keys. The timings stay in the job result, so your player can keep using them. --- ## PATH: Start > Go Live (Source: start/04-go-live.md) Go live ======= 1. In the console, the account's owner chooses Request live access. We get in touch to set up billing, and turn on live keys for the account. 2. Make a live key under Keys. A live key takes a code we email you, so a signed-in browser alone cannot make one. 3. Put the live key on your server in place of the test key. Nothing else changes: the same requests, the same responses. 4. To replace a key, roll it in the console and give the old one a grace period of up to a week. Both work while you deploy the new one, and the old one then stops. Every live key made, rolled or revoked is emailed to the account's owners, and the console's Activity shows who did what and when. --- ## PATH: Api > Jobs (Source: api/05-jobs.md) See a job's state and result ============================ GET /v1/jobs/{id} Poll until status is done or failed. A done job holds its result; a failed one holds its error. A job that fails for a reason in the request (an unreadable file, text that does not match) fails at once with that reason. One that fails on our side is tried again, up to three times, and goes back to queued in between. Jobs of a suspended account wait in queued until it is restored. Authentication ============== Send Authorization: Bearer $KEY from your server. Create or rotate keys (/start/authentication). Example request =============== curl https://play.fellowverse.app/v1/jobs/job_Hk3vQ9xTmP2a \ -H "Authorization: Bearer $KEY" Example IDs are placeholders. Use the upload or job ID returned by your own request. Parameters ========== | Name | In | Required | Type | Description and constraints | | --- | --- | --- | --- | --- | | id | path | Yes | string | The job id returned when the alignment was started. | Responses ========= | Status | Meaning | | --- | --- | | 200 | The job. | | 401 | The key is missing, wrong, revoked, or past the end of its grace period after a roll. The code is api_unauthorized, and the reason says which. | | 403 | The account is suspended (api_suspended), or a live key was used on an account that has test keys only (api_live_locked). | | 404 | The upload, job or clip does not exist, or belongs to another account. The codes are api_upload_missing, api_job_missing and api_file_missing. api_off means the request went to an address that does not serve the API; use https://play.fellowverse.app. | | 500 | Something went wrong on our side. The code is internal. | 200: application/json ===================== | Field | Type | Required | Description and constraints | | --- | --- | --- | --- | | id | string | Yes | | | kind | string | Yes | One of alignment. | | status | string | Yes | One of queued, running, done, failed. | | attempts | integer | Yes | How many times the job has been started. Minimum: 0. Maximum: 3. | | createdAt | string | Yes | Format: date-time. | | finishedAt | string or null | Yes | Format: date-time. | | result | object | No | Present when the job is done. Times are milliseconds in the file you uploaded. | | result.passage | object | No | For an alignment sent without text, the passage the recording was found to read. | | result.passage.reference | string | Yes | | | result.passage.book | string | Yes | The standard book code. | | result.passage.bookName | string | Yes | | | result.passage.firstChapter | integer | Yes | Minimum: 1. | | result.passage.firstVerse | integer | Yes | Minimum: 1. | | result.passage.lastChapter | integer | Yes | Minimum: 1. | | result.passage.lastVerse | integer | Yes | Minimum: 1. | | result.passage.translation | string | Yes | Always World English Bible (public domain). | | result.durationMs | integer | Yes | The length of the recording. | | result.matched | number | Yes | The share of your text's words heard as written. Below 0.5 the job fails, because the audio does not read the text. Minimum: 0.5. Maximum: 1. | | result.speechStartMs | integer | Yes | Where the text starts being read. | | result.speechEndMs | integer | Yes | Where the text stops being read. | | result.droppedBefore | string | Yes | What was heard before the text and left out, such as an announcement. | | result.droppedAfter | string | Yes | What was heard after the text and left out. | | result.segments | object[] | Yes | One per section you sent, in the same order. | | result.segments[].id | string | Yes | Your id for the section, or the verse id, such as PSA.23.1, for a Bible reading. | | result.segments[].reference | string | No | For a Bible reading, the verse, such as "Psalm 23:1". | | result.segments[].text | string | No | For a Bible reading, the verse's text. | | result.segments[].startMs | integer | Yes | | | result.segments[].endMs | integer | Yes | | | result.segments[].clip | string | No | The clip's name, when clips were asked for. Fetch it from /v1/jobs/{id}/files/{name}. | | result.words | object[] | Yes | Every word of your text, in order, with when it is spoken. | | result.words[].text | string | Yes | | | result.words[].startMs | integer | Yes | | | result.words[].endMs | integer | Yes | | | error | object | No | Present when the job failed. | | error.code | string | No | A stable code to branch on, such as api_passage_unknown when a recording sent without text is not a Bible reading. Jobs that failed before October 2026 have none. | | error.problem | string | Yes | What went wrong. | | error.reason | string | Yes | Why it went wrong. | | error.fix | string | Yes | What would change the outcome. | “Required” is relative to the containing object. Optional success results and failure errors depend on the job state. Errors and limits ================= JSON errors contain error.code, error.problem, error.reason and error.fix. Handle errors and retries (/guides/errors) and check test and live limits (/start/limits). --- ## PATH: Guides > Server Relay (Source: guides/05-server-relay.md) Keep your key on your server ============================ tip Before your users reach the relay, check their session, cap the request size, validate the query, set a rate limit per user, and put a timeout on the call to Fellowverse. A key in an Android or iOS app, or in a web page, can be read by anyone who has the app. Call Fellowverse from your own server instead, and have your app call your server. Your server holds the key, decides who may search, and can cache popular results. A minimal relay in Node 18 or later, with no dependencies: import { createServer } from 'node:http'; const KEY = process.env.FELLOWVERSE_KEY; // fv_live_... or fv_test_... createServer(async (req, res) => { if (req.method !== 'POST' || req.url !== '/search') { res.writeHead(404).end(); return; } // Check your own user's session here before spending a request. let body = ''; for await (const chunk of req) body += chunk; const { query } = JSON.parse(body || '{}'); const upstream = await fetch('https://play.fellowverse.app/v1/search', { method: 'POST', headers: { authorization: `Bearer ${KEY}`, 'content-type': 'application/json' }, body: JSON.stringify({ query, limit: 10 }), }); res.writeHead(upstream.status, { 'content-type': 'application/json' }); res.end(await upstream.text()); }).listen(8080); Use one key per server or environment, named for it in the console ("android backend", "staging"), so you can see which one is in use and replace one without touching the others. --- ## PATH: Api > Clips (Source: api/06-clips.md) Fetch a clip an alignment made ============================== GET /v1/jobs/{id}/files/{name} Clips exist only for alignments made with clips set to true, once the job is done. Their names are given in the result, one per section. Clips are kept for 7 days after the job finishes (a day with a test key); then this returns 404 with the code api_file_expired. Authentication ============== Send Authorization: Bearer $KEY from your server. Create or rotate keys (/start/authentication). Example request =============== curl https://play.fellowverse.app/v1/jobs/job_Hk3vQ9xTmP2a/files/1.mp3 \ -H "Authorization: Bearer $KEY" --output verse-1.mp3 Example IDs are placeholders. Use the upload or job ID returned by your own request. Parameters ========== | Name | In | Required | Type | Description and constraints | | --- | --- | --- | --- | --- | | id | path | Yes | string | The job id returned when the alignment was started. | | name | path | Yes | string | The clip's name from the job result, such as 1.mp3. Pattern: ^\d{1,5}\.mp3$. | Responses ========= | Status | Meaning | | --- | --- | | 200 | The clip, as MP3. | | 400 | The request could not be used. invalid_request when a field is missing or has the wrong type; bad_request when the audio or a value could not be read. | | 401 | The key is missing, wrong, revoked, or past the end of its grace period after a roll. The code is api_unauthorized, and the reason says which. | | 403 | The account is suspended (api_suspended), or a live key was used on an account that has test keys only (api_live_locked). | | 404 | The upload, job or clip does not exist, or belongs to another account. The codes are api_upload_missing, api_job_missing and api_file_missing. api_off means the request went to an address that does not serve the API; use https://play.fellowverse.app. | | 500 | Something went wrong on our side. The code is internal. | 200: audio/mpeg =============== The response body is the MP3 clip. Save or stream it from your backend. Errors and limits ================= JSON errors contain error.code, error.problem, error.reason and error.fix. Handle errors and retries (/guides/errors) and check test and live limits (/start/limits). --- ## PATH: Guides > Errors (Source: guides/06-errors.md) Handle errors and retries ========================= { "error": { "code": "api_rate_limited", "problem": "...", "reason": "...", "fix": "..." } } Choose what to retry ==================== Every error has the same shape, and code is stable, so branch on it: | Status | Codes you will see | What to do | | --- | --- | --- | | 400 | bad_request | Fix the request; fix says how. Do not retry it as it is. | | 401 | api_unauthorized | Check the key your server sends. A rolled key past its grace period says when it expired; use the key that replaced it. | | 403 | api_suspended | Requests are paused on the account; contact Fellowverse to restore it. | | 404 | api_upload_missing, api_job_missing, api_file_expired | The id is wrong, belongs to another account, or its files have been removed. | | 429 | api_rate_limited, api_test_quota | Wait and retry. For api_test_quota, fix says when the day's allowance resets. | | 5xx | internal, api_stt_off | Retry after a short wait, doubling it each time. Typed search keeps working while voice search is unavailable. | A failed alignment job carries its code in error: api_audio_unreadable for a file that cannot be read, api_audio_too_long for a recording over the length an alignment takes, api_text_mismatch when the audio does not read the text, and api_passage_unknown for a recording sent without text that is not a Bible reading. These fail at once; send a different file or text rather than the same job again. problem, reason and fix are written to be read, so you can log them as they are or show problem and fix to your own team. Searches are safe to retry. Before retrying POST /v1/alignments after a timeout, check whether the first request made a job, so the same recording is not aligned twice. --- ## PATH: Api > Usage (Source: api/07-usage.md) See metered use over a period ============================= GET /v1/usage Returns your use by product, as billed. Without dates it covers the current month, in UTC, to now. Called with a test key, it returns the account's test use instead. Authentication ============== Send Authorization: Bearer $KEY from your server. Create or rotate keys (/start/authentication). Example request =============== curl 'https://play.fellowverse.app/v1/usage?from=2026-10-01&to=2026-11-01' \ -H "Authorization: Bearer $KEY" Example IDs are placeholders. Use the upload or job ID returned by your own request. Parameters ========== | Name | In | Required | Type | Description and constraints | | --- | --- | --- | --- | --- | | from | query | No | string | The start of the period, included. An ISO date or time. | | to | query | No | string | The end of the period, left out. An ISO date or time. Now when not given. | Responses ========= | Status | Meaning | | --- | --- | | 200 | Use in the period. | | 400 | The request could not be used. invalid_request when a field is missing or has the wrong type; bad_request when the audio or a value could not be read. | | 401 | The key is missing, wrong, revoked, or past the end of its grace period after a roll. The code is api_unauthorized, and the reason says which. | | 403 | The account is suspended (api_suspended), or a live key was used on an account that has test keys only (api_live_locked). | | 500 | Something went wrong on our side. The code is internal. | 200: application/json ===================== | Field | Type | Required | Description and constraints | | --- | --- | --- | --- | | from | string | Yes | Format: date-time. | | to | string | Yes | Format: date-time. | | livemode | boolean | Yes | True for a live key's billed use; false for a test key's free use. | | usage | object[] | Yes | | | usage[].product | string | Yes | One of search, voice_search, alignment. | | usage[].unit | string | Yes | One of request, audio_second. | | usage[].units | number | Yes | The billed amount, to two decimal places. | | usage[].events | integer | Yes | How many billable calls or jobs. | “Required” is relative to the containing object. Optional success results and failure errors depend on the job state. Errors and limits ================= JSON errors contain error.code, error.problem, error.reason and error.fix. Handle errors and retries (/guides/errors) and check test and live limits (/start/limits). --- ## PATH: Guides > Usage (Source: guides/07-usage.md) Track usage =========== GET /v1/usage?from=2026-10-01&to=2026-11-01 returns your metered use by product, as billed. Without dates it covers the current month to now. With a test key it returns the account's test use, and livemode is false. Test use is free. See the usage response fields (/api/usage). --- ## PATH: Api > Index (Source: api/index.md) API reference ============= Base URL: https://play.fellowverse.app. Every request uses a server-held bearer key. | Method | Endpoint | Purpose | | --- | --- | --- | | POST | /v1/search (/api/search) | Search by words or a reference | | POST | /v1/voice-search (/api/voice-search) | Search by a spoken request | | POST | /v1/uploads (/api/uploads) | Upload a recording to align | | POST | /v1/alignments (/api/alignments) | Align an upload to the text it reads | | GET | /v1/jobs/{id} (/api/jobs) | See a job's state and result | | GET | /v1/jobs/{id}/files/{name} (/api/clips) | Fetch a clip an alignment made | | GET | /v1/usage (/api/usage) | See metered use over a period | OpenAPI 3.1 =========== Browse the OpenAPI viewer (/api/openapi) or download the canonical YAML (/api/content/assets/openapi.yaml) or equivalent JSON (/api/content/assets/openapi.json). The viewer is read-only. It does not send requests or ask for your API key. New to the API? Start with the quickstart (/start/quickstart). For practical examples, read the integration guides (/guides). --- ## PATH: Api > Openapi (Source: api/08-openapi.json) # OpenAPI viewer

JSON over HTTPS, one key per request. Every error says what went wrong, why,
and what would change the outcome.

Scripture is the World English Bible, which is in the public domain.

Billing is metered: search per request, voice search and alignment per
second of audio. GET /v1/usage shows the same figures billing uses.

Make your keys in the developer console at
https://play.fellowverse.app/developers. Every account starts with free
test keys, so you can try everything before anything is billed.

Get started

  • Sign in to the developer console with your email and name your account.
  • Under Keys, make a test key and copy it: it is shown once.
  • Search:
  • curl https://play.fellowverse.app/v1/search \
         -H "Authorization: Bearer $KEY" -H 'content-type: application/json' \
         -d '{"query": "rest for the weary", "limit": 3}'
  • See what you used with GET /v1/usage, or under Use in the console.
  • Test keys call the same API as live keys, so the code you write now is the
    code you ship.

    Keep your key on your server

    A key in an Android or iOS app, or in a web page, can be read by anyone who
    has the app. Call Fellowverse from your own server and have your app call
    your server: it holds the key, decides who may search, and can cache
    popular results. Use one key per server or environment, named for it, so
    you can replace one without touching the others.

    Add voice search to an app

    Record the request in the app, send it to your server, and pass the audio
    to POST /v1/voice-search as the body with its type. On Android,
    MediaRecorder with MPEG-4 and AAC gives audio/mp4; in a browser,
    MediaRecorder gives audio/webm or audio/mp4. Show the transcript
    with the results, so people see what was heard; an empty one means nothing
    could be heard.

    Play a recording verse by verse

  • Upload the recording (POST /v1/uploads) and start an alignment
  • (POST /v1/alignments). For a Bible reading, leave out segments and the passage and its verses are found for you.
  • Poll GET /v1/jobs/{id} every few seconds until it is done or failed.
  • Store the result with the recording. Each segment's startMs and
  • endMs let your player seek to a verse; words times each word.
  • If you asked for clips, download them with your key and host them
  • yourself. They are there for 7 days after the job finishes.

    Align each recording once, when it is published, and serve the timings
    from your own storage, so listeners never wait on an alignment.

    Handle errors and limits

    Every error has a stable code to branch on, and problem, reason and
    fix written to be read. Fix a 400 before sending it again. Retry a
    429 after a short wait (for api_test_quota, fix says when the day's
    allowance resets) and a 5xx after a wait that doubles each time.
    Searches are safe to retry; before retrying POST /v1/alignments after a
    timeout, check whether the first request made a job. A failed job's
    error.code says why it failed, such as api_text_mismatch or
    api_passage_unknown, and it fails at once rather than being retried.

    Go live

  • The account's owner chooses Request live access in the console. We
  • get in touch to set up billing and turn on live keys.
  • Make a live key under Keys. It takes a code we email you.
  • Put it on your server in place of the test key. Nothing else changes.
  • To replace a key, roll it and give the old one up to a week of grace
  • while you deploy the new one.

    Every live key made, rolled or revoked is emailed to the account's owners,
    and the console's Activity shows who did what and when.

    Version: 1.0.0 Base URL: https://play.fellowverse.app ## Authentication - **apiKey**: http (bearer) Send your key as `Authorization: Bearer fv_live_...`. A key is shown once, when it is made. Keep it on your server, not in a web page or an app. Lost keys are revoked and replaced, not recovered. Test keys (`fv_test_...`) call the same API for free, with a daily allowance per account and alignments of up to 10 minutes. Rolling a key replaces it and lets the old one keep working for up to a week while you switch over. ## Search

    Passages by meaning or by reference, typed or spoken.

    ### POST /v1/search Search by words or a reference

    Takes a few words, a feeling or a reference, and returns passages of two
    to six verses each, with a context line saying where they sit and who is
    speaking.

    A reference such as "Psalm 23" or "John 3:16" returns that passage and the
    rest of its chapter, with kind set to reference. Anything else is a
    search by meaning and wording, with kind set to search.

    Billed per request. Limited to 120 a minute per key.

    **Request Body:** Content-Type: application/json **Responses:** - 200: The passages found, best first. - 400: The request could not be used. `invalid_request` when a field is missing or has the wrong type; `bad_request` when the audio or a value could not be read. - 401: The key is missing, wrong, revoked, or past the end of its grace period after a roll. The code is `api_unauthorized`, and the reason says which. - 403: The account is suspended (`api_suspended`), or a live key was used on an account that has test keys only (`api_live_locked`). - 429: A per-key limit for the minute was reached (`api_rate_limited`; the reason names the limit), or a test key used up its account's daily allowance (`api_test_quota`; the fix says when it resets). - 500: Something went wrong on our side. The code is `internal`. ### POST /v1/voice-search Search by a spoken request

    Takes a spoken request of up to 30 seconds and 10 MB as the request
    body, with its type, and returns what was heard and the same results as a search. The
    audio is not kept.

    When nothing could be heard, the response holds an empty transcript
    and no results.

    Billed per second of audio, at least one second. Limited to 60 a minute
    per key.

    **Parameters:** - `limit` [query]: integer How many passages to return. **Request Body:** Content-Type: audio/mpeg The recording itself as the body, with its type in Content-Type. MP3, WAV, M4A, OGG, WebM and FLAC are read. **Responses:** - 200: What was heard, and the passages found for it. - 400: The request could not be used. `invalid_request` when a field is missing or has the wrong type; `bad_request` when the audio or a value could not be read. - 401: The key is missing, wrong, revoked, or past the end of its grace period after a roll. The code is `api_unauthorized`, and the reason says which. - 403: The account is suspended (`api_suspended`), or a live key was used on an account that has test keys only (`api_live_locked`). - 413: The body is over the limit: 200 MB for an upload, 10 MB for voice search. The code is `request_error`. - 415: The Content-Type is not one of the audio types read. The code is `request_error`. - 429: A per-key limit for the minute was reached (`api_rate_limited`; the reason names the limit), or a test key used up its account's daily allowance (`api_test_quota`; the fix says when it resets). - 500: Something went wrong on our side. The code is `internal`. - 503: Voice search cannot be served right now; typed search keeps working. The code is `api_stt_off`. ## Alignment

    When each section and word of a text is spoken in a recording.

    ### POST /v1/uploads Upload a recording to align

    Takes a recording of up to 200 MB as the request body, with its type.
    One alignment takes up to an hour of audio, or 10 minutes with a test key.
    Uploads are kept for 7 days (a day with a test key), long enough to align
    them; this is not a storage service.

    Limited to 30 a minute per key.

    **Request Body:** Content-Type: audio/mpeg The recording itself as the body, with its type in Content-Type. MP3, WAV, M4A, OGG, WebM and FLAC are read. **Responses:** - 201: The upload was kept. Use its id to start an alignment. - 400: The request could not be used. `invalid_request` when a field is missing or has the wrong type; `bad_request` when the audio or a value could not be read. - 401: The key is missing, wrong, revoked, or past the end of its grace period after a roll. The code is `api_unauthorized`, and the reason says which. - 403: The account is suspended (`api_suspended`), or a live key was used on an account that has test keys only (`api_live_locked`). - 413: The body is over the limit: 200 MB for an upload, 10 MB for voice search. The code is `request_error`. - 415: The Content-Type is not one of the audio types read. The code is `request_error`. - 429: A per-key limit for the minute was reached (`api_rate_limited`; the reason names the limit), or a test key used up its account's daily allowance (`api_test_quota`; the fix says when it resets). - 500: Something went wrong on our side. The code is `internal`. ### POST /v1/alignments Align an upload to the text it reads

    Starts aligning an upload to its text, given as sections with your own
    ids: paragraphs, sentences, verses or lines. The job runs in the
    background; poll GET /v1/jobs/{id} for the result.

    Announcements and closing lines set apart by a pause are found and left
    out of the sections. Wording that differs slightly from your text is
    tolerated.

    Leave out segments for a Bible reading: the passage is identified from
    what is said, from any verse to any verse and across chapters, and its
    verses become the sections. The result then holds passage. A recording
    that is not a reading, or only quotes a verse, fails with the code
    api_passage_unknown. Only the World English Bible is held, so a reading
    of another translation is found only when enough of its wording agrees.

    Billed per second of audio when the job is done. Limited to 30 a minute
    per key.

    **Request Body:** Content-Type: application/json **Responses:** - 202: The job was queued. - 400: The request could not be used. `invalid_request` when a field is missing or has the wrong type; `bad_request` when the audio or a value could not be read. - 401: The key is missing, wrong, revoked, or past the end of its grace period after a roll. The code is `api_unauthorized`, and the reason says which. - 403: The account is suspended (`api_suspended`), or a live key was used on an account that has test keys only (`api_live_locked`). - 404: The upload, job or clip does not exist, or belongs to another account. The codes are `api_upload_missing`, `api_job_missing` and `api_file_missing`. `api_off` means the request went to an address that does not serve the API; use https://play.fellowverse.app. - 429: A per-key limit for the minute was reached (`api_rate_limited`; the reason names the limit), or a test key used up its account's daily allowance (`api_test_quota`; the fix says when it resets). - 500: Something went wrong on our side. The code is `internal`. ### GET /v1/jobs/{id} See a job's state and result

    Poll until status is done or failed. A done job holds its
    result; a failed one holds its error.

    A job that fails for a reason in the request (an unreadable file, text
    that does not match) fails at once with that reason. One that fails on
    our side is tried again, up to three times, and goes back to queued
    in between. Jobs of a suspended account wait in queued until it is
    restored.

    **Parameters:** - `id` [path]: string (required) The job id returned when the alignment was started. **Responses:** - 200: The job. - 401: The key is missing, wrong, revoked, or past the end of its grace period after a roll. The code is `api_unauthorized`, and the reason says which. - 403: The account is suspended (`api_suspended`), or a live key was used on an account that has test keys only (`api_live_locked`). - 404: The upload, job or clip does not exist, or belongs to another account. The codes are `api_upload_missing`, `api_job_missing` and `api_file_missing`. `api_off` means the request went to an address that does not serve the API; use https://play.fellowverse.app. - 500: Something went wrong on our side. The code is `internal`. ### GET /v1/jobs/{id}/files/{name} Fetch a clip an alignment made

    Clips exist only for alignments made with clips set to true, once the
    job is done. Their names are given in the result, one per section. Clips
    are kept for 7 days after the job finishes (a day with a test key); then
    this returns 404 with
    the code api_file_expired.

    **Parameters:** - `id` [path]: string (required) The job id returned when the alignment was started. - `name` [path]: string (required) The clip's name from the job result, such as `1.mp3`. **Responses:** - 200: The clip, as MP3. - 400: The request could not be used. `invalid_request` when a field is missing or has the wrong type; `bad_request` when the audio or a value could not be read. - 401: The key is missing, wrong, revoked, or past the end of its grace period after a roll. The code is `api_unauthorized`, and the reason says which. - 403: The account is suspended (`api_suspended`), or a live key was used on an account that has test keys only (`api_live_locked`). - 404: The upload, job or clip does not exist, or belongs to another account. The codes are `api_upload_missing`, `api_job_missing` and `api_file_missing`. `api_off` means the request went to an address that does not serve the API; use https://play.fellowverse.app. - 500: Something went wrong on our side. The code is `internal`. ## Account

    Metered use, as billed.

    ### GET /v1/usage See metered use over a period

    Returns your use by product, as billed. Without dates it covers the
    current month, in UTC, to now. Called with a test key, it returns the
    account's test use instead.

    **Parameters:** - `from` [query]: string The start of the period, included. An ISO date or time. - `to` [query]: string The end of the period, left out. An ISO date or time. Now when not given. **Responses:** - 200: Use in the period. - 400: The request could not be used. `invalid_request` when a field is missing or has the wrong type; `bad_request` when the audio or a value could not be read. - 401: The key is missing, wrong, revoked, or past the end of its grace period after a roll. The code is `api_unauthorized`, and the reason says which. - 403: The account is suspended (`api_suspended`), or a live key was used on an account that has test keys only (`api_live_locked`). - 500: Something went wrong on our side. The code is `internal`. --- ## PATH: Concepts > Index (Source: concepts/index.md) Concepts ======== The guides show the requests. These pages show the thinking behind them, and how it carries over to recordings well beyond the Bible. - Aligning real recordings (/concepts/real-recordings) Introductions, sign-offs and small slips are handled, so any recording of a known text can be timed to the word. - Record once, cut many ways (/concepts/record-once) One continuous reading becomes sections, verses, words and clips that keep its rhythm. - Pacing and background sound (/concepts/pacing-and-sound) The same recordings played for work, a run, the gym or rest, with background sound mixed into one stream. - Agents that quote scripture (/concepts/agents) A tool that hands an assistant or voice agent the exact words, so it quotes scripture instead of recalling it. --- ## PATH: Guides > Index (Source: guides/index.md) Integration guides ================== Choose the part you want to add to your app. Calls to Fellowverse go through your backend so your API key stays private. - Search scripture (/guides/search) Match a reference, words or a feeling. - Add voice search (/guides/voice-search) Show the transcript with the passages. - Align recordings (/guides/alignment) Supply text or identify a Bible reading automatically. - Build a read-along player (/guides/read-along) Seek by section and highlight by word. - Keep your key on your server (/guides/server-relay) Connect web and mobile clients through a backend. - Handle errors and retries (/guides/errors) Branch on stable error codes. - Track usage (/guides/usage) See your account's test or live use. --- ## PATH: Home (Source: home.md)
    FELLOWVERSE / PUBLIC API v1
    Scripture by meaning.
    Audio to the word. =========================================== Ask for a feeling, a few words or a reference, typed or spoken, and get back passages of two to six verses, each with a line saying where it sits and who is speaking. Send a recording, with or without its text, and get the time of every verse and word.
    A few words to your first passage ================================= Create a free test key in the developer console (https://play.fellowverse.app/developers), then call the API from your server. export KEY=fv_test_... curl https://play.fellowverse.app/v1/search \ -H "Authorization: Bearer $KEY" \ -H 'Content-Type: application/json' \ -d '{"query":"rest for the weary","limit":3}' - Search by text or reference (/guides/search) Find a thought or go straight to a verse in the World English Bible. - Search by voice (/guides/voice-search) Send a short spoken request and receive its transcript with matching passages. - Align a recording (/guides/alignment) Get section and word timings, with optional MP3 clips. - Build a read-along player (/guides/read-along) Use the timings to seek to a verse and highlight words in your own app. How the audio works =================== One continuous recording, timed to the word and cut cleanly, whatever it contains. Read aligning real recordings (/concepts/real-recordings), record once, cut many ways (/concepts/record-once) and pacing and background sound (/concepts/pacing-and-sound). For agents and assistants ========================= Give your assistant or voice agent a tool that returns the exact words (/concepts/agents), so it quotes scripture instead of recalling it. Your coding assistant can read these docs through llms-full.txt (/llms-full.txt) or this site's MCP server. Built for your server ===================== One bearer key per request. JSON over HTTPS. Use the same requests with test and live keys. Keep your key in your backend, including when you build for iOS, Android or the web. - Authentication and key rotation (/start/authentication) - Test allowances and live limits (/start/limits) - Errors and retries (/guides/errors) - Request live access (/start/go-live) Read it your way ================ Browse the endpoint reference (/api), download the OpenAPI YAML (/api/content/assets/openapi.yaml) or JSON (/api/content/assets/openapi.json), or give your development assistant the documentation index (/llms-index.txt) and full documentation (/llms-full.txt). Scripture text is the World English Bible (public domain). Alignment also accepts your own text, divided into sections with your own IDs. --- ## PATH: Start > Index (Source: start/index.md) Start here ========== The API base URL is https://play.fellowverse.app. Every endpoint uses a bearer key held on your server. Your first integration ====================== Make a test request =================== Follow the quickstart (/start/quickstart) to create a free test key and search for a passage. Connect your backend ==================== Read authentication (/start/authentication) and the server relay guide (/guides/server-relay). Check the limits ================ Test accounts have daily allowances (/start/limits). Live keys have separate per-minute limits. Move to live access =================== The account owner can request live access (/start/go-live) in the developer console. --- --- END OF DOCUMENTATION CONTEXT Total documents: 28 Generated at: 2026-11-02T07:45:57.720Z