Jawami' al-Kalim API

A JSON API over the whole hadith database behind جوامع الكلم: 1,400 books, about 825,000 narrations, 638,000 isnads and 50,000 narrators. Every table is queryable, and you can report mistakes. No key needed.

Quick start

The base URL is https://hadith.adhamzahran.com/api. Every example below has a Try button that runs it.

A narrator (Ibn Abbas), with a link to him on the website:

GET /api/narrators/4883Try

The first five sahih narrations of Sahih al-Bukhari (book 146), only two columns:

GET /api/hadith?book_id=146&hokm=0&fields=bhid,cluster_id&limit=5Try

Search, the same search as the website:

GET /api/search?q=إنما الأعمال بالنيات&book=146Try
curl -G "https://hadith.adhamzahran.com/api/search" \
  --data-urlencode 'q=إنما الأعمال بالنيات' --data-urlencode book=146

Conventions

Routes

RequestReturns
GET /api/This API as JSON: every table with its columns (and which are indexed), keys and foreign keys, plus the meaning of coded values.
GET /api/{table}Rows of a table, with filters, paging and column selection. Works for every table, see Tables.
GET /api/{table}/{key}One row by primary key. A composite key is joined with commas, in the table's key order: /api/commentaries/146_1,1.
GET /api/search?q=The website's hadith search. See Search.
GET /api/hadith_fts?q=Raw full-text search over every narration (FTS5 syntax).
POST /api/reportsReport a mistake in the data or the website. See Reports.
GET /api/reports, GET /api/reports/{id}The reports so far, newest first.
GET /api/docsThis page.

Filtering, columns and order

Any column of a table can be a filter: column=value means equals, and column=op.value uses an operator. Several filters must all match.

OperatorMeaningExample
eq (default)equalsbook_id=146
neqnot equalshokm=neq.0
gt gte lt ltecomparisonsdeath_year=gte.100
likepattern, * matches anything. Case-sensitive; every other character is literal.name=like.عبد الله*
inone of a comma-separated list (up to 1000)book_id=in.146,158
isis.null or is.notnullbadeel=is.notnull

Paging and totals

A list returns:

{
  "data": [ { "book_id": "146", "name": "صحيح البخاري" }, ... ],
  "limit": 2,
  "offset": 0,
  "next": "/api/books?fields=book_id%2Cname&limit=2&count=1&after=1402",
  "total": 1400
}

A single row (/api/narrators/4883?fields=id,name,rotba,wasf_rotba) is returned bare, not wrapped in data:

{
  "id": 4883,
  "name": "عبد الله بن عباس بن عبد المطلب بن هاشم بن عبد مناف",
  "rotba": 1,
  "wasf_rotba": "صحابي",
  "ui_url": "https://hadith.adhamzahran.com/#view=narrator&id=4883"
}

There are three ways to search, from the most convenient to the most flexible.

1. /api/search: the website's search

ParameterMeaning
qrequired. The words to find. Diacritics (tashkeel) are ignored.
bookonly this book, by book_id (146 is Sahih al-Bukhari).
hokmonly this grading, 0 sahih to 5 mawdu', see Codes.
page, limitpage number (default 1) and page size (default 20, max 100).
{
  "total": 26527, "page": 1, "pages": 26527,
  "results": [ {
    "bhid": "2028_6", "book_id": "2028", "hadith_num": 6,
    "book_name": "...", "author": "...",
    "snippet": "...إلى <mark>عماله</mark> : إياكم أن <mark>تستعملوا</mark> على...",
    "cluster_id": null, "hokm": null, "hokm_label": "غير محدد",
    "ui_url": "https://hadith.adhamzahran.com/#view=hadith&bhid=2028_6"
  } ],
  "root_used": "عمل", "morphological": true, "form_count": 400,
  "ui_url": "https://hadith.adhamzahran.com/#view=search&q=..."
}

