# Qaf developer docs > Search and read classical Islamic books from an AI app or your own code, with the Qaf MCP server and API. # Qaf developer docs Source: https://developers.qaf.ai/ The Qaf library holds more than 8,000 books of Qur’an exegesis, hadith, jurisprudence, theology, history and language, in Arabic. ## Connect an AI app Paste the server URL into an AI app that supports MCP, then sign in with your Qaf account. You don't need an API key. ```text https://api.qaf.ai/mcp ``` Setup steps for [Claude](/mcp/connect?client=claude), [ChatGPT](/mcp/connect?client=chatgpt), [Claude Code](/mcp/connect?client=claude-code), [Codex](/mcp/connect?client=codex), [Cursor](/mcp/connect?client=cursor) and [VS Code](/mcp/connect?client=vscode). ## Call the API Create an API key, then search the library from your own code. ```bash curl https://api.qaf.ai/v1/search \ -H "Authorization: Bearer $QAF_API_KEY" \ -H "Content-Type: application/json" \ -d '{"query": "الصبر عند المصيبة"}' ``` [Get an API key](https://qaf.ai/login?redirect=%2F%3Fsettings%3Dapi-keys) or follow the [quickstart](/api/quickstart). ## What you can do The MCP server and the API run the same operations on the same library, and return the same passages. | Task | MCP tool | API endpoint | | --- | --- | --- | | Search passages by meaning and keywords | [`search`](/mcp/tools#search) | [`POST /v1/search`](/reference/passages/search-passages) | | Read a passage and the passages around it | [`fetch`](/mcp/tools#fetch) | [`GET /v1/passages/{id}`](/reference/passages/get-passage) | | Look up an author or book by name | [`lookup_catalog`](/mcp/tools#lookup_catalog) | [`GET /v1/catalog/lookup`](/reference/catalog/lookup-catalog) | ## Before you build - [The Qaf library](/library) explains books, authors, passages, IDs and genres. - [Search and read passages](/guides/search) shows how to write queries and filters that find the right text. - [Cite passages](/guides/citations) covers how to show where text comes from. - [Docs for AI agents](/agents) lists the Markdown and machine-readable versions of these docs. --- # Docs for AI agents Source: https://developers.qaf.ai/agents Every page on this site has a Markdown version, and the site publishes the indexes agents look for. Point a coding agent at these instead of the HTML. ## Markdown - **Any page:** add `.md` to its URL, such as [`/api/quickstart.md`](/api/quickstart.md). The home page is [`/index.md`](/index.md). - **Content negotiation:** request a page with `Accept: text/markdown` and the site returns the Markdown version. - **On a page:** **Copy as Markdown** copies the page, and **Open in chat** opens it in Claude, ChatGPT or Cursor. ```bash curl -H "Accept: text/markdown" https://developers.qaf.ai/api/quickstart ``` The Markdown includes the content of every tab, so the setup steps for each app on [Connect an AI app](/mcp/connect) are all there. ## Indexes | File | Contains | | --- | --- | | [`/llms.txt`](/llms.txt) | A short guide for agents, then every page with its summary. | | [`/llms-full.txt`](/llms-full.txt) | Every page's Markdown in one file. | | [`/skill.md`](/skill.md) | An agent skill that maps these docs. | | [`/.well-known/agent-skills/index.json`](/.well-known/agent-skills/index.json) | The agent skills this site publishes. `qaf-api-and-mcp-server` teaches an agent to connect the MCP server, call the API, search and cite. | | [`/sitemap.xml`](/sitemap.xml) | Every page, for crawlers. | | [`/agent-readability.json`](/agent-readability.json) | Where each of these lives. | | [`/.well-known/api-catalog`](/.well-known/api-catalog) | The Qaf API, with links to its OpenAPI document and docs, and the MCP server, with a link to its docs (RFC 9727). | | [`/.well-known/ai-catalog.json`](/.well-known/ai-catalog.json) | The MCP server, the OpenAPI document, the agent skills and `llms.txt`, for agent registries. | ## The API's schema The OpenAPI document is the exact schema of every endpoint: ```text https://api.qaf.ai/v1/openapi.json ``` It needs no key. An agent writing code against the API can read fields, limits and error codes from it rather than from prose. ## Use Qaf from an agent To let an agent search the library while it works, connect the [MCP server](/mcp) rather than giving it an API key. It signs in with your Qaf account, and its tools describe their own inputs. --- # API overview Source: https://developers.qaf.ai/api The Qaf API lets your own code search and read the Qaf library. It runs the same operations as the [MCP server](/mcp) and returns the same JSON. ```text https://api.qaf.ai/v1 ``` ## Endpoints | Endpoint | Does | MCP tool | | --- | --- | --- | | [`POST /v1/search`](/reference/passages/search-passages) | Searches passages, optionally filtered by author, book, genre or death year. | `search` | | [`GET /v1/passages/{id}`](/reference/passages/get-passage) | Reads a passage, with up to five neighbouring passages on each side. | `fetch` | | [`GET /v1/catalog/lookup`](/reference/catalog/lookup-catalog) | Resolves an author or book name to IDs for search filters. | `lookup_catalog` | The [API reference](/reference) lists every field, limit and response. [The Qaf library](/library) explains the passage fields, IDs and genres. ## Requests and responses - **Format:** requests and responses are JSON in UTF-8. Send `Content-Type: application/json` with a request body. - **Parameters:** `POST /v1/search` takes a JSON body. The `GET` endpoints take query parameters, and the passage ID goes in the path. - **`HEAD`:** works wherever `GET` does. It returns the same status and headers without the body, and counts toward the rate limit like a `GET`. - **Unknown fields:** the search body refuses fields it doesn't define with a `422`, so a misspelled filter fails instead of being ignored. - **Body size:** a request body over 64 KiB gets a `413`. - **Names:** fields are camelCase, such as `sequenceNumber`. Set `locale` to `ar` for Arabic book and author names; it defaults to `en`. ## Authentication Send an API key from qaf.ai **Settings › API keys** as a bearer token: ```http Authorization: Bearer qaf_sk_… ``` Keep keys on your server. See [Authentication](/api/authentication). ## Limits Each account can make 60 requests a minute and 1,000 searches a day, across all its keys. See [Rate limits](/api/rate-limits). ## OpenAPI document The OpenAPI 3.1 document for v1 is at: ```text https://api.qaf.ai/v1/openapi.json ``` It needs no key. Use it to generate a client or to give an agent the exact schema. It is the same document this site's [API reference](/reference) is built from. ## Request IDs Every response has an `X-Request-Id` header, including errors. The only exception is the answer to a browser's CORS preflight (`OPTIONS`) request. When you email us at contact@qaf.ai about a response, include it. ## Browsers The API accepts cross-origin requests from any site, so the reference's **Try it** panel can call it. That doesn't make it safe to put a key in a web page or an app: anyone who loads the page can read the key. Call the API from your server. ## Versioning v1 changes only in ways that keep existing clients working. Write your client to: - ignore response fields it doesn't know; - handle enum values and error codes it doesn't know, such as a new genre slug or a new lookup candidate `type`. These changes can happen in v1 without notice: - new endpoints and new optional request fields; - new response fields, enum values and error codes; - higher limits. Renaming or removing a field, tightening a type or a limit, or changing a default needs a new version, `v2`. When part of v1 is deprecated, its responses carry `Deprecation` and `Link` headers that point to a migration guide, and the [changelog](/changelog) says so. --- # Authentication Source: https://developers.qaf.ai/api/authentication The Qaf API authenticates every request with an API key that belongs to your Qaf account. Send it as a bearer token: ```http Authorization: Bearer qaf_sk_… ``` The [MCP server](/mcp) doesn't use API keys: AI apps sign in with your Qaf account instead. ## Create a key 1. Open [Settings › API keys](https://qaf.ai/login?redirect=%2F%3Fsettings%3Dapi-keys) on qaf.ai. Sign in first if asked; a guest session can't create keys. 2. Select **Create key** and give the key a name that says where you'll use it, such as "Production server". 3. Copy the key and store it somewhere safe, such as your server's secret store. The key is shown only once; Qaf keeps only a hash of it. Keys start with `qaf_sk_`. The list in **Settings › API keys** shows the start of each key, when it was created and when it was last used, so you can tell keys apart without seeing them in full. An account can have up to 10 keys at a time, and can create up to 20 keys an hour. ## Keep keys secret A key gives full access to the API under your account, so treat it like a password. - **Call the API from your server.** Never put a key in a web page, a mobile app or a desktop app, where anyone can read it. - **Keep keys out of source control.** Read them from an environment variable or a secret store. - **Use one key per app or environment.** You can then revoke one without stopping the others. All of an account's keys share one [rate limit](/api/rate-limits), so a second key doesn't add capacity. ## Revoke a key In **Settings › API keys**, select the delete icon next to the key, then **Revoke**. Apps using it stop working right away, and revoking can't be undone. Create a new key to replace it. If a key may have leaked, revoke it first, then create a new one. ## Keys and your account - **Requesting account deletion** revokes all your keys, so requests with them get a `401`. - **A suspended account's** keys are refused with `403` while the suspension lasts, and work again when it ends. ## Try it in the reference The [API reference](/reference) has a **Try it** panel. It sends your key from your browser straight to api.qaf.ai; the docs site never receives it. If you select **Remember on this device**, the key is stored in this browser. Use a separate key for testing, and revoke it when you're done. ## Errors | Status | Message | Cause | | --- | --- | --- | | `401` | `Send an API key in the Authorization header, as Bearer .` | The `Authorization` header is missing or isn't a bearer token. | | `401` | `Invalid API key.` | The bearer token isn't a Qaf API key, or names a key that doesn't exist or was revoked. | | `401` | `This key can't call the Qaf API. Create a key in qaf.ai Settings › API keys.` | The key starts with `qaf_live_`, an older kind of key that v1 doesn't accept. | | `403` | `This Qaf account can't use the API.` | The key's account is suspended. | | `429` | `Too many requests. Try again later.` | The key doesn't exist or was revoked, and your network address has sent too many such keys. `data.reason` is `invalid_keys`. Fix the key rather than retrying. | A `401` has a `WWW-Authenticate` header: `Bearer realm="api.qaf.ai"` when the request sent no key, and `Bearer realm="api.qaf.ai", error="invalid_token"` when it sent a key that can't be used. See [Errors](/api/errors) for the error format. --- # Errors Source: https://developers.qaf.ai/api/errors An error response has a 4xx or 5xx status and a JSON body with the same shape on every endpoint: ```json { "code": "NOT_FOUND", "status": 404, "message": "No passage has ID d9335f69-e54b-59ae-a78c-50db734b8d51." } ``` | Field | Description | | --- | --- | | `code` | A stable, machine-readable code. Branch on this, not on `message`. | | `status` | The HTTP status, repeated. | | `message` | A sentence for people. Its wording can change. | | `data` | Details for some codes, described below. Left out when there are none. | ## Status codes | Status | `code` | When | Retry | | --- | --- | --- | --- | | `400` | `BAD_REQUEST` | The body isn't valid JSON, or the request can't be run as sent, such as a filter with an unknown author ID. | No. Fix the request. | | `401` | `UNAUTHORIZED` | The API key is missing, invalid or revoked. The `WWW-Authenticate` header is `Bearer realm="api.qaf.ai"`, with `error="invalid_token"` added when the request sent a key. | No. | | `403` | `FORBIDDEN` | The key's account can't use the API. | No. | | `404` | `NOT_FOUND` | No passage has the ID, or no endpoint matches the method and path. | No. | | `405` | `METHOD_NOT_SUPPORTED` | The path exists but takes another method. The `Allow` header lists the methods it takes. | No. | | `413` | `PAYLOAD_TOO_LARGE` | The request body is larger than 64 KiB. | No. | | `422` | `INPUT_VALIDATION_FAILED` | A field is missing, has the wrong type or breaks a limit, or the search body has a field it doesn't define. | No. Fix the request. | | `429` | `RATE_LIMITED` | The account went over a [rate limit](/api/rate-limits), or your address sent too many keys that don't exist (`invalid_keys`). | Only when `data.reason` is `minute`, after `Retry-After` seconds. | | `500` | `INTERNAL_SERVER_ERROR` | Something went wrong on our side. | Yes, with backoff. | | `503` | `SERVICE_UNAVAILABLE` | Qaf couldn't finish the request within 30 seconds, or a service it relies on is down. | Yes, with backoff. | Handle codes you don't know by their status: new codes can appear in v1. ## Validation errors A `422` lists the problems in `data`. `fieldErrors` is keyed by the field's name, and `formErrors` holds problems that belong to no single field: ```json { "code": "INPUT_VALIDATION_FAILED", "status": 422, "message": "✖ Too big: expected number to be <=15\n → at limit", "data": { "formErrors": [], "fieldErrors": { "limit": ["Too big: expected number to be <=15"] } } } ``` A field the search body doesn't define appears in `fieldErrors` under the top-level field that holds it: `topK` under `topK`, and an unknown filter key under `filters`. `message` gives the full path, so you can tell which field to drop: ```json { "code": "INPUT_VALIDATION_FAILED", "status": 422, "message": "✖ Unrecognized key: \"categoryIds\"\n → at filters.categoryIds", "data": { "formErrors": [], "fieldErrors": { "filters": ["Unrecognized key: \"categoryIds\""] } } } ``` When the unknown key is the only one in `filters`, `filters` also lists `Specify at least one filter, or omit filters for an unrestricted query.` ## Requests that can't be run A `400` from `POST /v1/search` carries `data.reason`: | `reason` | Means | | --- | --- | | `unknown_author_id` | An ID in `filters.authorIds` isn't in the catalog. Look the author up with [`GET /v1/catalog/lookup`](/reference/catalog/lookup-catalog). | | `unknown_book_id` | An ID in `filters.bookIds` isn't in the catalog. | | `no_matching_books` | No book can match the filters together, such as a genre and a death-year range none of its authors fall in. | New reasons can appear in v1. When `reason` is missing or unknown, show `message`. ## Rate limit errors A `429` has a `Retry-After` header and `data`: ```json { "code": "RATE_LIMITED", "status": 429, "message": "Too many requests. Try again later.", "data": { "retryAfter": 23, "reason": "minute" } } ``` `retryAfter` is the same number of seconds as the header. `reason` says which limit was reached. Only a `minute` refusal is worth waiting out: a `day` refusal can last most of a day, and `invalid_keys` means the key you sent doesn't exist or was revoked. See [Back off and retry](/api/rate-limits#back-off-and-retry). ## Request IDs Every API response, including errors, has an `X-Request-Id` header. The only exception is the answer to a browser's CORS preflight (`OPTIONS`) request. Log the ID with failed requests, and include it when you email us at contact@qaf.ai, so we can find the request. --- # Quickstart Source: https://developers.qaf.ai/api/quickstart 1. **Create an API key** Open [Settings › API keys](https://qaf.ai/login?redirect=%2F%3Fsettings%3Dapi-keys) on qaf.ai and sign in if asked. Select **Create key**, give the key a name and copy it. The key is shown only once. 2. **Store the key** Keep the key in an environment variable rather than in your code: ```bash export QAF_API_KEY="qaf_sk_…" ``` 3. **Search the library** Send a query in Arabic or English to `POST /v1/search`: ```bash cURL curl https://api.qaf.ai/v1/search \ -H "Authorization: Bearer $QAF_API_KEY" \ -H "Content-Type: application/json" \ -d '{"query": "الصبر عند المصيبة", "limit": 3}' ``` ```ts TypeScript const response = await fetch("https://api.qaf.ai/v1/search", { method: "POST", headers: { Authorization: `Bearer ${process.env.QAF_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ query: "الصبر عند المصيبة", limit: 3 }), }); if (!response.ok) throw new Error(await response.text()); const { results } = await response.json(); for (const passage of results) console.log(passage.title, passage.url); ``` ```python Python import os import requests response = requests.post( "https://api.qaf.ai/v1/search", headers={"Authorization": f"Bearer {os.environ['QAF_API_KEY']}"}, json={"query": "الصبر عند المصيبة", "limit": 3}, timeout=30, ) response.raise_for_status() for passage in response.json()["results"]: print(passage["title"], passage["url"]) ``` Each result is a passage with its book, author, printed pages and a link to the page. One result looks like this, with its text shortened: ```json { "id": "d9335f69-e54b-59ae-a78c-50db734b8d50", "title": "Fayd al-Qadir — Abd al-Rauf al-Munawi, vol. 4, p. 233", "url": "https://app.turath.io/book/21660?print_page=4%2C233", "text": "… ٥١٣٤ - (الصبر) الكثير الثواب: الصبر (عند أول صدمة) أي عند فورة المصيبة …", "pages": [ { "vol": "4", "page": 233 }, { "vol": "4", "page": 234 } ], "book": { "id": "31f522f9-b85c-5c78-a8ff-dc48ebce183c", "title": "Fayd al-Qadir", "titleArabic": "فيض القدير" }, "author": { "id": 423, "name": "Abd al-Rauf al-Munawi", "nameArabic": "عبد الرؤوف المناوي" }, "versionId": 21660, "sequenceNumber": 4929 } ``` 4. **Read around a passage** Pass a result's `id` to `GET /v1/passages/{id}`. `radius` adds up to five neighbouring passages on each side, in `before` and `after`: ```bash cURL curl "https://api.qaf.ai/v1/passages/d9335f69-e54b-59ae-a78c-50db734b8d50?radius=1" \ -H "Authorization: Bearer $QAF_API_KEY" ``` ```ts TypeScript const id = "d9335f69-e54b-59ae-a78c-50db734b8d50"; const response = await fetch( `https://api.qaf.ai/v1/passages/${id}?radius=1`, { headers: { Authorization: `Bearer ${process.env.QAF_API_KEY}` } } ); if (!response.ok) throw new Error(await response.text()); const passage = await response.json(); console.log(passage.before.length, passage.text, passage.after.length); ``` ```python Python import os import requests passage_id = "d9335f69-e54b-59ae-a78c-50db734b8d50" response = requests.get( f"https://api.qaf.ai/v1/passages/{passage_id}", headers={"Authorization": f"Bearer {os.environ['QAF_API_KEY']}"}, params={"radius": 1}, timeout=30, ) response.raise_for_status() passage = response.json() print(len(passage["before"]), passage["text"], len(passage["after"])) ``` ## Next steps **[Search and read passages](/guides/search)** Filter by author, book, genre or death year, and page through results. **[Cite passages](/guides/citations)** Show the book, author and printed page for every passage you use. **[Errors](/api/errors)** Read the error format and decide what to retry. **[API reference](/reference)** Every endpoint, field and limit. --- # Rate limits Source: https://developers.qaf.ai/api/rate-limits Limits apply per Qaf account, across all of its API keys: | Limit | Applies to | | --- | --- | | 60 requests a minute | Every endpoint. | | 1,000 searches a day | `POST /v1/search`, counted over 24 hours. | The MCP server has its own limit, also counted per account: see [MCP server](/mcp#limits). ## Headers Once the API has accepted your key, its response reports the account's minute window: | Header | Value | | --- | --- | | `X-RateLimit-Limit` | Requests the account can make per minute. | | `X-RateLimit-Remaining` | Requests left in the current minute. | | `X-RateLimit-Reset` | Seconds until the minute's allowance resets. | These headers always describe the minute window. A `429` for the `minute` or `day` limit has them too; on a `day` refusal, `Retry-After` is the time until searches work again instead. They're absent when the API refuses a request before it accepts the key: any `401` or `403`, an `invalid_keys` `429`, a body over 64 KiB (`413`), a body that isn't valid JSON (`400`), or a path the API doesn't have (`404`, `405`). ## When you reach a limit The API answers `429` with a `Retry-After` header, in seconds, and the reason in the body: ```json { "code": "RATE_LIMITED", "status": 429, "message": "Too many requests. Try again later.", "data": { "retryAfter": 23, "reason": "minute" } } ``` | `reason` | Means | | --- | --- | | `minute` | The account made 60 requests this minute. | | `day` | The account ran 1,000 searches in the last 24 hours. The other endpoints still work. `Retry-After` is the time until searches work again, which can be many hours. | | `invalid_keys` | The key you sent doesn't exist or was revoked, and your network address has sent too many such keys. A key that works is never refused for this reason. | ## Back off and retry Retry a `429` only when `reason` is `minute`: wait `Retry-After` seconds, then send the request again. Don't wait out the other reasons: - **`day`:** stop searching, and tell your users when searches work again. `Retry-After` can be close to a day. - **`invalid_keys`:** the key you send doesn't exist or was revoked. Fix it: retrying with the same key fails again. For `500` and `503` responses, which have no `Retry-After`, wait longer after each failed attempt. ```ts TypeScript const sleep = (seconds: number) => new Promise((resolve) => setTimeout(resolve, seconds * 1000)); async function callQaf(url: string, init: RequestInit, attempts = 4) { for (let attempt = 1; ; attempt++) { const response = await fetch(url, init); if (response.status === 429) { const { data } = await response.clone().json(); if (data?.reason === "day") throw new Error(`Daily search limit. Searches resume in ${data.retryAfter} s.`); if (data?.reason === "invalid_keys") throw new Error("Invalid API key. Check QAF_API_KEY."); if (data?.reason !== "minute" || attempt === attempts) return response; await sleep(Number(response.headers.get("Retry-After"))); } else if (response.status >= 500 && attempt < attempts) { await sleep(2 ** attempt); } else { return response; } } } ``` ```python Python import time import requests def call_qaf(method, url, attempts=4, **kwargs): for attempt in range(1, attempts + 1): response = requests.request(method, url, timeout=30, **kwargs) if response.status_code == 429: data = response.json().get("data") or {} if data.get("reason") == "day": raise RuntimeError(f"Daily search limit. Searches resume in {data['retryAfter']} s.") if data.get("reason") == "invalid_keys": raise RuntimeError("Invalid API key. Check QAF_API_KEY.") if data.get("reason") != "minute" or attempt == attempts: return response time.sleep(int(response.headers["Retry-After"])) elif response.status_code >= 500 and attempt < attempts: time.sleep(2**attempt) else: return response ``` Spread requests over the minute rather than sending them in bursts, and cache results you read more than once: passages rarely change. ## Need more If your project needs higher limits, email us at contact@qaf.ai with what you're building and the volume you expect. --- # Qaf API v1 and API keys Source: https://developers.qaf.ai/changelog/2026-11-14-api-v1 - **Qaf API v1.** Three endpoints at `https://api.qaf.ai/v1`: [search passages](/reference/passages/search-passages), [read a passage](/reference/passages/get-passage) and [look up an author or book](/reference/catalog/lookup-catalog). They run the same operations as the MCP server's tools. Start with the [quickstart](/api/quickstart). - **API keys.** Create keys in qaf.ai **Settings › API keys**. Each account can have up to 10. See [Authentication](/api/authentication). - **Limits.** 60 requests a minute and 1,000 searches a day per account. See [Rate limits](/api/rate-limits). - **MCP server docs.** [Connect an AI app](/mcp/connect) has setup steps for Claude, ChatGPT, Claude Code, Codex, Cursor and VS Code. --- # Cite passages Source: https://developers.qaf.ai/guides/citations Every passage the MCP server or the API returns carries what a reader needs to find it in print. Show it wherever you use the text. ## Cite each passage A citation names the book, the author and the printed volume and page, and links to the page: > فيض القدير — عبد الرؤوف المناوي، ج 4، ص 233 > Fayd al-Qadir — Abd al-Rauf al-Munawi, vol. 4, p. 233 - **`title`** is that line, ready to show, in the language you asked for with `locale`. - **`url`** opens the printed page in the Turath reader, so a reader can check the text in its context. Link the citation to it. - **`book`**, **`author`** and **`pages`** hold the same details as separate fields, for your own format. When a passage spans two pages, `title` and `url` name the first; `pages` lists them all. ## Quote the text as returned - Show passage text as the API returns it, in Arabic, right to left. Don't shorten a quotation without marking where text is left out. - If you translate a passage, say that the translation is yours or your app's, and keep the Arabic beside it. - An AI app should quote passages rather than retell them, and should cite every passage it relies on. ## Qur’an verses and hadiths Qur’an verses and hadiths appear inside passages, in the wording of the book that quotes them. Never alter, complete or paraphrase them: quote that wording exactly and cite the passage it came from. Don't complete a verse or a hadith from memory or join wording from different books. ## What a citation shows A citation shows where text came from. It doesn't show that an answer built on it is correct, and a passage can be one scholar's view among several. Say so in your app where readers might take an answer as a ruling. --- # Search and read passages Source: https://developers.qaf.ai/guides/search Search finds passages by meaning and by wording, then reranks the matches for relevance. These examples use the API; the [`search`](/mcp/tools#search) and [`fetch`](/mcp/tools#fetch) tools take the same arguments. ## Write the query - **Ask for the idea.** A question or a short description, such as "patience at the first shock of a calamity", finds passages that discuss it, even in other words. - **Use Arabic for exact wording.** The texts are Arabic, so an Arabic query also matches passages that use those words, such as a phrase from a hadith: `الصبر عند الصدمة الأولى`. - **Keep one topic per query.** Run two searches for two questions, rather than one search that mixes them. A query can be up to 2,000 characters. ```bash cURL curl https://api.qaf.ai/v1/search \ -H "Authorization: Bearer $QAF_API_KEY" \ -H "Content-Type: application/json" \ -d '{"query": "patience at the first shock of a calamity", "limit": 5}' ``` ```json MCP { "name": "search", "arguments": { "query": "patience at the first shock of a calamity", "limit": 5 } } ``` ## Filter by author or book Filters take IDs, not names. Look the name up first, then pass the ID: ```bash curl "https://api.qaf.ai/v1/catalog/lookup?type=author&query=Ibn%20Hajar%20al-Asqalani" \ -H "Authorization: Bearer $QAF_API_KEY" ``` ```json { "candidates": [ { "type": "author", "id": 202, "label": "Ibn Hajar al-Asqalani", "deathYearAH": 852, "deathInexactLabel": null, "matchScore": 1 }, { "type": "author", "id": 2939, "label": "Bashshar Awwad Maruf", "deathYearAH": null, "deathInexactLabel": null, "matchScore": 0.384 } ] } ``` When several candidates look alike, choose by death year or by the book you have in mind. Then search that author's books: ```bash cURL curl https://api.qaf.ai/v1/search \ -H "Authorization: Bearer $QAF_API_KEY" \ -H "Content-Type: application/json" \ -d '{"query": "النية", "filters": {"authorIds": [202]}}' ``` ```json MCP { "name": "search", "arguments": { "query": "النية", "filters": { "authorIds": [202] } } } ``` Filters match a passage's book and author, not what the passage mentions. To find what other scholars said about Ibn Hajar, search for his name instead of filtering by it. ## Filter by genre A genre describes a book, such as `hadith-commentary` or `hanbali-fiqh`. Use one when you want results from books of that kind; to find passages about a topic, put the topic in the query. [The Qaf library](/library#genres) lists every genre. ```json { "query": "الوقف", "filters": { "genres": ["hanbali-fiqh"] } } ``` ## Filter by death year `deathYearAH` limits results to authors who died in a range of Hijri years. This finds passages by authors who died before 500 AH: ```json { "query": "الإخلاص", "filters": { "deathYearAH": { "lt": 500 } } } ``` Combine `gt`, `gte`, `lt` and `lte` for a range, such as `{ "gte": 600, "lte": 800 }`. Authors with no recorded death year are left out. ## Combine filters By default every filter field must match (`"filterOperator": "AND"`): Hanbali books by authors who died before 800 AH. Set `"filterOperator": "OR"` to accept a passage that matches any field. IDs within one list always combine with OR, so `"authorIds": [202, 423]` means either author. If no book can match the filters together, search returns `400` with `data.reason` `no_matching_books` instead of an empty list. ## Get more results `limit` returns up to 15 passages. To get further passages for the same query, send the IDs you already have in `excludeIds`, up to 200: ```json { "query": "الصبر عند المصيبة", "limit": 15, "excludeIds": [ "d9335f69-e54b-59ae-a78c-50db734b8d50", "c89a5395-46f6-5416-afb5-5e55e66e11f7" ] } ``` ## Read around a passage A passage is a short excerpt. To read the argument it belongs to, ask for its neighbours with `radius`, up to five on each side: ```bash cURL curl "https://api.qaf.ai/v1/passages/d9335f69-e54b-59ae-a78c-50db734b8d50?radius=2" \ -H "Authorization: Bearer $QAF_API_KEY" ``` ```json MCP { "name": "fetch", "arguments": { "id": "d9335f69-e54b-59ae-a78c-50db734b8d50", "radius": 2 } } ``` The response has the passage, plus `before` and `after` in reading order. Each neighbour has its own `id`, so you can keep reading from either end. A passage near the start or end of a book has fewer neighbours. --- # The Qaf library Source: https://developers.qaf.ai/library The MCP server and the API read the same library: more than 8,000 classical Islamic books of Qur’an exegesis, hadith, jurisprudence, theology, history, language and more. The terms on this page apply to both. ## Passages A passage is a short run of text from one book, the unit that search returns and that you read by ID. Every passage has: | Field | What it holds | | --- | --- | | `id` | A stable UUID. Read the passage with it, or skip it in a later search. | | `title` | The book, the author and the first printed page, as one line: `Fayd al-Qadir — Abd al-Rauf al-Munawi, vol. 4, p. 233`. | | `url` | A link that opens the printed page in the Turath reader. Cite this. | | `text` | The passage's Arabic text. | | `footnotes` | The edition's footnotes for the passage. Left out when there are none. | | `pages` | The printed volume and page numbers the passage spans, in reading order: `[{"vol": "4", "page": 233}, {"vol": "4", "page": 234}]`. `vol` is a string because some editions label volumes; either value can be `null`. | | `book` | `id`, `title` and `titleArabic`. | | `author` | `id`, `name` and `nameArabic`. | | `versionId` | The printed edition the passage comes from. | | `sequenceNumber` | The passage's position within that edition. Neighbouring passages have neighbouring numbers. | Passage text is always Arabic. When a page link can't name a printed page, because the edition has no volume and page for the passage, `url` opens the book instead. ## Books, editions and authors - **Book:** a work in the library, identified by a UUID such as `31f522f9-b85c-5c78-a8ff-dc48ebce183c`. - **Edition:** the printed edition a passage was taken from, identified by `versionId`, an integer. The page numbers in `pages` and `url` refer to this edition. - **Author:** identified by a positive integer, such as `202` for Ibn Hajar al-Asqalani. Look up an author's or a book's ID by name with [catalog lookup](/reference/catalog/lookup-catalog), or the [`lookup_catalog`](/mcp/tools#lookup_catalog) tool. A passage also carries the IDs of its book and author. ## Death years Authors' death years are Hijri years (AH). Lookup returns `deathYearAH`, and search filters on it with `filters.deathYearAH`. When the year is uncertain, `deathInexactLabel` gives the label the catalog uses instead. A death-year filter leaves out authors with no recorded death year. ## Names and languages - **Queries:** search accepts Arabic or English. - **`locale`:** `en` (the default) or `ar`. It picks the language of book and author names in `title`, `book.title` and `author.name`. The Arabic names in `titleArabic` and `nameArabic` are always included, and passage text stays Arabic. ## Genres A genre describes a book, such as hadith commentary or Hanafi jurisprudence. It doesn't describe a passage's topic: to find passages about patience, search for patience. Filter by genre only when you want results from books of that genre. Search filters take the genre's slug. Book lookup returns the genre's ID as `categoryId`. | ID | Slug | Genre | | --- | --- | --- | | 1 | `creed` | Creed | | 2 | `sects-and-refutations` | Sects and refutations | | 3 | `quran-commentary` | Qur’an commentary | | 4 | `quranic-sciences-and-principles-of-exegesis` | Qur’anic sciences and principles of exegesis | | 5 | `quran-recitation-and-readings` | Qur’an recitation and readings | | 6 | `hadith-collections` | Hadith collections | | 7 | `hadith-commentary` | Hadith commentary | | 8 | `hadith-sourcing-and-indexes` | Hadith sourcing and indexes | | 9 | `hadith-defects-and-scholarly-questions` | Hadith defects and scholarly questions | | 10 | `hadith-sciences` | Hadith sciences | | 11 | `principles-of-jurisprudence` | Principles of jurisprudence | | 12 | `legal-maxims-and-jurisprudence` | Legal maxims and jurisprudence | | 13 | `logic` | Logic | | 14 | `hanafi-fiqh` | Hanafi fiqh | | 15 | `maliki-fiqh` | Maliki fiqh | | 16 | `shafii-fiqh` | Shafi‘i fiqh | | 17 | `hanbali-fiqh` | Hanbali fiqh | | 18 | `general-fiqh` | General fiqh | | 19 | `fiqh-issues` | Fiqh issues | | 20 | `islamic-governance-and-judiciary` | Islamic governance and judiciary | | 21 | `inheritance-and-wills` | Inheritance and wills | | 22 | `fatwas` | Fatwas | | 23 | `spirituality-etiquette-and-remembrances` | Spirituality, etiquette and remembrances | | 24 | `prophetic-biography` | Prophetic biography | | 25 | `history` | History | | 26 | `biographies-and-generations` | Biographies and generations | | 27 | `genealogy` | Genealogy | | 28 | `geography-and-travel` | Geography and travel | | 29 | `arabic-language` | Arabic language | | 30 | `rare-words-and-dictionaries` | Rare words and dictionaries | | 31 | `grammar-and-morphology` | Grammar and morphology | | 32 | `literature` | Literature | | 33 | `prosody-and-rhyme` | Prosody and rhyme | | 34 | `poetry` | Poetry | | 35 | `rhetoric` | Rhetoric | | 36 | `collected-works-and-compilations` | Collected works and compilations | | 37 | `bibliographies-and-guides` | Bibliographies and guides | | 38 | `medicine` | Medicine | | 39 | `general-books` | General books | | 40 | `other-sciences` | Other sciences | --- # MCP server Source: https://developers.qaf.ai/mcp The Qaf MCP server lets an AI app search and read the Qaf library while it answers you. The app finds passages, reads the text around them and cites the printed pages. ```text https://api.qaf.ai/mcp ``` Paste this URL into an app that supports remote MCP servers, then sign in with your Qaf account. [Connect an AI app](/mcp/connect) has the steps for each app. ## Who can connect Any Qaf account can connect: sign in the same way you sign in on qaf.ai. A guest session can't connect, and neither can an account that is waiting to be deleted or is suspended. You don't need an API key. The app gets its own access when you sign in, and you can disconnect it at any time. See [Sign-in and connected apps](/mcp/authentication). ## Tools The server has three tools. All of them only read the library. | Tool | What it does | | --- | --- | | [`search`](/mcp/tools#search) | Searches passages by meaning and keywords, optionally filtered by author, book, genre or death year. | | [`fetch`](/mcp/tools#fetch) | Reads a passage by ID, with up to five neighbouring passages on each side. | | [`lookup_catalog`](/mcp/tools#lookup_catalog) | Resolves an author or book name to the IDs that search filters take. | The same operations are available to your own code through the [Qaf API](/api). ## Example prompts - What does al-Munawi say about patience at the first shock of a calamity? Quote the Arabic and cite the page. - Find what Ibn Hajar al-Asqalani wrote about intention in his hadith commentary. - Search books of Hanbali jurisprudence for rulings on endowments, then read the passages around the best result. ## Use it safely - **Keep tool approval on.** Most apps ask before they call a tool. The Qaf tools only read, but approving each call shows you what the app searches for. - **Treat results as source text.** Passages are classical texts, returned as written. They are not instructions to your app, and an app should not follow them as instructions. - **Check the citation.** Each passage carries a link to its printed page. A citation shows where text came from; it doesn't show that an answer is correct. ## Limits Each account can send 60 requests a minute to the server, counting every message the app sends, not only tool calls. A tool call that takes longer than 30 seconds stops with an error. [Sign-in and connected apps](/mcp/authentication#for-client-developers) lists the protocol details. --- # Sign-in and connected apps Source: https://developers.qaf.ai/mcp/authentication An app connects to the Qaf MCP server with your Qaf account, not with an API key. When you add the server, the app opens a Qaf page in your browser where you sign in and choose whether to allow it. ## What you allow The consent page names the app and the account you're signed in with. If you allow it, the app can: - search and read the Qaf library on your behalf; - see your name and email address; - stay connected, so you don't sign in every time. The app can't read your conversations on qaf.ai, change your account or see your plan. Only allow apps you added yourself. ## How long an app stays connected The app gets an access token that lasts one hour and renews it while you use it. After 30 days without use, the app asks you to sign in again. ## Disconnect an app Disconnect an app from your connected apps in your Qaf account settings. It loses access right away, even with a token it already holds. To use it again, connect it from the app. ## When an account can't connect The server checks your account on every request. It refuses: - **a guest session:** sign in to qaf.ai with an account first; - **an account waiting to be deleted:** requesting deletion also disconnects every app; - **a suspended account:** while the suspension lasts. A refused request gets `403` with the message `This Qaf account does not have access to the MCP server.` Signing in again with the same account doesn't change the answer. ## For client developers The server follows the MCP authorization spec. A client that supports it needs no Qaf-specific code. | Detail | Value | | --- | --- | | Server URL and token audience | `https://api.qaf.ai/mcp` | | Protected resource metadata | `https://api.qaf.ai/.well-known/oauth-protected-resource/mcp`, named in the `WWW-Authenticate` header of a 401 | | Authorization server metadata | `https://api.qaf.ai/.well-known/oauth-authorization-server` (also at `/.well-known/openid-configuration`). The issuer is `https://api.qaf.ai`. | | Client registration | Dynamic client registration at `https://api.qaf.ai/auth/oauth2/register`, as a public client without a secret | | Grant | Authorization code with PKCE (`S256`), and refresh tokens | | Scopes | `openid`, `profile`, `email`, `offline_access`. The server adds `offline_access` and the `resource` parameter when a client leaves them out. | | Tokens | A one-hour RS256 JWT access token for the resource above, and a 30-day refresh token. DPoP-bound tokens are supported. | | Transport | Stateless Streamable HTTP, `POST` only. `GET` and `DELETE` get `405`. | | Protocol revisions | `2026-07-28` and the 2025 revisions. A 2025 client gets an SSE response; a `2026-07-28` client gets one JSON body. | | Rate limit | 60 requests a minute per account, counting every JSON-RPC message, such as `initialize` and `tools/list`. Over the limit: `429` with `Retry-After` in seconds. | | Request size | 256 KiB per request. Larger bodies get `413`. | | Batches | Not supported. A JSON-RPC batch gets `400`. | | Browser requests | A request with an `Origin` header from a site Qaf doesn't serve gets `403`. Clients that run on a server send no `Origin` and aren't affected. | Token errors come back as `401` with `WWW-Authenticate: Bearer error="invalid_token"` and a description, such as `The access token expired`. A client that gets one should refresh its token or send the user through sign-in again. --- # Connect an AI app Source: https://developers.qaf.ai/mcp/connect Every app connects to the same server URL. After you add it, the app opens a Qaf sign-in page in your browser: sign in with your Qaf account and allow access. ```text https://api.qaf.ai/mcp ``` App menus change often. If a step doesn't match what you see, follow the app's own guide, linked in each tab. **Claude** 1. In Claude, open **Customize › Connectors** and select **Add › Add custom connector**. 2. Name it Qaf, paste the server URL and select **Continue**. Review the detected settings and select **Continue** again. 3. Under **Authentication**, keep **Sign in now**. Under **OAuth client**, choose **Register automatically**: Qaf doesn't support Claude's published identity. 4. Select **Add**, then sign in with your Qaf account and allow access. On Team and Enterprise plans, an owner adds the connector first, under **Organization settings › Connectors**. Each member then selects **Connect** next to it in **Customize › Connectors**. To use Qaf in a chat, select **+** in the message box, open **Connectors** and turn on Qaf. Claude's guide: [Get started with custom connectors using remote MCP](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp). **ChatGPT** 1. In ChatGPT on the web, open [Plugins](https://chatgpt.com/plugins), select **+** and choose **Add custom MCP server**. 2. Name it Qaf and paste the server URL under **Connection**. For authentication, choose **OAuth**. 3. Read the warning, select **I understand and want to continue**, then **Create as a plugin**. 4. Install the plugin, then sign in with your Qaf account and allow access. To use Qaf in a conversation, type **@** in the message box and select Qaf. In a workspace, admin settings can restrict custom MCP servers. OpenAI's guide: [Add custom MCP server](https://developers.openai.com/api/docs/guides/custom-mcp-server). **Claude Code** 1. Add the server for all your projects: ```bash claude mcp add --transport http --scope user qaf https://api.qaf.ai/mcp ``` 2. In Claude Code, run `/mcp`, select qaf and sign in with your Qaf account in the browser that opens. To sign in from a terminal without a session, run `claude mcp login qaf`. Anthropic's guide: [Connect Claude Code to tools via MCP](https://code.claude.com/docs/en/mcp). **Codex** 1. Add the server: ```bash codex mcp add qaf --url https://api.qaf.ai/mcp ``` 2. Sign in with your Qaf account in the browser that opens: ```bash codex mcp login qaf ``` The Codex CLI, the IDE extension and the ChatGPT desktop app share this setting. It lives in `~/.codex/config.toml`: ```toml [mcp_servers.qaf] url = "https://api.qaf.ai/mcp" ``` OpenAI's guide: [MCP in Codex](https://learn.chatgpt.com/docs/extend/mcp?surface=cli). **Cursor** 1. [Add Qaf to Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=qaf&config=eyJ1cmwiOiJodHRwczovL2FwaS5xYWYuYWkvbWNwIn0=), or add it to `~/.cursor/mcp.json` yourself. If the file exists, add the `qaf` entry to its `mcpServers`. ```json { "mcpServers": { "qaf": { "url": "https://api.qaf.ai/mcp" } } } ``` 2. Open **Customize** in Cursor, turn on qaf, and sign in with your Qaf account when the browser opens. Cursor's guide: [Model Context Protocol](https://cursor.com/docs/mcp). **VS Code** 1. In VS Code, run **MCP: Add Server** from the Command Palette. 2. Choose **HTTP (HTTP or Server-Sent Events)**, paste the server URL and enter `qaf` as the server ID. 3. Select **Global**, then **Copilot Global**, to use it in every workspace. 4. When VS Code starts the server, sign in with your Qaf account in the browser that opens. To share it with everyone who works on a project, add it to `.mcp.json` at the workspace root: ```json { "mcpServers": { "qaf": { "type": "http", "url": "https://api.qaf.ai/mcp" } } } ``` **MCP: Add Server** offers this file only when the experimental `chat.mcp.workspaceRootConfig.enabled` setting is on. An existing `.mcp.json` works with the setting off. VS Code's guide: [Add and use MCP servers in VS Code](https://code.visualstudio.com/docs/agent-customization/mcp-servers). **Other apps** Any app that supports remote MCP servers with OAuth sign-in can connect. It needs: - **Server URL:** `https://api.qaf.ai/mcp` - **Transport:** Streamable HTTP - **Sign-in:** OAuth 2.1 with dynamic client registration and PKCE. The app registers itself; there's no client ID to enter. An app that only runs local (stdio) servers can reach Qaf through the [mcp-remote](https://github.com/punkpeye/mcp-remote) bridge: ```json { "mcpServers": { "qaf": { "command": "npx", "args": ["mcp-remote", "https://api.qaf.ai/mcp"] } } } ``` Building a client? [Sign-in and connected apps](/mcp/authentication#for-client-developers) lists the endpoints and protocol details. ## Troubleshooting **The app can't connect** Check that the URL is exactly `https://api.qaf.ai/mcp`. The app must support remote MCP servers over Streamable HTTP; an app that only runs local servers needs the bridge in **Other apps**. **Your account can't connect** The sign-in page says so when an account can't use the server. A guest session can't connect: sign in to qaf.ai with an account first. An account that is waiting to be deleted, or is suspended, can't connect either. If you think your account should have access, email us at contact@qaf.ai. **The sign-in page doesn't open** Allow pop-ups for the app's site, then try again. On a machine without a browser, run `claude mcp login qaf --no-browser` or `codex mcp login qaf --no-browser` and follow the prompt. **Qaf's tools don't appear** Most apps need the server turned on for each chat or workspace: in Claude, turn on Qaf under **+ › Connectors**; in ChatGPT, type **@** and select Qaf; in Cursor, turn on qaf in **Customize**. If the tools still don't appear, remove the server and add it again. **The app asks you to sign in again** An app stays signed in while you use it. It has to sign in again after 30 days without use, or after you disconnect it. In Claude Code, run `/mcp`, select qaf and choose **Re-authenticate**. In Codex, run `codex mcp login qaf`. In other apps, remove the server and add it again. --- # Tools Source: https://developers.qaf.ai/mcp/tools The Qaf MCP server has three tools. They only read the library: none of them changes anything or reads your account. Each result comes back twice, as structured content and as the same JSON in a text block, so apps that read either one get the full result. Every tool takes `locale`, `en` (the default) or `ar`, which picks the language of book and author names. Passage text stays Arabic. ## `search` Searches the library with hybrid semantic and keyword retrieval, then reranks the matches for relevance. Returns passages with their book, author, printed pages and a link to the page. API equivalent: [`POST /v1/search`](/reference/passages/search-passages). | Input | Type | Limits and default | Description | | --- | --- | --- | --- | | `query` | string | Required. 1–2,000 characters after trimming. | What to search for, in Arabic or English. | | `filters` | object | Optional. Set at least one field. | Restricts results by the passage's book and author, not by what the passage mentions. | | `filters.authorIds` | integer[] | 1–20 IDs | Author IDs from `lookup_catalog` or a passage's `author.id`. | | `filters.bookIds` | string[] | 1–20 UUIDs | Book IDs from `lookup_catalog` or a passage's `book.id`. | | `filters.deathYearAH` | object | At least one of `gt`, `gte`, `lt`, `lte`, as integers | The author's death year in AH. All bounds must hold, and together they must allow at least one year. Leaves out authors with no recorded death year. | | `filters.genres` | string[] | 1–20 slugs | [Genre slugs](/library#genres). A genre describes a book, not a topic. | | `filterOperator` | `"AND"` or `"OR"` | Default `"AND"` | How the filter fields combine: `AND` requires every one, `OR` accepts any. The IDs within one list always combine with OR. | | `limit` | integer | 1–15, default 5 | Passages to return. | | `excludeIds` | string[] | Up to 200 UUIDs | Passages to skip. Pass the IDs you already have to get further passages for the same query. | | `locale` | `"en"` or `"ar"` | Default `"en"` | Language of book and author names. | **Output:** `{ "results": [passage, …] }`, best match first. Each passage has the fields described in [The Qaf library](/library#passages). **Errors the app can correct:** an author or book ID that isn't in the catalog, or filters that no book can match, such as a genre with a death-year range none of its authors fall in. The tool explains the problem so the app can fix its call. ## `fetch` Reads a passage by ID, with up to `radius` neighbouring passages on each side, in reading order. Neighbours carry their own IDs, so reading can continue from either end. API equivalent: [`GET /v1/passages/{id}`](/reference/passages/get-passage). | Input | Type | Limits and default | Description | | --- | --- | --- | --- | | `id` | string | Required. A UUID. | A passage ID from `search` or `fetch`. | | `radius` | integer | 0–5, default 0 | Neighbouring passages to include on each side. | | `locale` | `"en"` or `"ar"` | Default `"en"` | Language of book and author names. | **Output:** the passage's fields, plus `before` and `after`: arrays of passages in reading order. Neighbours come from the same edition, so a passage near the start or end of a book has fewer. **Errors:** an ID with no passage: `No passage has ID ; use an ID returned by search or fetch.` ## `lookup_catalog` Resolves an author or book name to catalog IDs for search filters. Returns up to five candidates, each with a match score. API equivalent: [`GET /v1/catalog/lookup`](/reference/catalog/lookup-catalog). | Input | Type | Limits and default | Description | | --- | --- | --- | --- | | `type` | `"author"` or `"book"` | Required | Whether the name is an author or a book. | | `query` | string | Required. 1–200 characters after trimming. | The name to resolve, in Arabic or English. | | `locale` | `"en"` or `"ar"` | Default `"en"` | Language of the candidate labels. | **Output:** `{ "candidates": [candidate, …] }`. Each candidate has a `type`: | Field | Author | Book | Description | | --- | --- | --- | --- | | `id` | integer | UUID | The ID that `filters.authorIds` or `filters.bookIds` takes. | | `label` | yes | yes | The author's name or the book's title, in `locale`. | | `authorId`, `authorLabel` | | yes | The book's author. | | `deathYearAH` | yes | yes | The author's death year in AH, or `null` when unknown. | | `deathInexactLabel` | yes | yes | A label for an approximate or unknown death year, or `null`. | | `categoryId` | | yes | The book's [genre ID](/library#genres). | | `matchScore` | yes | yes | How closely the name matches the query, from 0 to 1. | When several candidates are close, the app should choose by the question's context or ask you which one you mean. ## Errors A tool that fails returns an error result instead of output: - **Invalid input**, such as a `limit` of 20: the message names the field and the rule. - **Input the app can correct**, such as an unknown author ID: the message explains what to change. - **Anything else**, such as a call that runs past 30 seconds: `Qaf could not complete this request. Try again shortly.` --- # Qaf API Source: https://developers.qaf.ai/reference Search and read classical Arabic Islamic-heritage books from your own code. The API serves the same library as the Qaf MCP server. **Base URL:** `https://api.qaf.ai/v1` **Authentication:** create a key in qaf.ai **Settings › API keys** and send it as a bearer token: `Authorization: Bearer qaf_sk_…`. A 401 response carries a `WWW-Authenticate: Bearer` challenge. Keep keys on your server, never in a browser or app. **Try it** in this reference sends your key from the browser straight to api.qaf.ai, and **Remember on this device** keeps it in this browser: use a separate key for testing and revoke it afterwards. **Rate limits:** 60 requests per minute per account, across all its keys, and 1,000 searches per day. A 429 names the limit in `data.reason`; for `minute`, retry after `Retry-After` seconds. **HEAD** works wherever GET does, with GET's status and headers and no body, and counts against the rate limit like GET. **Errors** are JSON with `code`, `status`, `message` and sometimes `data`. Every response except a CORS preflight has an `X-Request-Id` header; include it when you email us at contact@qaf.ai. Guides and examples: [developers.qaf.ai](https://developers.qaf.ai/api). Version 1.0.0 Base URL: `https://api.qaf.ai/v1` ## Passages Search the library, then read passages and their neighbours. - [`POST /search`](/reference/passages/search-passages) — Search passages. - [`GET /passages/{id}`](/reference/passages/get-passage) — Read a passage. ## Catalog Resolve author and book names to the IDs search filters take. - [`GET /catalog/lookup`](/reference/catalog/lookup-catalog) — Look up an author or book. --- # Look up an author or book Source: https://developers.qaf.ai/reference/catalog/lookup-catalog Resolves an author or book name to the catalog IDs that search filters take. Returns up to five candidates with the author's death year (AH) and a match score. `GET /catalog/lookup` URL: `https://api.qaf.ai/v1/catalog/lookup` **Parameters** - `type` (query, string, required, one of `author`, `book`) — Whether the name is an author or a book. - `query` (query, string, required, 1–200 characters) — The name to resolve, in Arabic or English. - `locale` (query, string, one of `ar`, `en`, default `en`) — Language of the candidate labels; no other is returned. **Responses** - `200` — The candidates for the name. - `401` — The API key is missing, invalid or revoked. - `403` — The key's account can't use the API. - `422` — A field is invalid. data.fieldErrors lists the problems by field. - `429` — Too many requests; data.reason says which limit was reached. For minute, the account's requests per minute, retry after Retry-After seconds. For day, the account's daily search cap, Retry-After is the time until the cap resets. For invalid\_keys, the key doesn't exist or was revoked and this address has sent too many such keys, so fix the key rather than retrying. The X-RateLimit headers describe the account's minute window, so an invalid\_keys refusal, which comes before any key is known, has none. - `500` — Something went wrong on our side. - `503` — Qaf could not complete the request in time, or a service it relies on is unavailable. Retry shortly. Response example, 200: ```json { "candidates": [ { "type": "author", "id": 54, "label": "Ibn Taymiyya", "deathYearAH": 728, "deathInexactLabel": null, "matchScore": 1 }, { "type": "author", "id": 988, "label": "Majd al-Din Ibn Taymiyya", "deathYearAH": 652, "deathInexactLabel": null, "matchScore": 0.95 } ] } ``` --- # Read a passage Source: https://developers.qaf.ai/reference/passages/get-passage Returns a passage by ID, with up to `radius` neighbouring passages on each side, in reading order. Neighbours carry their own IDs, so you can keep reading from either end. `GET /passages/{id}` URL: `https://api.qaf.ai/v1/passages/{id}` **Parameters** - `id` (path, string, required) — A passage ID from search results or from another passage's neighbours. - `radius` (query, integer, 0–5, default `0`) — Neighbouring passages to include on each side. - `locale` (query, string, one of `ar`, `en`, default `en`) — Language for book and author names; Arabic names are always included. **Responses** - `200` — The passage and its neighbours. - `401` — The API key is missing, invalid or revoked. - `403` — The key's account can't use the API. - `404` — No passage has this ID. - `422` — A field is invalid. data.fieldErrors lists the problems by field. - `429` — Too many requests; data.reason says which limit was reached. For minute, the account's requests per minute, retry after Retry-After seconds. For day, the account's daily search cap, Retry-After is the time until the cap resets. For invalid\_keys, the key doesn't exist or was revoked and this address has sent too many such keys, so fix the key rather than retrying. The X-RateLimit headers describe the account's minute window, so an invalid\_keys refusal, which comes before any key is known, has none. - `500` — Something went wrong on our side. - `503` — Qaf could not complete the request in time, or a service it relies on is unavailable. Retry shortly. Response example, 200: ```json { "id": "d9335f69-e54b-59ae-a78c-50db734b8d50", "title": "Fayd al-Qadir — Abd al-Rauf al-Munawi, vol. 4, p. 233", "url": "https://app.turath.io/book/21660?print_page=4%2C233", "text": "… ٥١٣٤ - (الصبر) الكثير الثواب: الصبر (عند أول صدمة) أي عند فورة المصيبة …", "pages": [ { "vol": "4", "page": 233 }, { "vol": "4", "page": 234 } ], "book": { "id": "31f522f9-b85c-5c78-a8ff-dc48ebce183c", "title": "Fayd al-Qadir", "titleArabic": "فيض القدير" }, "author": { "id": 423, "name": "Abd al-Rauf al-Munawi", "nameArabic": "عبد الرؤوف المناوي" }, "versionId": 21660, "sequenceNumber": 4929, "before": [ { "id": "0d4b07f3-22a9-596d-8c73-9cd01630967f", "title": "Fayd al-Qadir — Abd al-Rauf al-Munawi, vol. 4, p. 233", "url": "https://app.turath.io/book/21660?print_page=4%2C233", "text": "… وبالصبر يفتح كل باب مغلق …", "pages": [ { "vol": "4", "page": 233 } ], "book": { "id": "31f522f9-b85c-5c78-a8ff-dc48ebce183c", "title": "Fayd al-Qadir", "titleArabic": "فيض القدير" }, "author": { "id": 423, "name": "Abd al-Rauf al-Munawi", "nameArabic": "عبد الرؤوف المناوي" }, "versionId": 21660, "sequenceNumber": 4928 } ], "after": [ { "id": "e16a944f-0af5-51d8-9c75-6ac535d3c92a", "title": "Fayd al-Qadir — Abd al-Rauf al-Munawi, vol. 4, p. 234", "url": "https://app.turath.io/book/21660?print_page=4%2C234", "text": "… لأن الصبر يدخل في كل باب بل في كل مسألة من مسائل الدين …", "pages": [ { "vol": "4", "page": 234 } ], "book": { "id": "31f522f9-b85c-5c78-a8ff-dc48ebce183c", "title": "Fayd al-Qadir", "titleArabic": "فيض القدير" }, "author": { "id": 423, "name": "Abd al-Rauf al-Munawi", "nameArabic": "عبد الرؤوف المناوي" }, "versionId": 21660, "sequenceNumber": 4930 } ] } ``` --- # Search passages Source: https://developers.qaf.ai/reference/passages/search-passages Searches the library with hybrid semantic and keyword retrieval, reranked for relevance. Returns passages with their book, author, printed pages and a link to the printed page. To restrict results to an author or book, resolve its name to an ID with GET /catalog/lookup first. Body fields not listed here are refused with a 422. `POST /search` URL: `https://api.qaf.ai/v1/search` **Request body** (`application/json`, required) - `query` (string, required, 1–2000 characters) — What to search for, in Arabic or English. - `filters` (object) — Restrict results by author, book, death year or genre. Filters match a passage's book and author, not what the passage mentions. - `authorIds` (integer[], 1–20 items, each at least 1) — Author IDs, from GET /catalog/lookup or a passage's author.id. - `bookIds` (string[], 1–20 items) — Book IDs, from GET /catalog/lookup or a passage's book.id. - `deathYearAH` (object) — Bounds on the author's death year, in AH. Give at least one; all must hold. - `gt` (integer) - `gte` (integer) - `lt` (integer) - `lte` (integer) - `genres` (string[], 1–20 items, each one of `creed`, `sects-and-refutations`, `quran-commentary`, `quranic-sciences-and-principles-of-exegesis`, `quran-recitation-and-readings`, `hadith-collections`, `hadith-commentary`, `hadith-sourcing-and-indexes`, `hadith-defects-and-scholarly-questions`, `hadith-sciences`, `principles-of-jurisprudence`, `legal-maxims-and-jurisprudence`, `logic`, `hanafi-fiqh`, `maliki-fiqh`, `shafii-fiqh`, `hanbali-fiqh`, `general-fiqh`, `fiqh-issues`, `islamic-governance-and-judiciary`, `inheritance-and-wills`, `fatwas`, `spirituality-etiquette-and-remembrances`, `prophetic-biography`, `history`, `biographies-and-generations`, `genealogy`, `geography-and-travel`, `arabic-language`, `rare-words-and-dictionaries`, `grammar-and-morphology`, `literature`, `prosody-and-rhyme`, `poetry`, `rhetoric`, `collected-works-and-compilations`, `bibliographies-and-guides`, `medicine`, `general-books`, `other-sciences`) — Genre slugs. A genre describes a book, not a topic, so use one only to search books of that genre. - `filterOperator` (string, one of `AND`, `OR`) — Combine supplied filter dimensions with AND (default) or OR. Values within each ID list always use OR; all death-year bounds must hold together. - `limit` (integer, 1–15, default `5`) — Passages to return. - `excludeIds` (string[], up to 200 items) — IDs of passages you already have; the search skips them. Pass the IDs from earlier results to get further passages for the same query. - `locale` (string, one of `ar`, `en`, default `en`) — Language for book and author names; Arabic names are always included. Request body example: ```json { "query": "الصبر عند المصيبة" } ``` **Responses** - `200` — The matching passages, ranked by relevance. - `400` — The request can't be processed as sent, such as a filter with an unknown author ID. data.reason, when present, says why. - `401` — The API key is missing, invalid or revoked. - `403` — The key's account can't use the API. - `413` — The request body is larger than 64 KiB. - `422` — A field is invalid. data.fieldErrors lists the problems by field. - `429` — Too many requests; data.reason says which limit was reached. For minute, the account's requests per minute, retry after Retry-After seconds. For day, the account's daily search cap, Retry-After is the time until the cap resets. For invalid\_keys, the key doesn't exist or was revoked and this address has sent too many such keys, so fix the key rather than retrying. The X-RateLimit headers describe the account's minute window, so an invalid\_keys refusal, which comes before any key is known, has none. - `500` — Something went wrong on our side. - `503` — Qaf could not complete the request in time, or a service it relies on is unavailable. Retry shortly. Response example, 200: ```json { "results": [ { "id": "d9335f69-e54b-59ae-a78c-50db734b8d50", "title": "Fayd al-Qadir — Abd al-Rauf al-Munawi, vol. 4, p. 233", "url": "https://app.turath.io/book/21660?print_page=4%2C233", "text": "… ٥١٣٤ - (الصبر) الكثير الثواب: الصبر (عند أول صدمة) أي عند فورة المصيبة …", "pages": [ { "vol": "4", "page": 233 }, { "vol": "4", "page": 234 } ], "book": { "id": "31f522f9-b85c-5c78-a8ff-dc48ebce183c", "title": "Fayd al-Qadir", "titleArabic": "فيض القدير" }, "author": { "id": 423, "name": "Abd al-Rauf al-Munawi", "nameArabic": "عبد الرؤوف المناوي" }, "versionId": 21660, "sequenceNumber": 4929 } ] } ```