# @quranjs/api

[![NPM Version][npm-badge]][npm]
[![MIT License][license-badge]][license]
[![Build Status][build-badge]][build]
[![NPM Monthly downloads][downloads-badge]][npm]

A JavaScript/TypeScript library for fetching **authentic, scholarly verified Quran data** from the [Quran.com API](https://api-docs.quran.foundation/docs/category/content-apis).

Unlike other sources, this SDK connects you directly to the **[Quran Foundation](https://quran.foundation)**—ensuring a **trusted, highly scrutinized source** of reliable content, including properly licensed translations, tafsir, and supplementary materials.

Works in both server and browser environments through separate runtime entrypoints:

- `@quranjs/api/server`
- `@quranjs/api/public`

**Built by the [Quran Foundation](https://quran.foundation) — the team behind [Quran.com](https://quran.com)**

## Installation

```bash
# npm
npm install @quranjs/api

# yarn
yarn add @quranjs/api

# pnpm
pnpm add @quranjs/api
```

## Quick Start

```typescript
import { SearchMode } from "@quranjs/api";
import { createServerClient } from "@quranjs/api/server";

const client = createServerClient({
  clientId: process.env.CLIENT_ID!,
  clientSecret: process.env.CLIENT_SECRET!,
});

const chapters = await client.content.v4.chapters.list();
const results = await client.search.v1.query({
  query: "mercy",
  mode: SearchMode.Quick,
});
```

### Analytics Events

Analytics submission uses the `analytics.events.write` scope and is available
only from the server entrypoint. The SDK obtains and caches the required
client-credentials token. Keep `CLIENT_SECRET` in server-side environment
variables.

```typescript
const result = await client.analytics.v1.events.submit({
  events: [
    {
      eventId: crypto.randomUUID(),
      name: "quran.reader.verse_viewed",
      version: 1,
      occurredAt: new Date(),
      userId: "QURAN_FOUNDATION_USER_ID",
      sessionId: "session-123",
      properties: { verseKey: "2:255", surface: "reader" },
    },
    {
      eventId: crypto.randomUUID(),
      name: "quran.app.started",
      version: 1,
      occurredAt: new Date(),
      anonymousId: "anonymous-123",
    },
  ],
});
```

A successful response accepts the complete batch. Retry a failed batch with
the same event IDs so downstream processing can identify duplicates.

For browser or mobile apps, use `@quranjs/api/public`. Public usage docs live in the API docs portal.

### App State

App State stores app-owned JSON documents for signed-in users. It is available
from both runtime entrypoints under `client.auth.v1.appState`. Read the enabled
data groups before writing, use a fresh high-entropy idempotency key for each
logical mutation, and store quoted ETags unchanged.

```typescript
const config = await client.auth.v1.appState.getConfiguration();

const created = await client.auth.v1.appState.putDocument(
  "settings",
  "theme",
  { value: { mode: "dark" }, schemaVersion: 1 },
  { idempotencyKey: crypto.randomUUID(), ifNoneMatch: "*" },
);

const current = await client.auth.v1.appState.getDocument("settings", "theme");
await client.auth.v1.appState.putDocument(
  "settings",
  "theme",
  { value: { mode: "light" }, schemaVersion: 1 },
  { idempotencyKey: crypto.randomUUID(), ifMatch: current.etag! },
);
```

For offline startup, page through `bootstrap()` until `hasMore` is false and
then persist `nextSyncToken`. Apply each `getChanges()` page and its next token
atomically. On HTTP 410, preserve pending writes, bootstrap and drain changes,
replay pending writes, and then pull again. An unchanged replay request retains
its idempotency key; a conflict rebase rotates it with the changed fingerprint.

For transactional offline reconciliation, provide an account-scoped durable
`AppStateStore`. Its `transaction(accountId, reducer)` implementation must
initialize missing accounts, run the reducer synchronously, and atomically
commit the complete draft only when the reducer returns successfully. Reducers
must not perform network I/O. The reconciler stages bootstrap pages separately,
applies change pages with their tokens atomically, replays immutable local
replacements, and rejects responses from an account that is no longer active.

```typescript
import { createAppStateReconciler } from "@quranjs/api/public";

const appState = createAppStateReconciler({
  accountId: signedInAccountId, // Explicit identity; never derive it from a token.
  store: durableAppStateStore,
  transport: client.auth.v1.appState,
});

await appState.putDocument("settings", "theme", {
  schemaVersion: 1,
  value: { mode: "dark" },
});
await appState.reconcile();

const state = await appState.getState();
const theme = state.visible["settings/theme"];

await appState.switchAccount(
  nextSignedInAccountId,
  nextAccountClient.auth.v1.appState,
);
```

Account switching replaces the local account boundary and transport atomically. Create a separate
client/transport whose immutable session belongs to the target account; do not pass a facade that
reads a mutable cross-account session at request time. An in-flight request retains the transport
captured for its original account, and its late result cannot commit after the generation changes.

`putDocument()` and `deleteDocument()` only queue local mutations. Call
`reconcile()` to pull, replay the captured pending set, and pull again. Calls to
`reconcile()` are serialized, while local queue writes remain available. On a
strict `412` conflict, the complete replacement is rebased onto the refreshed
ETag with a new idempotency key. `createAppStateMemoryStore()` is available for
tests and short-lived sessions; it is not durable across process restarts.

Existing `QuranClient` imports from `@quranjs/api` remain supported for backwards compatibility:

```typescript
import { QuranClient } from "@quranjs/api";

const client = new QuranClient({
  clientId: process.env.CLIENT_ID!,
  clientSecret: process.env.CLIENT_SECRET!,
});

const chapters = await client.chapters.findAll();
```

For new apps, prefer the runtime-specific `@quranjs/api/server` and `@quranjs/api/public` entrypoints.

## Documentation

For complete documentation, guides, and API reference, visit:

📚 **[SDK Documentation](https://api-docs.quran.foundation/docs/sdk/javascript)**

## Features

- 🚀 Full TypeScript support
- 🌐 Works in Node.js and browsers
- ✅ Scholarly verified data
- 📖 Access chapters, verses, juzs, and more
- 🔍 Full-text search
- 🎧 Audio recitations
- 🌍 Multiple verified translations and languages

## Content Sync

Bootstrap an approved public Mushaf, download its snapshot for offline use, and
then poll the same resource filter for incremental changes:

```ts
import type { MushafSnapshotRecord } from "@quranjs/api";

const changes = await client.resources.sync({
  bootstrap: true,
  resources: "mushafs:1",
});
const snapshot = await client.resources.findSnapshot<MushafSnapshotRecord>(
  "mushafs",
  1,
);
```

Mushaf snapshots include layout metadata, pages, publicly distributable font
assets, and words. Store the final `nextSyncToken` and use it with the same
`resources` filter on subsequent sync calls.

Once published, the singleton `quran_core:1` provides canonical Uthmani verse
text, Surah metadata, and Juz/Hizb/Rub-el-Hizb boundaries without duplicating
them in every Mushaf snapshot. Mushaf-specific pages and glyphs remain in
`mushafs:<id>`. Publication is pending content/licensing approval.

```ts
import type { QuranCoreSnapshotRecord } from "@quranjs/api";

await client.resources.sync({
  bootstrap: true,
  resources: "mushafs:1;quran_core:1",
});
const core = await client.resources.findSnapshot<QuranCoreSnapshotRecord>(
  "quran_core",
  1,
);
for (const record of core.records) {
  if (record.recordType === "verse") console.log(record.verseKey, record.textUthmani);
}
```

Word-by-word transliterations use their resource content ID and expose a typed,
camel-cased snapshot payload:

```ts
import type { WordByWordTransliterationSnapshotRecord } from "@quranjs/api";

await client.resources.sync({
  bootstrap: true,
  resources: "word_by_word_transliterations:60",
});

const transliterations =
  await client.resources.findSnapshot<WordByWordTransliterationSnapshotRecord>(
    "word_by_word_transliterations",
    60,
  );
```

## Links

- [Quran Foundation](https://quran.foundation) — Our mission to make the Quran accessible to everyone
- [API Documentation](https://api-docs.quran.foundation) — Full API reference
- [GitHub Repository](https://github.com/quran/api-js) — Source code and issues

## License

MIT © [Quran Foundation](https://quran.foundation)

<!-- Links -->

[npm]: https://www.npmjs.com/package/@quranjs/api
[npm-badge]: https://img.shields.io/npm/v/@quranjs/api
[license-badge]: https://img.shields.io/npm/l/@quranjs/api
[license]: https://github.com/quran/api-js/blob/main/LICENSE
[build-badge]: https://github.com/quran/api-js/workflows/CI/badge.svg
[build]: https://github.com/quran/api-js/actions?query=workflow%3ACI
[downloads-badge]: https://img.shields.io/npm/dm/@quranjs/api