It runs the same code as the website's search box, so the results match what you see there. The top-level ui_url opens the same search on the website.

2. /api/hadith_fts?q=: raw full-text syntax

For when you want control: "exact phrase", a OR b, word*, a NOT b. Rows come ranked by relevance with a snippet, and can be combined with the usual filters and fields. A malformed query is a 400. There is no root expansion here.

GET /api/hadith_fts?q="إنما الأعمال"&book_id=146&fields=bhid&limit=3Try

3. Filters on any table

Everything else (narrator names, critics' wording, commentary text, dictionary entries) is searched with like: /api/critic_opinions?opinion=like.*اختلط*. A prefix match (name=like.عبد*) is instant on an indexed column; a match in the middle of long text reads the table.

Limits worth knowing

Every hadith and every narrator comes with a link to its page on the website, so a result is one click from the full view (the isnad tree, the takhreej, the biography). The links are added to the rows by the API, whichever table they come from, and even when fields= left the key column out.

The row…GetsExample
is a narration (hadith, hadith_text, hadith_fts) or a narrator (narrators)ui_urlhttps://hadith.adhamzahran.com/#view=hadith&bhid=146_1
points at one through a column (bhid, rawy_id)bhid_ui_url, rawy_id_ui_url…/#view=narrator&id=4883
holds a comma-separated list of narrators (sahabi_id, rawy_ids)an array: sahabi_id_ui_urls, rawy_ids_ui_urlsin the same order as the ids
is a search (/api/search)ui_url on every result, and one for the whole search…/#view=search&q=…

Recipes

Common questions and the requests that answer them.

Find a narrator, then everything about him

GET /api/narrators?name=like.*عباس*&fields=id,nameTry
GET /api/critic_opinions?rawy_id=4883Try

The second one is what every critic said about him; alem_id is the critic, see /api/critics/{alem_id}.

Every isnad a narrator appears in

The isnads store their narrators as a comma-separated list (sanad.rawy_ids), which cannot be searched efficiently. So the same data is also split into a table with one row per narrator in a chain:

GET /api/sanad_rawy?rawy_id=4883&limit=5Try

Each row says which isnad (sanad_id) and where in it he stands (pos, counted from 0: 0 is the compiler's side, the last position is the companion). Fetch the isnad itself with /api/sanad/{sanad_id}, which carries its bhid, grading and every narrator's link. The same pattern covers companions: /api/hadith_sahabi?rawy_id=4883 lists the narrations he is the companion of, and book_cluster_sahabi does the same for book_clusters.

The isnads of one narration, and its text

GET /api/sanad?bhid=146_1Try
GET /api/hadith_text/146_1Try

The same hadith in every book

Narrations of the same hadith share a cluster_id:

GET /api/hadith?cluster_id=29&fields=bhid&limit=10Try
GET /api/clusters/29Try

Commentary on a narration

GET /api/commentaries?bhid=146_1Try

commentary_book says which: 1 Fath al-Bari, 2 Sharh al-Nawawi, 3 Awn al-Ma'bud, 4 Tuhfat al-Ahwadhi.

A word's root

GET /api/word_forms?form=الأعمالTry

Reports

If you find a mistake in the data, or a bug in the website or this API, report it here. This is the only route that takes a POST. Reports are public (anyone can read them below), so don't put personal information in one. They are kept in a file of their own, so they are not part of the database download.

Send a report

curl -X POST https://hadith.adhamzahran.com/api/reports \
  -H 'Content-Type: application/json' \
  -d '{"kind": "data",
       "problem": "missing",
       "table": "sanad",
       "bhid": "146_4412",
       "narrator_id": 3954,
       "message": "No sanad row. The text names 1548, 896, 1302, 3954, 4049.",
       "suggested": "1548,896,1302,3954,4049",
       "source": "islamic-debate.adhamzahran.com"}'

Only kind and message are required.

FieldMeaning
kindrequired. data for a mistake in the database, website for a mistake or bug in the website or the API.
problemoptional. What kind of mistake: wrong_value (what is stored is wrong), missing (something that should be there is not), unexpected (something is there that should not be) or other.
tableoptional, only for kind=data. The table the mistake is in, as named under Tables.
bhidoptional. The narration it concerns, <book_id>_<number> like 146_4412, whichever table the mistake is in (a sanad row, a commentary, a grading, …). Not looked up, so a narration that is missing can be reported.
narrator_idoptional. The narrator it concerns: a narrators.id, a whole number. Not looked up either.
keyoptional, only for kind=data, needs table. The primary key of the row that exists and is wrong, written as in /api/{table}/{key} (a composite key is comma-joined; a number is fine). Not checked against the table. Up to 200 characters. Leave it out when the row is missing: say which narration with bhid.
columnoptional, only for kind=data, needs table. The column that is wrong; it must be a column of that table.
messagerequired. What is wrong. Up to 4,000 characters.
suggestedoptional. What the value, or the row, should be. Up to 4,000 characters; a number is fine.
sourceoptional. Who is reporting, such as the name of a site or tool. It is whatever you write: nothing checks it. Up to 100 characters.
{
  "id": 7,
  "created_at": "2026-10-25T14:11:59Z",
  "kind": "data",
  "status": "open",
  "table": "sanad",
  "key": null,
  "column": null,
  "message": "No sanad row. The text names 1548, 896, 1302, 3954, 4049.",
  "problem": "missing",
  "bhid": "146_4412",
  "narrator_id": 3954,
  "suggested": "1548,896,1302,3954,4049",
  "source": "islamic-debate.adhamzahran.com",
  "bhid_ui_url": "https://hadith.adhamzahran.com/#view=hadith&bhid=146_4412",
  "narrator_id_ui_url": "https://hadith.adhamzahran.com/#view=narrator&id=3954"
}

Limits. A body can be 64 KB at most. Each address can send 10 reports an hour, and everyone together 1,000 a day; past that the answer is 429 with a Retry-After header (seconds).

Read the reports

GET /api/reportsTry
GET /api/reports?status=open&kind=dataTry
GET /api/reports?bhid=146_4412Try
GET /api/reports?problem=missing&table=sanadTry

Reports behave like any table: the same filters, fields, order, paging and count, and /api/reports/{id} for one. They come newest first unless you set order. The columns are the ones in the response above, so any of them filters: everything reported about a narration (bhid=), a narrator (narrator_id=) or a table, everything of one problem, everything from one source. status starts as open and becomes fixed or rejected (the Codes list the values of kind, problem and status). Links to the website come as for any table: bhid_ui_url and narrator_id_ui_url, and ui_url when table and key name a narration or a narrator.

The same list is a page on the website: /#view=reports.

Errors

Errors are JSON with the reason, never HTML: {"error": "unknown column: bogus"}.

StatusWhen
400an unknown column or operator, a malformed value, an invalid full-text query, a missing q, a filter on hadith_text that would scan it, or a report with a missing or invalid field.
404an unknown table, or no row with that key.
405anything but GET, HEAD or OPTIONS, except POST /api/reports.
413a report whose body is over 64 KB.
415a report that is neither JSON nor application/x-www-form-urlencoded.
429too many reports from this address, or from everyone, lately. See Retry-After.
500a server fault. The message is always just internal error; the details are only in the server log.

Tables

Every table in the database, read live from /api/. indexed columns filter and sort fast. Click a table for its columns.

Loading…

Codes

Columns whose values stand for something, read live from /api/.

Loading…

Bulk download

If you want all of it, don't page through the API: download the whole database as one SQLite file from Hugging Face (under 600 MB compressed). The API is read-only on that same file, so every column and table listed here is in it, and the table descriptions are on that page. Reports are the exception: they are not part of the database.