{"_id":"@americanbible/api-bible-sdk","_rev":"2-56911363947509805be3f1b9115ecad3","name":"@americanbible/api-bible-sdk","dist-tags":{"latest":"1.1.0"},"versions":{"1.0.0":{"name":"@americanbible/api-bible-sdk","version":"1.0.0","keywords":["api.bible","bible","scripture","sdk","typescript","esm","cjs"],"author":{"name":"American Bible Society"},"license":"MIT","_id":"@americanbible/api-bible-sdk@1.0.0","maintainers":[{"name":"bryceallison","email":"ballison@americanbible.org"},{"name":"hanatgit","email":"han@hanat.work"},{"name":"johnmitchell","email":"jmitchell@americanbible.org"},{"name":"chuck-abs","email":"cbarker@americanbible.org"},{"name":"aburroughs97","email":"alexburroughs1@gmail.com"}],"homepage":"https://github.com/americanbible/api-bible-sdks/tree/main/packages/typescript#readme","bugs":{"url":"https://github.com/americanbible/api-bible-sdks/issues"},"dist":{"shasum":"3caced85f48f5af25bb3a24b09d3d20c90df5057","tarball":"https://registry.npmjs.org/@americanbible/api-bible-sdk/-/api-bible-sdk-1.0.0.tgz","fileCount":10,"integrity":"sha512-jJUORFSaDQp8nOxY9ojkf1e18fLQJW/Y8GSCDk/niBREsY0fQ//8Jj8C9NuYhHVYonZQ0TwWyKoamEmwxIHl+g==","signatures":[{"sig":"MEUCIQC12iag41LITU/g0YaSyhXfW8M2PRvaB9lEwr6Lq14ztAIgJLWSOQWACKppGBVuyCjSFLhzXmTJxNXY9pbb/2ff8Co=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":538747},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=20"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"4fc353fb571ec09cf3fe7a598c73f601636b64b4","scripts":{"test":"vitest run","build":"tsdown","example":"tsx examples/basic-usage.ts","prebuild":"npm run sync-version","typecheck":"tsc --noEmit","sync-version":"node scripts/sync-version.mjs","check-version":"node scripts/sync-version.mjs --check","test:coverage":"vitest run --coverage","record-fixtures":"tsx scripts/record-fixtures.ts","test:contract:live":"vitest run tests/contract/contract.live.test.ts"},"_npmUser":{"name":"aburroughs97","email":"alexburroughs1@gmail.com"},"repository":{"url":"git+https://github.com/americanbible/api-bible-sdks.git","type":"git","directory":"packages/typescript"},"_npmVersion":"10.9.3","description":"TypeScript SDK for api.bible","directories":{},"sideEffects":false,"_nodeVersion":"22.19.0","dependencies":{"zod":"^3.22.4"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.21.0","dotenv":"^17.4.2","tsdown":"~0.21.10","vitest":"^1.4.0","typescript":"^5.4.0","@types/node":"^20.19.41","@vitest/coverage-v8":"^1.6.1"},"_npmOperationalInternal":{"tmp":"tmp/api-bible-sdk_1.0.0_1782495854766_0.3192964149964086","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@americanbible/api-bible-sdk","version":"1.1.0","description":"TypeScript SDK for api.bible","license":"MIT","author":{"name":"American Bible Society"},"repository":{"type":"git","url":"git+https://github.com/americanbible/api-bible-sdks.git","directory":"packages/typescript"},"bugs":{"url":"https://github.com/americanbible/api-bible-sdks/issues"},"homepage":"https://github.com/americanbible/api-bible-sdks/tree/main/packages/typescript#readme","publishConfig":{"access":"public"},"keywords":["api.bible","bible","scripture","sdk","typescript","esm","cjs"],"type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"sideEffects":false,"scripts":{"typecheck":"tsc --noEmit","test":"vitest run","test:coverage":"vitest run --coverage","test:contract:live":"vitest run tests/contract/contract.live.test.ts","record-fixtures":"tsx scripts/record-fixtures.ts","sync-version":"node scripts/sync-version.mjs","check-version":"node scripts/sync-version.mjs --check","prebuild":"npm run sync-version","build":"tsdown","example":"tsx examples/basic-usage.ts"},"engines":{"node":">=20"},"dependencies":{"zod":"^3.22.4"},"devDependencies":{"@types/node":"^20.19.41","@vitest/coverage-v8":"^1.6.1","dotenv":"^17.4.2","tsdown":"~0.21.10","tsx":"^4.21.0","typescript":"^5.4.0","vitest":"^1.4.0"},"_id":"@americanbible/api-bible-sdk@1.1.0","gitHead":"8f1303809e589e4ff6a0739bc26bc3a70acba5b8","_nodeVersion":"22.19.0","_npmVersion":"10.9.3","dist":{"integrity":"sha512-0vzOGqu4BEMccgO7Hb1IqcYJ1snM6j+FrYYVACLyXGPuS3mIqA0H7Zs0ZU+61gLY/iFchkhtfvsUAIYu94dTag==","shasum":"47c2471dc5c587a38ecf4dfac7e8e8c31fdcdb6a","tarball":"https://registry.npmjs.org/@americanbible/api-bible-sdk/-/api-bible-sdk-1.1.0.tgz","fileCount":10,"unpackedSize":540081,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIBU/a2sKGlcb1q2rLqDb1Pcds/rJ/OdroNeQ3RKM6fChAiEAg1Z+FbiYrV8x/eSNeWipTmyzL37wQIDUjjQ+mX1biUw="}]},"_npmUser":{"name":"aburroughs97","email":"alexburroughs1@gmail.com"},"directories":{},"maintainers":[{"name":"bryceallison","email":"ballison@americanbible.org"},{"name":"hanatgit","email":"han@hanat.work"},{"name":"johnmitchell","email":"jmitchell@americanbible.org"},{"name":"chuck-abs","email":"cbarker@americanbible.org"},{"name":"aburroughs97","email":"alexburroughs1@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/api-bible-sdk_1.1.0_1783018887511_0.5222015095995864"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-26T17:44:14.636Z","modified":"2026-07-02T19:01:28.125Z","1.0.0":"2026-06-26T17:44:14.988Z","1.1.0":"2026-07-02T19:01:27.662Z"},"bugs":{"url":"https://github.com/americanbible/api-bible-sdks/issues"},"author":{"name":"American Bible Society"},"license":"MIT","homepage":"https://github.com/americanbible/api-bible-sdks/tree/main/packages/typescript#readme","keywords":["api.bible","bible","scripture","sdk","typescript","esm","cjs"],"repository":{"type":"git","url":"git+https://github.com/americanbible/api-bible-sdks.git","directory":"packages/typescript"},"description":"TypeScript SDK for api.bible","maintainers":[{"name":"bryceallison","email":"ballison@americanbible.org"},{"name":"hanatgit","email":"han@hanat.work"},{"name":"johnmitchell","email":"jmitchell@americanbible.org"},{"name":"chuck-abs","email":"cbarker@americanbible.org"},{"name":"aburroughs97","email":"alexburroughs1@gmail.com"}],"readme":"# api-bible-sdk\r\n\r\nA TypeScript SDK for the [API.Bible](https://api.bible/) REST API — type-safe access to Bibles, books, chapters, verses, passages, audio content, and search.\r\n\r\n![Node.js ≥20](https://img.shields.io/badge/node-%3E%3D20-brightgreen)\r\n![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue)\r\n![ESM](https://img.shields.io/badge/module-ESM-orange)\r\n\r\n---\r\n\r\n## Table of Contents\r\n\r\n- [Installation](#installation)\r\n- [Authentication](#authentication)\r\n- [Quickstart](#quickstart)\r\n- [Key Features](#key-features)\r\n- [Configuration](#configuration)\r\n- [API Reference](#api-reference)\r\n- [Content Parameters](#content-parameters)\r\n- [Error Handling](#error-handling)\r\n- [Advanced Usage](#advanced-usage)\r\n- [Development](#development)\r\n- [Contributing](#contributing)\r\n\r\n---\r\n\r\n## Installation\r\n\r\n```bash\r\nnpm install @americanbible/api-bible-sdk\r\n```\r\n\r\n> **Requires Node.js ≥20.** The SDK relies on the native `fetch` API and `Headers` global. Node 18 is end-of-life as of April 2025; consumers on EOL runtimes should upgrade before adopting this SDK.\r\n\r\n> **ESM and CommonJS.** This package ships both builds with full TypeScript types for each.\r\n> `import` resolves to the ESM build, `require()` to the CommonJS build — so it works in\r\n> Node 20+ (with or without `\"type\": \"module\"`), bundlers (Vite, Webpack 5+, esbuild, Rollup),\r\n> and test runners including Jest's default CJS transformer and Vitest.\r\n\r\n---\r\n\r\n## Authentication\r\n\r\n1. Sign up for a free API key at [api.bible](https://api.bible/).\r\n2. Pass the key as `apiKey` when creating the client.\r\n\r\nThe key is sent as an `api-key` header on every request. Never hard-code keys in source files — use an environment variable instead:\r\n\r\n```bash\r\nexport BIBLE_API_KEY=\"your-api-key-here\"\r\n```\r\n\r\n---\r\n\r\n## Quickstart\r\n\r\n```typescript\r\nimport { createBibleClient } from \"@americanbible/api-bible-sdk\";\r\n\r\nconst client = createBibleClient({\r\n  apiKey: process.env.BIBLE_API_KEY!,\r\n});\r\n\r\n// List all available Bibles\r\nconst { data: bibles } = await client.bibles.list();\r\nconsole.log(bibles.map((b) => `${b.id} — ${b.name}`));\r\n\r\n// Get a specific chapter (KJV Genesis 1)\r\nconst { data: chapter } = await client.chapters.get(\r\n  \"de4e12af7f28f599-02\", // Bible ID\r\n  \"GEN.1\", // Chapter ID\r\n  { contentType: \"text\", includeVerseNumbers: true },\r\n);\r\nconsole.log(chapter.content);\r\n```\r\n\r\n---\r\n\r\n## Key Features\r\n\r\n- **Full TypeScript support** — all responses are runtime-validated with [Zod](https://zod.dev/) and exposed as precise TypeScript types.\r\n- **Complete API coverage** — `bibles`, `books`, `chapters`, `verses`, `passages`, `sections`, `audioBibles`, and `search` resources.\r\n- **Automatic retry** — exponential backoff with full jitter on `429` and `5xx` responses, plus transport failures and timeouts; respects the `Retry-After` header.\r\n- **Request cancellation** — every method accepts an optional `AbortSignal`.\r\n- **Configurable timeout** — global timeout (default 10 s) enforced via `AbortController`.\r\n- **Flexible content types** — retrieve content as `html`, `json`, or plain `text`.\r\n- **Custom fetch injection** — swap in your own `fetch` implementation for testing or edge runtimes.\r\n- **ESM-only, minimal dependencies** — zero runtime dependencies beyond Zod.\r\n\r\n---\r\n\r\n## Configuration\r\n\r\n### `createBibleClient(config)`\r\n\r\n| Option             | Type                      | Default                     | Description                                                                                                  |\r\n| ------------------ | ------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------ |\r\n| `apiKey`           | `string`                  | —                           | **Required.** Your API.Bible key.                                                                            |\r\n| `baseUrl`          | `string`                  | `https://rest.api.bible/v1` | Override the API base URL. Must be `https://` except for loopback hosts (`localhost` / `127.0.0.1` / `[::1]`). |\r\n| `timeout`          | `number`                  | `10000`                     | **Per-attempt** request timeout in ms — not an end-to-end deadline. See [Timeouts are per-attempt](#timeouts-are-per-attempt). |\r\n| `retry`            | `RetryConfig`             | see below                   | Retry strategy configuration.                                                                                |\r\n| `maxResponseBytes` | `number`                  | `10485760` (10 MiB)         | Hard ceiling on a single response body. See [Response size and validation](#response-size-and-validation).   |\r\n| `headers`          | `Record<string, string>`  | —                           | Extra headers sent on every request (e.g. tracing IDs). The `api-key` header is always applied last and cannot be overridden. |\r\n| `fetch`            | `typeof globalThis.fetch` | `globalThis.fetch`          | Custom fetch implementation.                                                                                 |\r\n| `onResponse`       | `ResponseObserver`        | —                           | Observability hook fired per HTTP response received (each attempt, including error statuses). Carries `status`, `headers`, `attempt`, `url`, and `durationMs` (per-attempt latency). See [Observability](#observability). |\r\n| `onRetry`          | `RetryObserver`           | —                           | Observability hook fired on each retryable failure — including timeouts and network errors, which carry no HTTP response and so are invisible to `onResponse`. Carries `durationMs` for the failed attempt. |\r\n\r\n### `RetryConfig`\r\n\r\n| Option         | Default       | Description                                        |\r\n| -------------- | ------------- | -------------------------------------------------- |\r\n| `maxAttempts`  | `3`           | Total number of attempts (initial + retries).      |\r\n| `baseDelayMs`  | `500`         | Initial backoff delay in milliseconds.             |\r\n| `maxDelayMs`   | `30000`       | Maximum backoff delay in milliseconds.             |\r\n| `maxElapsedMs` | `60000`       | Total wall-clock budget (ms) across **all** attempts. Before each backoff sleep, if waiting would exceed it the SDK gives up instead. Bounds retry *scheduling*, not a single in-flight request. Pass `null` to disable. |\r\n| `jitter`       | `Math.random` | Function returning a value in `[0, 1)` for jitter. |\r\n\r\nRetry uses the formula `floor(jitter() * min(baseDelayMs * 2^attempt, maxDelayMs))`, with the server's `Retry-After` header value used as a floor when present. If the server's `Retry-After` exceeds `maxDelayMs`, the SDK does **not** retry — it throws `RateLimitError` / `ServerError` immediately so you can decide how to handle a wait longer than you've allowed, rather than silently consuming the retry budget.\r\n\r\n### Timeouts are per-attempt\r\n\r\n`timeout` bounds a **single HTTP attempt**, not the whole operation. With the default `retry.maxAttempts: 3`, a request that keeps timing out can take up to roughly `3 × timeout` plus the backoff sleeps between attempts before it finally throws.\r\n\r\n`retry.maxElapsedMs` (default 60s) caps the total wall-clock spent across attempts: before each backoff sleep the SDK checks the budget and gives up rather than sleep past it — so a long `Retry-After` during a rate-limit storm surfaces the error promptly instead of parking the caller for minutes. Note it bounds retry *scheduling*, not an in-flight request, so it is not a hard deadline. If you need a hard end-to-end deadline that also aborts the in-flight request, pass an `AbortSignal` and abort it on your own timer (see [Request cancellation](#request-cancellation-with-abortsignal)) — aborting takes priority over the internal timeout and stops retrying immediately.\r\n\r\n### Response size and validation\r\n\r\nEvery response is buffered fully into memory and validated against a [Zod](https://zod.dev/) schema before it is returned. Two things are worth knowing for high-throughput or large-payload workloads:\r\n\r\n- **Size cap.** Bodies larger than `maxResponseBytes` (default 10 MiB) are rejected with an `ApiError` before the whole payload is buffered. Raise it if you legitimately fetch larger responses — e.g. a whole Bible with chapter content inline.\r\n- **Validation is synchronous.** Parsing and validating a multi-megabyte response holds several copies of it in memory and runs on the event loop for the duration. This is negligible for typical payloads, but if you fetch very large bodies under high concurrency, budget for the CPU and memory cost.\r\n\r\n### Observability\r\n\r\nThe SDK emits no logs of its own. Instead it exposes two side-channel hooks so you can wire requests into whatever metrics/logging backend you use. `onResponse` fires once per HTTP response (every attempt, including error statuses); `onRetry` fires on every retryable failure — network errors, timeouts, `429`, `5xx` — including the final give-up. Both payloads carry `durationMs`, the monotonic time the attempt took, so you can build latency percentiles (P50/P95/P99) without wrapping every call site:\r\n\r\n```typescript\r\nimport { createBibleClient, type ResponseMeta } from '@americanbible/api-bible-sdk';\r\n\r\nconst latencies: number[] = [];\r\n\r\nconst client = createBibleClient({\r\n  apiKey: process.env.BIBLE_API_KEY!,\r\n  onResponse: (meta: ResponseMeta) => {\r\n    latencies.push(meta.durationMs);\r\n    // or record straight into your metrics backend, tagged by status:\r\n    // histogram.record(meta.durationMs, { status: meta.status });\r\n  },\r\n  onRetry: (meta) => {\r\n    const took = Math.round(meta.durationMs);\r\n    console.warn(\r\n      `attempt ${meta.attempt} failed in ${took}ms` +\r\n        (meta.willRetry ? `, retrying in ${meta.delayMs}ms` : ' — giving up'),\r\n    );\r\n  },\r\n});\r\n\r\nfunction percentile(values: number[], p: number): number {\r\n  if (values.length === 0) return NaN;\r\n  const sorted = [...values].sort((a, b) => a - b);\r\n  return sorted[Math.min(sorted.length - 1, Math.floor((p / 100) * sorted.length))];\r\n}\r\n// ...later: percentile(latencies, 99) → P99 latency in ms\r\n```\r\n\r\n`ResponseMeta` also exposes `status`, `headers`, `attempt`, and `url`; `RetryMeta` exposes `attempt`, `delayMs`, `error`, and `willRetry`. The hooks run on the request's hot path and any error they throw is swallowed, so keep them cheap and non-throwing. The api-key is never passed to these callbacks.\r\n\r\n### Rate limiting\r\n\r\nThe SDK handles rate limits **reactively**: a `429` is retried with full-jitter backoff that honors the server's `Retry-After`, bounded by `retry.maxAttempts` and `retry.maxElapsedMs` (see [RetryConfig](#retryconfig)). If retries are exhausted it throws `RateLimitError`, whose `.statusCode` is `429` and whose response body is on `.body`.\r\n\r\nIt does **not** throttle proactively — it won't slow down before you hit the limit. If you want to avoid `429`s in the first place (e.g. a batch job), read the rate-limit headers off `onResponse` and pace your own requests. api.bible returns `X-RateLimit-Remaining`; treat any reset hint as advisory and confirm the exact header semantics against a live response.\r\n\r\n```typescript\r\nimport { createBibleClient } from '@americanbible/api-bible-sdk';\r\n\r\nlet remaining = Infinity;\r\nlet resetAtMs = 0; // when the window resets, if the API reports it\r\n\r\nconst client = createBibleClient({\r\n  apiKey: process.env.BIBLE_API_KEY!,\r\n  onResponse: (meta) => {\r\n    const rem = meta.headers.get('x-ratelimit-remaining');\r\n    if (rem !== null) remaining = Number(rem);\r\n    // Adjust to your API's actual reset header (seconds-until-reset shown here):\r\n    const reset = meta.headers.get('x-ratelimit-reset');\r\n    if (reset !== null) resetAtMs = Date.now() + Number(reset) * 1000;\r\n  },\r\n});\r\n\r\n// Call before a request when you'd rather wait than spend your last token.\r\nasync function throttle(): Promise<void> {\r\n  if (remaining > 0) return;\r\n  const waitMs = Math.max(0, resetAtMs - Date.now());\r\n  if (waitMs > 0) await new Promise((r) => setTimeout(r, waitMs));\r\n}\r\n\r\nawait throttle();\r\nconst bibles = await client.bibles.list();\r\n```\r\n\r\nFor multiple clients or processes sharing one API key, enforce the limit in a shared place (a queue or a distributed limiter) rather than per-instance — the backoff already adds jitter so concurrent clients don't retry in lockstep, but only a shared limiter actually caps aggregate request rate.\r\n\r\n---\r\n\r\n## API Reference\r\n\r\nAll resource methods return `Promise<ApiResponse<T>>`, where `ApiResponse<T>` is:\r\n\r\n```typescript\r\ntype ApiResponse<T> = {\r\n  data: T;\r\n  meta?: Meta; // FUMS analytics metadata from API.Bible\r\n};\r\n```\r\n\r\nEvery method accepts an optional `AbortSignal` as its final argument.\r\n\r\n---\r\n\r\n### `client.bibles`\r\n\r\n```typescript\r\n// List all Bibles, optionally filtered\r\nbibles.list(params?: BibleListParams, signal?: AbortSignal): Promise<ApiResponse<Bible[]>>\r\n\r\n// Get a single Bible by ID\r\nbibles.get(bibleId: string, signal?: AbortSignal): Promise<ApiResponse<Bible>>\r\n```\r\n\r\n**`BibleListParams`:** `language?`, `abbreviation?`, `name?`, `ids?: string[]`, `includeFullDetails?: boolean`\r\n\r\n---\r\n\r\n### `client.books`\r\n\r\n```typescript\r\nbooks.list(bibleId: string, params?: BookListParams, signal?: AbortSignal): Promise<ApiResponse<Book[]>>\r\nbooks.get(bibleId: string, bookId: string, params?: BookGetParams, signal?: AbortSignal): Promise<ApiResponse<Book>>\r\n```\r\n\r\n**`BookListParams`:** `includeChapters?: boolean`, `includeChaptersAndSections?: boolean`  \r\n**`BookGetParams`:** `includeChapters?: boolean`\r\n\r\n---\r\n\r\n### `client.chapters`\r\n\r\n```typescript\r\nchapters.list(bibleId: string, bookId: string, signal?: AbortSignal): Promise<ApiResponse<ChapterSummary[]>>\r\nchapters.get(bibleId: string, chapterId: string, params?: ChapterGetParams, signal?: AbortSignal): Promise<ApiResponse<Chapter>>\r\n```\r\n\r\n---\r\n\r\n### `client.verses`\r\n\r\n```typescript\r\nverses.list(bibleId: string, chapterId: string, signal?: AbortSignal): Promise<ApiResponse<VerseSummary[]>>\r\nverses.get(bibleId: string, verseId: string, params?: VerseGetParams, signal?: AbortSignal): Promise<ApiResponse<Verse>>\r\n```\r\n\r\n---\r\n\r\n### `client.passages`\r\n\r\n```typescript\r\n// passageId is a range like \"GEN.1.1-GEN.1.10\"\r\npassages.get(bibleId: string, passageId: string, params?: PassageGetParams, signal?: AbortSignal): Promise<ApiResponse<Passage>>\r\n```\r\n\r\n---\r\n\r\n### `client.sections`\r\n\r\n```typescript\r\nsections.listForBook(bibleId: string, bookId: string, signal?: AbortSignal): Promise<ApiResponse<SectionSummary[]>>\r\nsections.listForChapter(bibleId: string, chapterId: string, signal?: AbortSignal): Promise<ApiResponse<SectionSummary[]>>\r\nsections.get(bibleId: string, sectionId: string, params?: SectionGetParams, signal?: AbortSignal): Promise<ApiResponse<Section>>\r\n```\r\n\r\n---\r\n\r\n### `client.audioBibles`\r\n\r\n```typescript\r\naudioBibles.list(params?: AudioBibleListParams, signal?: AbortSignal): Promise<ApiResponse<AudioBibleSummary[]>>\r\naudioBibles.get(audioBibleId: string, signal?: AbortSignal): Promise<ApiResponse<AudioBible>>\r\n\r\naudioBibles.listBooks(audioBibleId: string, params?: AudioBookListParams, signal?: AbortSignal): Promise<ApiResponse<AudioBookSummary[]>>\r\naudioBibles.getBook(audioBibleId: string, bookId: string, params?: AudioBookGetParams, signal?: AbortSignal): Promise<ApiResponse<AudioBookSummary>>\r\n\r\naudioBibles.listChapters(audioBibleId: string, bookId: string, signal?: AbortSignal): Promise<ApiResponse<AudioChapterSummary[]>>\r\n// Returns resourceUrl (signed audio stream URL); timecodes are included only when api.bible has them\r\naudioBibles.getChapter(audioBibleId: string, chapterId: string, signal?: AbortSignal): Promise<ApiResponse<AudioChapter>>\r\n```\r\n\r\n`AudioChapter` includes a signed `resourceUrl` for the audio stream. The `timecodes` array (mapping timestamps to verse IDs) is present only when api.bible provides timecode data for that chapter — it is not a request option, so it will be absent for audio Bibles that have no timecodes.\r\n\r\n---\r\n\r\n### `client.search`\r\n\r\n```typescript\r\nsearch.search(bibleId: string, params?: SearchParams, signal?: AbortSignal): Promise<ApiResponse<SearchResult>>\r\n```\r\n\r\n**`SearchParams`:**\r\n\r\n| Param       | Type                                                | Description                                                      |\r\n| ----------- | --------------------------------------------------- | ---------------------------------------------------------------- |\r\n| `query`     | `string`                                            | Search query string.                                             |\r\n| `limit`     | `number`                                            | Max results to return.                                           |\r\n| `offset`    | `number`                                            | Pagination offset.                                               |\r\n| `sort`      | `'relevance' \\| 'canonical' \\| 'reverse-canonical'` | Sort order.                                                      |\r\n| `range`     | `string`                                            | Limit search to a passage range (e.g. `\"GEN\"`, `\"MAT.1-MAT.5\"`). |\r\n| `fuzziness` | `'AUTO' \\| '0' \\| '1' \\| '2'`                       | Fuzzy match level.                                               |\r\n\r\n---\r\n\r\n## Content Parameters\r\n\r\nThe `get` methods on `chapters`, `verses`, `passages`, and `sections` share a common set of content parameters:\r\n\r\n| Param                   | Type                         | Description                                          |\r\n| ----------------------- | ---------------------------- | ---------------------------------------------------- |\r\n| `contentType`           | `'html' \\| 'json' \\| 'text'` | Format for the `content` field.                      |\r\n| `includeNotes`          | `boolean`                    | Include footnotes.                                   |\r\n| `includeTitles`         | `boolean`                    | Include section titles.                              |\r\n| `includeChapterNumbers` | `boolean`                    | Include chapter number markers.                      |\r\n| `includeVerseNumbers`   | `boolean`                    | Include verse number markers.                        |\r\n| `includeVerseSpans`     | `boolean`                    | Include verse span markers.                          |\r\n| `parallels`             | `string[]`                   | Additional Bible IDs to include as parallel content. |\r\n\r\n---\r\n\r\n## Error Handling\r\n\r\nEvery error the SDK throws extends a single root, `BibleError`, so one\r\n`instanceof BibleError` catches anything the SDK can throw. Narrow to a subtype\r\nfor specific handling:\r\n\r\n```\r\nBibleError                  ← catch-all for every error the SDK throws\r\n├─ ApiError                 ← a request was attempted; exposes statusCode + body\r\n│  ├─ AuthError               401 / 403\r\n│  ├─ NotFoundError           404\r\n│  ├─ BadRequestError         400\r\n│  ├─ RateLimitError          429        (auto-retried)\r\n│  ├─ ServerError             5xx        (auto-retried)\r\n│  └─ NetworkError            transport failure / timeout (auto-retried)\r\n├─ InvalidInputError        ← you called the SDK wrong; no request was made\r\n└─ ValidationError          ← API response didn't match the schema; exposes issues\r\n```\r\n\r\nImport the error classes you need and use `instanceof` checks. Order subtypes\r\nbefore their supertypes (e.g. `NetworkError` before the `ApiError` catch-all):\r\n\r\n```typescript\r\nimport {\r\n  createBibleClient,\r\n  InvalidInputError,\r\n  AuthError,\r\n  NotFoundError,\r\n  BadRequestError,\r\n  RateLimitError,\r\n  ServerError,\r\n  NetworkError,\r\n  ValidationError,\r\n  ApiError,\r\n  BibleError,\r\n} from \"@americanbible/api-bible-sdk\";\r\n\r\nconst client = createBibleClient({ apiKey: process.env.BIBLE_API_KEY! });\r\n\r\ntry {\r\n  const { data } = await client.verses.get(\"invalid-bible-id\", \"GEN.1.1\");\r\n} catch (err) {\r\n  if (err instanceof InvalidInputError) {\r\n    // You called the SDK wrong (missing apiKey, malformed params) — no request was sent\r\n    console.error(\"Invalid input:\", err.message);\r\n  } else if (err instanceof AuthError) {\r\n    // 401 or 403 — check your API key\r\n    console.error(\"Authentication failed:\", err.message);\r\n  } else if (err instanceof NotFoundError) {\r\n    // 404 — Bible or verse ID does not exist\r\n    console.error(\"Not found:\", err.message);\r\n  } else if (err instanceof BadRequestError) {\r\n    // 400 — malformed request\r\n    console.error(\"Bad request:\", err.message);\r\n  } else if (err instanceof RateLimitError) {\r\n    // 429 — automatically retried; only thrown when retries are exhausted\r\n    console.error(\"Rate limit exceeded\");\r\n  } else if (err instanceof ServerError) {\r\n    // 5xx — automatically retried; only thrown when retries are exhausted\r\n    console.error(\"Server error:\", err.statusCode);\r\n  } else if (err instanceof NetworkError) {\r\n    // DNS/TCP/TLS failure or timeout before any response — auto-retried.\r\n    // statusCode is 0 and body is \"\" (no response was received).\r\n    console.error(\"Network error:\", err.message);\r\n  } else if (err instanceof ValidationError) {\r\n    // SDK received a response that didn't match its schema\r\n    console.error(\"Schema validation failed:\", err.format());\r\n  } else if (err instanceof ApiError) {\r\n    // Catch-all for any other error where a request was attempted\r\n    console.error(`HTTP ${err.statusCode}:`, err.body);\r\n  } else if (err instanceof BibleError) {\r\n    // Catch-all for any SDK error\r\n    console.error(\"SDK error:\", err.message);\r\n  }\r\n}\r\n```\r\n\r\n| Error | Extends | Thrown when | Auto-retried | `statusCode` / `body` |\r\n| --- | --- | --- | --- | --- |\r\n| `AuthError` | `ApiError` | HTTP 401 / 403 | no | real / response body |\r\n| `NotFoundError` | `ApiError` | HTTP 404 | no | real / response body |\r\n| `BadRequestError` | `ApiError` | HTTP 400 | no | real / response body |\r\n| `RateLimitError` | `ApiError` | HTTP 429 | **yes** | real / response body |\r\n| `ServerError` | `ApiError` | HTTP 5xx | **yes** | real / response body |\r\n| `NetworkError` | `ApiError` | transport failure / timeout, before any response | **yes** | `0` / `\"\"` |\r\n| `InvalidInputError` | `BibleError` | bad caller input — thrown **synchronously**, no request sent | no | — |\r\n| `ValidationError` | `BibleError` | API response failed Zod validation (contract drift / bad JSON) | no | `issues` + `format()` |\r\n\r\nNotes:\r\n\r\n- **`BibleError`** is the single root — use it when you just want \"did the SDK\r\n  throw?\" `ApiError` narrows that to \"a request was attempted,\" and includes\r\n  `NetworkError` even though no HTTP response came back (`statusCode` `0`,\r\n  `body` `\"\"`).\r\n- **`InvalidInputError`** is deliberately *not* an `ApiError`: it's raised before\r\n  any request (missing `apiKey`, a plaintext `baseUrl`, out-of-range retry\r\n  bounds, an empty search query, a comma inside an `ids[]`/`parallels` element).\r\n  Catch it to distinguish \"I called the SDK wrong\" from a real API/transport\r\n  failure.\r\n- **`ValidationError`** is also not an `ApiError`: it signals an SDK/contract\r\n  problem, not an API failure. Inspect `issues` (Zod's path-based list) or call\r\n  `format()` for a log-ready multi-line summary.\r\n- **`RateLimitError`**, **`ServerError`**, and **`NetworkError`** are retried\r\n  automatically; they only reach your code once retries are exhausted — or, for\r\n  429/5xx, immediately when the server's `Retry-After` exceeds `retry.maxDelayMs`.\r\n\r\n---\r\n\r\n## Advanced Usage\r\n\r\n### Request cancellation with `AbortSignal`\r\n\r\n```typescript\r\nconst controller = new AbortController();\r\n\r\n// Cancel after 5 seconds\r\nsetTimeout(() => controller.abort(), 5000);\r\n\r\ntry {\r\n  const { data } = await client.chapters.get(\r\n    \"de4e12af7f28f599-02\",\r\n    \"GEN.1\",\r\n    { contentType: \"text\" },\r\n    controller.signal,\r\n  );\r\n} catch (err) {\r\n  if (err instanceof DOMException && err.name === \"AbortError\") {\r\n    console.log(\"Request cancelled\");\r\n  }\r\n}\r\n```\r\n\r\n### Custom retry configuration\r\n\r\n```typescript\r\nconst client = createBibleClient({\r\n  apiKey: process.env.BIBLE_API_KEY!,\r\n  retry: {\r\n    maxAttempts: 5,\r\n    baseDelayMs: 1000,\r\n    maxDelayMs: 60_000,\r\n  },\r\n});\r\n```\r\n\r\n### Custom fetch implementation\r\n\r\nUseful for testing, edge runtimes, or adding middleware:\r\n\r\n```typescript\r\nimport { createBibleClient } from \"@americanbible/api-bible-sdk\";\r\n\r\nconst client = createBibleClient({\r\n  apiKey: process.env.BIBLE_API_KEY!,\r\n  fetch: async (url, init) => {\r\n    console.log(\"→\", url);\r\n    return globalThis.fetch(url, init);\r\n  },\r\n});\r\n```\r\n\r\n### Paginating search results\r\n\r\n```typescript\r\nconst PAGE_SIZE = 20;\r\nlet offset = 0;\r\nlet total = Infinity;\r\n\r\nwhile (offset < total) {\r\n  const { data } = await client.search.search(\"de4e12af7f28f599-02\", {\r\n    query: \"love\",\r\n    limit: PAGE_SIZE,\r\n    offset,\r\n  });\r\n\r\n  total = data.total;\r\n  console.log(data.verses);\r\n  offset += PAGE_SIZE;\r\n}\r\n```\r\n\r\n---\r\n\r\n## Versioning and support\r\n\r\nThis package follows [Semantic Versioning](https://semver.org/). Notable changes are recorded in [CHANGELOG.md](CHANGELOG.md).\r\n\r\n- **Patch** (`1.0.x`) — bug fixes and internal changes with no API impact.\r\n- **Minor** (`1.x.0`) — backwards-compatible additions: new methods, new optional config, new fields on response/observer types. Because every response schema uses `.passthrough()`, new fields returned by api.bible do **not** require a release and will not break validation.\r\n- **Major** (`x.0.0`) — breaking changes to the public API: removed or renamed exports, changed method signatures, changed error types, or raising the minimum Node version.\r\n\r\nThe **public API** is everything exported from the package entry point — `createBibleClient`, the resource interfaces, the config/response types, and the error classes. Anything reached through `dist/` internals is not a stable surface.\r\n\r\n**Runtime support:** Node 20+. Dropping an end-of-life Node version is a breaking change and ships only in a major release.\r\n\r\n**Security support:** fixes land on the latest published minor release; see [SECURITY.md](../../SECURITY.md).\r\n\r\n---\r\n\r\n## Development\r\n\r\n```bash\r\n# Run the test suite (Vitest)\r\nnpm test\r\n\r\n# Type-check without emitting\r\nnpm run typecheck\r\n\r\n# Run the bundled example\r\nnpm run example\r\n```\r\n\r\nTests use Vitest with mocked `fetch` responses — no live API calls are made during testing.\r\n\r\n### Contract fixtures\r\n\r\nThe offline contract tests in `tests/contract/` replay recorded api.bible responses (`tests/contract/fixtures/recordings.json`) through the real client and schemas, so schema drift is caught without touching the network. The nightly **Contract** workflow runs the same cases against the live API to *detect* drift; the monthly **Refresh Contract Fixtures** workflow re-records the fixtures and opens a PR with any changes (or run it on demand via *workflow_dispatch*). To refresh locally:\r\n\r\n```bash\r\nBIBLE_API_KEY=... npm run record-fixtures\r\n```\r\n\r\n---\r\n\r\n## Contributing\r\n\r\nContributions are welcome. Please open an issue to discuss significant changes before submitting a pull request. Make sure `npm test` and `npm run typecheck` both pass before opening a PR.\r\n\r\nMaintainers: see [RELEASING.md](RELEASING.md) for the release process and the rollback/incident runbook.\r\n\r\n---\r\n","readmeFilename":"README.md"}