Menu
SermonIndex · Developer API

Programming Guide.

Worked examples for the things people actually build with this: showing preaching alongside a Bible passage, a topical shelf, a preacher page, and a full local copy.

sermons preachers topics scripture references

Preaching on the passage a reader is looking at

This is the commonest case, and the one the API is shaped around. If your page already knows which passage it is showing, you know the OSIS book code, the chapter and the verse — which is the whole request.

// John 3:16 const res = await fetch('https://api.sermonindex.net/v2/scripture/JHN/3/16'); if (res.ok) { const data = await res.json(); console.log(data.reference); // "John 3:16" console.log(data.sermonCount); // how many sermons open it for (const s of data.sermons.slice(0, 5)) { console.log(s.title, '—', s.speaker, s.duration); } }

Render a card from what comes back and you need no second call — the portrait, the summary, the audio link and the canonical page URL are all in the entry:

<a href="${s.url}" class="sermon-card"> <img src="${s.speakerImage}" alt="${s.speaker}"> <h3>${s.title}</h3> <p>${s.speaker} · ${s.duration}</p> <p>${s.summary}</p> <audio controls src="${s.mp3Url}"></audio> <!-- see fallback below --> </a>

A verse with no preaching returns 404. That is deliberate — it keeps us from storing thirty-one thousand empty files. Either check res.ok, or fetch the chapter first: its verses array tells you exactly which verses have anything, and how much.

Audio: Archive.org first, our CDN behind it

mp3Url and mp4Url give you the Archive.org mirror wherever one exists, and our own CDN where it does not. About 85% of the sermons that carry audio are mirrored there. cdnMp3Url and cdnMp4Url always hold our copy, and archiveAudioUrl / archiveVideoUrl always hold the Archive one, so you can be explicit whenever you need to be.

Play from mp3Url and fall back to cdnMp3Url on error. That order keeps this library free to run — Archive.org carries the bandwidth, and our CDN catches what it misses.

Two <source> elements will not fail over for you. A browser chooses between sources by MIME type, before it requests anything — if the first URL 404s or times out, it does not try the second. The audio simply never starts. Failing over needs an error handler:

// archive first, our CDN if it does not answer const audio = new Audio(s.mp3Url); audio.addEventListener('error', function () { if (s.cdnMp3Url && audio.src !== s.cdnMp3Url) { audio.src = s.cdnMp3Url; // one retry, no loop audio.play(); } }, { once: true });

A sermon with no audio at all — a text-only entry, and there are a good many — carries an empty string in every one of these fields. Check hasAudio before you render a player.

Widening the search when a verse comes up empty

Most preaching is filed against a passage rather than a single verse, so falling back a level usually finds something.

async function sermonsFor(book, chapter, verse) { const base = 'https://api.sermonindex.net/v2/scripture'; // try the exact verse let r = await fetch(`${base}/${book}/${chapter}/${verse}`); if (r.ok) return (await r.json()).sermons; // fall back to the chapter r = await fetch(`${base}/${book}/${chapter}`); if (r.ok) return (await r.json()).sermons; // finally the whole book r = await fetch(`${base}/${book}`); return r.ok ? [] : []; // book level gives counts, not sermons }

Drawing a chapter heat map

The chapter response includes a per-verse breakdown, which is enough to shade a passage by how heavily it has been preached.

const ch = await (await fetch('https://api.sermonindex.net/v2/scripture/ROM/8')).json(); const max = Math.max(...ch.verses.map(v => v.sermonCount)); for (const v of ch.verses) { const weight = v.sermonCount / max; // 0 → 1 paintVerse(v.verse, weight); }

A topical shelf

const t = await (await fetch('https://api.sermonindex.net/v2/topics/revival')).json(); t.description // a written introduction to the theme t.topScriptures // passages most preached under it, with BSB text t.sermons // every sermon filed there

To offer a browsable list of themes, fetch /v2/topics once and keep it. It is a few megabytes and it changes rarely.

A preacher page

const p = await (await fetch('https://api.sermonindex.net/v2/speakers/leonard-ravenhill')).json(); p.name, p.bio, p.image // portrait is an absolute URL p.topTopics // what he is most heard on p.sermons // all of them

Walking the whole library

Please do not fetch /v2/sermons in a loop. Either page through it, or — better for anything more than a one-off — take a bulk download and work locally.

#!/usr/bin/env python3 import requests page, out = 1, [] while True: r = requests.get(f'https://api.sermonindex.net/v2/sermons/page/{page}') if not r.ok: break d = r.json() out.extend(d['sermons']) if page >= d['totalPages']: break page += 1 print(len(out), 'sermons')

Bible text from the same host

If you are showing the passage as well as the preaching, you can take both from us and keep one dependency instead of two.

const c = await (await fetch('https://api.sermonindex.net/v2/bible/BSB/JHN/3.json')).json(); c.chapter.content // verses, headings and footnotes

The structure matches the Free Use Bible API, whose work this corpus comes from, so existing code needs only a new base URL.

Please cache

The library is rebuilt periodically, not continuously, so there is nothing to gain from fetching the same path twice in a day. Poll /v2/stats if you want to know whether anything has moved — it carries the timestamp of the last rebuild and costs almost nothing to read.

We have no rate limit and no intention of adding one. That works only as long as people are reasonable with it, and so far they have been.

Book codes

Scripture endpoints use OSIS book codes — GEN, JHN, 1CO, REV. The full table lists all sixty-six with the aliases we accept.

Everything we make is available for free because of a generous community of supporters.

Donate