Developer docs
Build with the CryptoHalal public API.
Human-readable guides to the public CryptoHalal API. These pages are free to read; the AI-facing index and agent guide are reserved for paying customers and live in your dashboard.
CryptoHalal API — Developer Guide
API version: v1 (multi-source). See changelog.md for incremental updates since v1.
Last updated: 2026-06-21T19:15 UTC
The official developer reference for the CryptoHalal REST API. This document is the authoritative, self-contained guide for paying customers integrating Shariah crypto-ruling data into bots, wallets, screeners, and research tools.
- Base URL:
https://api.cryptohalalstatus.com - Interactive schema (Swagger UI):
https://api.cryptohalalstatus.com/docs - Live machine-readable contract:
GET https://api.cryptohalalstatus.com/v1/meta - Versioned changelog:
GET https://api.cryptohalalstatus.com/v1/docs/changelog
Table of contents
- Introduction
- Authentication
- Quickstart
- Full endpoint reference
- The verdict model in depth
- Rate limits
- Error handling
- Code examples
- Best practices
1. Introduction
The CryptoHalal API is a read-only HTTP API that exposes structured Shariah rulings for cryptocurrencies. Every ruling is source-attributed: it republishes structured data extracted from public source ruling cards (the Crypto Islam Telegram channel @crypoislam and the CryptoHalal / كريبتو حلال channel @cr_halal) along with the extraction metadata. The API does not issue independent religious, financial, or investment rulings of its own — it surfaces what each source published, in a structured, queryable form.
Multi-source: rulings are per (coin, source)
The API publishes rulings per (coin, source). A single coin can carry a different verdict from each source, returned as separate rows distinguished by the source field. The same ?q=BTC&exact=true may return up to two rows — one per source — and when both sources have ruled the same coin the response sets ambiguous: true with item: null and lists both per-source verdicts in items[]. See §4.14 for the full contract.
source value | Telegram channel | Card shape | Where the "why" lives |
|---|---|---|---|
"Crypto Islam" | @crypoislam | Verdict panel + reason | reason_summary_<lang> + reason_points_<lang> (translated into 6 languages) |
"Crypto Halal" | @cr_halal (display CRYPTOHALAL كريبتو حلال) | Header + Project + Services + Use-of + Judgment | notes / notes_en / notes_ar (project description) + services_<lang> (translated into 6 languages) + use_cases_en / use_cases_ar (bilingual only) |
The verdict model
Each asset (per source) carries one of four verdict values (the field is named judgment in responses):
| Verdict | Meaning |
|---|---|
halal | Permitted according to the source ruling card. |
haram | Not permitted according to the source ruling card. |
suspicious | Contains suspicion / doubt according to the source ruling card. Treat as caution, not approval. |
unknown | The ruling could not be determined from the source card. This means not yet ruled — it is NOT an implicit "halal". |
Each ruling includes the originating source, a human-readable reason (or the source-appropriate "why" — reason_summary_* for Crypto Islam, services_* / use_cases_* / notes_* for CryptoHalal), a confidence score, source/verdict dates, and — when the source has revised a verdict — ruling-change history linking the previous and current verdicts. See §5 for full semantics.
What you can do
- Look up the current verdict for a single ticker or name (
/v1/status/check,/v1/status/current) — up to two rows per coin in v1. - Check many assets in one call (
/v1/status/check/bulk). - Page through the full current dataset with cursor pagination (
/v1/status/current,/v1/status/latest). - Read a ruling's revision timeline (
/v1/status/history— entitled plans). - Fetch a single ruling by its stable ID.
- Discover the contract, plans, and your own account limits (
/v1/meta,/v1/plans,/v1/account). - Read the versioned changelog (
/v1/docs/changelog) for what changed since v1.
2. Authentication
How keys are issued
API access is available through a self-serve Free tier or paid plans:
- Free: create an account, verify your email, then claim one Free API key from the dashboard. Free responses require visible CryptoHalalStatus attribution.
- Paid: choose a plan (Growth, Pro, or Unlimited) in the customer portal.
- Pay in stablecoin (USDT / USDC) and submit your payment proof.
- An administrator reviews and approves the subscription.
- On approval an API client is provisioned for your account, and you generate your API key from the dashboard. The plaintext key is shown only once — store it immediately in a secret manager.
Production keys are prefixed chal_live_ (test/non-production environments issue chal_test_ keys).
Your subscription must remain active and approved for the key to work. If the subscription expires, or the client is suspended/unpaid, requests are rejected with
403(see §7).
Sending the key
Send your key on every request to a protected endpoint. Two header forms are accepted; the dedicated header takes precedence if both are present:
X-API-Key: chal_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
or
Authorization: Bearer chal_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Public vs. protected endpoints
These endpoints are public (no key required):
GET /v1/metaGET /v1/plansGET /v1/showcaseGET /v1/docs(integration docs index)GET /v1/docs/developer-guideGET /v1/docs/integrators-quickstartGET /v1/docs/changelog(the versioned changelog)
The AI-facing docs do require a key (delivered to paid portal users via /portal/api/integration-docs as well):
GET /v1/docs/llm-agent-guide(X-API-Key)GET /v1/docs/llms.txt(X-API-Key)
Every other /v1/... endpoint requires a valid API key. Public endpoints are rate-limited by IP; protected endpoints are rate-limited per API client (see §6).
3. Quickstart
Your first authenticated request — look up Bitcoin's current verdict. In v1 the response can carry up to two rows for the same coin (one per source); a coin ruled by only one source is shown below.
curl -H "X-API-Key: chal_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
"https://api.cryptohalalstatus.com/v1/status/check?q=BTC&exact=true"
Example response (Crypto Islam only — see §4.14 for the dual-source shape):
{
"query": "BTC",
"exact": true,
"found": true,
"ambiguous": false,
"match_count": 1,
"item": {
"ruling_id": 412,
"ruling_uid": "crypoislam:52:0",
"current_status_uid": "crypoislam:status:bitcoin-btc",
"source": "Crypto Islam",
"source_image_sha256": "9f2c1a...e7",
"source_post_url": "https://t.me/crypoislam/52",
"source_timestamp": "2026-05-30T11:04:00Z",
"observed_name": "Bitcoin",
"observed_ticker": "BTC",
"canonical_asset_id": null,
"canonical_name": null,
"canonical_ticker": null,
"judgment": "halal",
"verdict_text_en": "Permitted",
"verdict_text_ar": "حلال",
"verdict_text_ru": "Халяль (дозволено)",
"verdict_text_fa": "حلال (مجاز)",
"verdict_text_tr": "Helal",
"verdict_text_uz": "Halol",
"verdict_panel_color": "green",
"reason_summary_en": "Decentralized payment network with no interest-bearing mechanism.",
"reason_summary_ar": "...",
"reason_summary_ru": "...", "reason_summary_fa": "...", "reason_summary_tr": "...", "reason_summary_uz": "...",
"reason_points_en": ["No riba", "Utility as a medium of exchange"],
"reason_points_ar": ["..."],
"reason_points_ru": ["..."], "reason_points_fa": ["..."], "reason_points_tr": ["..."], "reason_points_uz": ["..."],
"services_en": [], "services_ar": [], "services_ru": [], "services_fa": [], "services_tr": [], "services_uz": [],
"use_cases_en": [], "use_cases_ar": [],
"observed_chain": "Bitcoin",
"observed_contract_address": null,
"verdict_date_gregorian": "2026-05-30",
"verdict_date_hijri": "1447-11-13",
"is_changed_ruling": false,
"previous_ruling_uid": null,
"previous_judgment": null,
"changed_from": null,
"changed_to": null,
"changed_at": null,
"confidence": 0.95,
"requires_review": false,
"publication_status": "published",
"extracted_at": "2026-05-30T11:06:12Z",
"notes": null,
"notes_en": null, "notes_ar": null, "notes_ru": null, "notes_fa": null, "notes_tr": null, "notes_uz": null
},
"items": [ /* same item as above */ ],
"disclaimer": "This response republishes structured data extracted from Crypto Islam source posts. It is not independent religious, financial, or investment advice."
}
That's it — you're integrated. The rest of this guide documents every endpoint, every field, and the multi-source model.
4. Full endpoint reference
All paths are relative to https://api.cryptohalalstatus.com. Auth column: 🔓 = public (no key), 🔑 = API key required.
| Auth | Method | Path | Purpose |
|---|---|---|---|
| 🔓 | GET | /v1/meta | API contract / discovery metadata. |
| 🔓 | GET | /v1/plans | Public plans, limits, entitlements, prices. |
| 🔓 | GET | /v1/showcase | One example ruling per verdict (for landing pages). |
| 🔓 | GET | /v1/terms | Terms of Service. |
| 🔓 | GET | /v1/docs | Index of integration docs. |
| 🔓 | GET | /v1/docs/developer-guide | This guide. |
| 🔓 | GET | /v1/docs/integrators-quickstart | Quickstart for new integrators. |
| 🔓 | GET | /v1/docs/changelog | Versioned changelog. v1 is the baseline. |
| 🔑 | GET | /v1/docs/llms.txt | Machine-readable API index (AI agents). |
| 🔑 | GET | /v1/docs/llm-agent-guide | Dense AI-agent guide. |
| 🔑 | GET | /v1/status/check | Single status lookup by ticker/name. May return up to 2 rows (one per source). |
| 🔑 | POST | /v1/status/check/bulk | Bulk status lookup (entitled plans). |
| 🔑 | GET | /v1/status/current | Paginated current rulings; q is REQUIRED. |
| 🔑 | GET | /v1/status/latest | Alias of /v1/status/current; q is REQUIRED. |
| 🔑 | GET | /v1/status/history | Ruling-change history (entitled plans). |
| 🔑 | GET | /v1/changes | Dataset-wide delta feed since a timestamp/cursor (requires history entitlement). |
| 🔑 | GET | /v1/stats | Dataset-wide counts and freshness. |
| 🔑 | GET | /v1/rulings/{ruling_uid} | One ruling by stable ID. |
| 🔑 | GET | /v1/mentions/{mention_id} | Compatibility alias of /v1/rulings/{id}. |
| 🔑 | GET | /v1/account | Your client, plan, limits, entitlements. |
| 🔑 | GET | /v1/account/usage | Your usage counters per window. |
Common response headers. Every
/v1response carriesX-Request-ID(echo it in support requests; you may supply your own via theX-Request-IDrequest header — letters/digits/_/./:/-, up to 128 chars). Successful authenticated responses also includeX-RateLimit-*headers (§6), and cacheableGETresponses includeCache-Control(§9).
4.1 GET /v1/meta 🔓
Machine-readable description of the API contract: judgment meanings, the preferred status endpoint, the stable ID field, the full endpoint map, cache and rate-limit settings, and the auth header name. Use it to bootstrap a client without hardcoding values.
Parameters: none.
Example request:
curl "https://api.cryptohalalstatus.com/v1/meta"
Example response (PublicApiMetaResponse):
{
"name": "Crypto Halal API",
"version": "1.0.0",
"channel": "cryptoislam",
"source_channel_url": "https://t.me/cryptoislam",
"documentation_url": "/docs",
"plans_endpoint": "/v1/plans",
"disclaimer": "This API republishes structured data extracted from Crypto Islam source posts. It does not issue independent religious, financial, or investment advice.",
"preferred_status_endpoint": "/v1/status/check?q={ticker_or_name}&exact=true",
"stable_id_field": "ruling_uid",
"judgment_values": {
"halal": "Permitted according to the source ruling card.",
"haram": "Not permitted according to the source ruling card.",
"suspicious": "Contains suspicion according to the source ruling card.",
"unknown": "Ruling could not be determined from the source card."
},
"endpoints": {
"stats": "/v1/stats",
"plans": "/v1/plans",
"status_check": "/v1/status/check?q={ticker_or_name}&exact=true",
"bulk_status_check": "/v1/status/check/bulk",
"account": "/v1/account",
"account_usage": "/v1/account/usage",
"current_statuses": "/v1/status/current",
"latest_statuses_alias": "/v1/status/latest",
"history": "/v1/status/history",
"changes": "/v1/changes",
"terms": "/v1/terms",
"ruling_detail": "/v1/rulings/{ruling_uid}"
},
"cache_seconds": 60,
"rate_limit_per_minute": 120,
"rate_limit_scope": "client",
"api_key_required": true,
"api_key_header": "X-API-Key",
"cors_origins": ["https://app.cryptohalalstatus.com"],
"cursor_pagination": true
}
Field reference:
| Field | Type | Description |
|---|---|---|
name | string | API product name. |
version | string | API version. |
channel | string | Source channel slug. |
source_channel_url | string | URL of the source channel. |
documentation_url | string | Path to the Swagger UI (/docs). |
plans_endpoint | string | Path to the plans endpoint. |
disclaimer | string | Informational-not-advice disclaimer. |
preferred_status_endpoint | string | Recommended lookup pattern for integrators. |
stable_id_field | string | The field to use as a stable identifier (ruling_uid). |
judgment_values | object | Map of each verdict value to its meaning. |
endpoints | object | Map of logical endpoint name → path. |
cache_seconds | integer | max-age applied to cacheable GET /v1 responses (0 = caching disabled). |
rate_limit_per_minute | integer | Per-IP minute limit for the public/legacy path (not your plan's client limit). |
rate_limit_scope | string | Scope of plan rate limiting; "client". |
api_key_required | boolean | Whether protected endpoints require a key (normally true). |
api_key_header | string | The header name to send your key in (X-API-Key). |
cors_origins | string[] | Allowed browser CORS origins. |
cursor_pagination | boolean | Whether list endpoints use cursor pagination (true). |
4.2 GET /v1/plans 🔓
Lists the active public plans with their request windows, page/bulk limits, entitlements, and monthly price. Internal plans are excluded. Returns a JSON array of PublicApiPlanResponse.
Parameters: none.
Example request:
curl "https://api.cryptohalalstatus.com/v1/plans"
Example response:
[
{
"slug": "free",
"name": "Free",
"description": "Self-serve starter plan for low-volume attribution-required API use.",
"price_usd_monthly": 0.0,
"time_limits": { "minute": 60, "hour": null, "day": null, "month": null },
"request_limits": { "max_page_size": 25, "max_bulk_size": 10, "distinct_cap": 50, "max_band": "top50" },
"entitlements": { "bulk": true, "history": false },
"unlimited": false
},
{
"slug": "standard",
"name": "Growth",
"description": "Entry plan for status checks and current ruling data.",
"price_usd_monthly": 30.0,
"time_limits": { "minute": 60, "hour": null, "day": null, "month": null },
"request_limits": { "max_page_size": 50, "max_bulk_size": 25, "distinct_cap": 250, "max_band": "top250" },
"entitlements": { "bulk": true, "history": false },
"unlimited": false
},
{
"slug": "upper",
"name": "Pro",
"description": "For production apps and bots that need higher limits and full ruling history.",
"price_usd_monthly": 50.0,
"time_limits": { "minute": 60, "hour": null, "day": null, "month": null },
"request_limits": { "max_page_size": 100, "max_bulk_size": 50, "distinct_cap": 1000, "max_band": "top1000" },
"entitlements": { "bulk": true, "history": true },
"unlimited": false
},
{
"slug": "unlimited",
"name": "Unlimited",
"description": "Top-tier access with the highest limits and full ruling history.",
"price_usd_monthly": 80.0,
"time_limits": { "minute": 60, "hour": null, "day": null, "month": null },
"request_limits": { "max_page_size": 200, "max_bulk_size": 100, "distinct_cap": null, "max_band": "all" },
"entitlements": { "bulk": true, "history": true },
"unlimited": false
}
]
Field reference (per plan):
| Field | Type | Description |
|---|---|---|
slug | string | Plan identifier. Note: Growth uses slug upper; Pro uses slug unlimited. |
name | string | Human-readable plan name. |
description | string | null | Plan description. |
price_usd_monthly | number | null | Monthly price in USD. |
time_limits | object | Request limits per window: minute, hour, day, month. A null value means no limit for that window. |
request_limits | object | max_page_size (max limit per list call), max_bulk_size (max queries per bulk call), distinct_cap (distinct coin ids served per API key per calendar month), and max_band (highest market-cap band visible on the plan). null = unlimited where applicable. |
entitlements | object | Booleans: bulk, history. |
unlimited | boolean | true only when every time-limit and request-limit is unlimited. (Note: the public Pro plan still has a 60/min limit and page/bulk caps, so this flag is false for it.) |
See §6 for the full per-plan numbers.
4.3 GET /v1/showcase 🔓
Returns a small set of example rulings (typically one per verdict: halal / haram / suspicious) for use on a landing page. Returns a JSON array of ShowcaseItemResponse.
Parameters: none.
Example request:
curl "https://api.cryptohalalstatus.com/v1/showcase"
Example response:
[
{
"name": "Bitcoin",
"ticker": "BTC",
"judgment": "halal",
"reason": "Decentralized payment network with no interest-bearing mechanism.",
"source": "Crypto Islam",
"date": "2026-05-30"
},
{
"name": "Example Lending Token",
"ticker": "XLT",
"judgment": "haram",
"reason": "Core protocol pays interest (riba).",
"source": "Crypto Islam",
"date": "2026-04-18"
}
]
Field reference:
| Field | Type | Description |
|---|---|---|
name | string | Observed asset name (may be empty if only a ticker is known). |
ticker | string | Observed ticker (may be empty). |
judgment | string | Verdict: halal | haram | suspicious | unknown. |
reason | string | null | Short reason summary, or the first reason point. |
source | string | Source label (e.g. "Crypto Islam"). |
date | string | null | Verdict date (Gregorian) or the source date. |
4.4 GET /v1/status/check 🔑
The preferred single-lookup endpoint for bots and wallets. Resolves one query (ticker, observed name, or identity) to its current ruling. Defaults to exact matching so an integration never silently acts on a fuzzy match.
Query parameters:
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
q | string | yes | — | Ticker, observed name, or identity to check (min length 1). |
exact | boolean | no | true | Require an exact ticker/name/identity match. |
limit | integer | no | 3 | Max matches returned when ambiguous. Range 1–10. Also capped by your plan's max_page_size. |
Example request:
curl -H "X-API-Key: $KEY" \
"https://api.cryptohalalstatus.com/v1/status/check?q=BTC&exact=true"
Example response (StatusCheckResponse):
{
"query": "BTC",
"exact": true,
"found": true,
"ambiguous": false,
"match_count": 1,
"item": { /* a StatusItem — see §4.11 */ },
"items": [ { /* StatusItem */ } ],
"disclaimer": "This response republishes structured data extracted from Crypto Islam source posts. It is not independent religious, financial, or investment advice."
}
Field reference:
| Field | Type | Description |
|---|---|---|
query | string | The normalized query that was searched. |
exact | boolean | Whether exact matching was applied. |
found | boolean | true if at least one match was returned. |
ambiguous | boolean | true if more than one match was found (or the search was inherently ambiguous). |
match_count | integer | Number of matches in items. |
item | StatusItem | null | The single resolved ruling. Populated only when there is exactly one unambiguous match; null when found=false or ambiguous=true. |
items | StatusItem[] | All matches (empty when not found). When ambiguous=true, iterate this and present the choices. |
disclaimer | string | Informational-not-advice disclaimer. |
Handling
ambiguous: whenambiguous=true,itemisnulland you should present the list initemsrather than assuming the first. For deterministic bot flows, passexact=true(the default).
4.5 POST /v1/status/check/bulk 🔑
Resolve many queries in a single request. Requires a plan with the bulk entitlement (all public plans currently allow bulk). The number of queries is capped by your plan's max_bulk_size.
Request body (BulkStatusCheckRequest, Content-Type: application/json):
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
queries | string[] | yes | — | List of tickers/names to check. 1–1000 items (further capped by your plan's max_bulk_size). Blank entries are dropped. |
exact | boolean | no | true | Exact matching for all queries. |
limit_per_query | integer | no | 3 | Max matches per query. Range 1–10. |
Example request:
curl -X POST "https://api.cryptohalalstatus.com/v1/status/check/bulk" \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"queries":["BTC","ETH","XLT"],"exact":true,"limit_per_query":3}'
Example response (BulkStatusCheckResponse):
{
"count": 3,
"exact": true,
"results": [
{ "query": "BTC", "exact": true, "found": true, "ambiguous": false, "match_count": 1, "item": { /* StatusItem */ }, "items": [ /* ... */ ], "disclaimer": "..." },
{ "query": "ETH", "exact": true, "found": true, "ambiguous": false, "match_count": 1, "item": { /* StatusItem */ }, "items": [ /* ... */ ], "disclaimer": "..." },
{ "query": "XLT", "exact": true, "found": false, "ambiguous": false, "match_count": 0, "item": null, "items": [], "disclaimer": "..." }
]
}
Field reference:
| Field | Type | Description |
|---|---|---|
count | integer | Number of results (one per non-blank query). |
exact | boolean | Whether exact matching was applied to all queries. |
results | StatusCheckResponse[] | One result object per query, each with the same shape as §4.4. |
Errors specific to this endpoint: 400 if all queries are blank; 403 (bulk_not_allowed) if your plan lacks bulk; 403 if the query count exceeds your plan's max_bulk_size.
4.6 GET /v1/status/current 🔑 / 4.7 GET /v1/status/latest 🔑
Paginated list of current rulings matching a required search query, newest first, with opaque cursor pagination. /v1/status/current is an alias of /v1/status/latest — identical behavior and response. Full-dataset enumeration is not supported; use /v1/changes to sync.
Query parameters (both endpoints):
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
q | string | yes | — | Required search term by observed name or ticker. |
limit | integer | no | 20 | Page size, 1–5000. Capped by your plan's max_page_size (exceeding it returns 403 page_size_exceeded). |
cursor | string | no | null | Opaque pagination cursor; pass back the meta.next_cursor from the previous page. |
exact | boolean | no | false | Require exact name/ticker/identity match. Note: defaults to false here (unlike /v1/status/check). |
Example request:
# First page
curl -H "X-API-Key: $KEY" \
"https://api.cryptohalalstatus.com/v1/status/current?q=BTC&limit=20"
# Next page — feed meta.next_cursor back in
curl -H "X-API-Key: $KEY" \
"https://api.cryptohalalstatus.com/v1/status/current?q=BTC&limit=20&cursor=eyJrIjoiMTIzNCJ9"
Example response (SearchResponse):
{
"items": [ { /* StatusItem — see §4.11 */ } ],
"meta": {
"count": 20,
"limit": 20,
"next_cursor": "eyJrIjoiMTIzNCJ9",
"query": "BTC",
"exact": false,
"ambiguous": false
}
}
SearchResponse fields:
| Field | Type | Description |
|---|---|---|
items | StatusItem[] | The page of rulings. |
meta | SearchMeta | null | Pagination metadata (see below). |
SearchMeta fields:
| Field | Type | Description |
|---|---|---|
count | integer | Number of items in this page. |
limit | integer | The page size that was applied. |
next_cursor | string | null | Cursor for the next page; null when there are no more results. |
query | string | null | The search term that was applied. |
exact | boolean | Whether exact matching was applied. |
ambiguous | boolean | Whether the search term matched ambiguously. |
Pagination contract: keep requesting with the returned
next_cursoruntil it isnull. The cursor is opaque — do not parse or construct it. An invalid cursor returns400.
4.8 GET /v1/status/history 🔑
Lists ruling-change records — the timeline of verdict revisions. Requires a plan with the history entitlement (Pro and Unlimited; Free and Growth return 403 history_not_allowed). Same SearchResponse shape and cursor pagination as the current/latest endpoints.
Query parameters:
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
q | string | no | null | Filter by observed name or ticker. |
limit | integer | no | 100 | Page size, 1–5000. Capped by your plan's max_page_size. |
cursor | string | no | null | Opaque pagination cursor from meta.next_cursor. |
exact | boolean | no | false | Require exact name/ticker/identity match. |
Example request:
curl -H "X-API-Key: $KEY" \
"https://api.cryptohalalstatus.com/v1/status/history?q=BTC&limit=50"
Example response: identical structure to §4.6 (SearchResponse of StatusItem). History rows typically have is_changed_ruling: true and populated changed_from / changed_to / changed_at fields — see §5.
GET /v1/changes 🔑 — poll the delta feed
Use /v1/changes to keep a local cache fresh. It is API-key gated and requires the same history entitlement as /v1/status/history (Free and Growth return 403 history_not_allowed; Pro and Unlimited are allowed). It returns every published ruling row from the append-only rulings log, ordered forward by (source_timestamp ASC, id ASC).
First call with since=<ISO-8601 datetime>; then keep passing the returned meta.next_cursor as cursor. If cursor is present it takes precedence over since. A full page returns a non-null cursor so you can continue polling from the last delivered row; next_cursor: null means you are caught up.
curl -H "X-API-Key: $KEY" \
"https://api.cryptohalalstatus.com/v1/changes?since=2026-06-20T00:00:00Z&limit=100"
curl -H "X-API-Key: $KEY" \
"https://api.cryptohalalstatus.com/v1/changes?cursor=NEXT_CURSOR_FROM_META&limit=100"
Response shape:
{
"items": [
{
"ruling_uid": "src7f0a2990:777:0",
"ticker": "BTC",
"name": "Bitcoin",
"source": "Crypto Islam",
"judgment": "halal",
"changed": false,
"previous_judgment": null,
"changed_at": "2026-06-20T10:00:00Z",
"post_url": "https://t.me/crypoislam/777"
}
],
"meta": { "count": 1, "limit": 100, "next_cursor": null }
}
4.9 GET /v1/stats 🔑
Dataset-wide counts and freshness indicators. Useful for dashboards and monitoring how current the dataset is.
Parameters: none.
Example request:
curl -H "X-API-Key: $KEY" "https://api.cryptohalalstatus.com/v1/stats"
Example response (DatasetStatsResponse):
{
"channel": "cryptoislam",
"current_statuses": 1284,
"published_rulings": 1310,
"total_source_posts": 5402,
"candidate_posts": 1502,
"pending_jobs": 0,
"failed_jobs": 1,
"review_jobs": 4,
"changed_rulings": 37,
"judgments": { "halal": 540, "haram": 612, "suspicious": 98, "unknown": 34 },
"latest_ingested_message_id": 54021,
"latest_source_timestamp": "2026-06-05T09:12:00Z"
}
Field reference:
| Field | Type | Description |
|---|---|---|
channel | string | Source channel slug. |
current_statuses | integer | Count of distinct assets with a current ruling. |
published_rulings | integer | Total published rulings. |
total_source_posts | integer | Total source posts ingested from the channel. |
candidate_posts | integer | Posts identified as ruling candidates. |
pending_jobs | integer | Extraction jobs queued. |
failed_jobs | integer | Extraction jobs that failed. |
review_jobs | integer | Jobs flagged for human review. |
changed_rulings | integer | Number of rulings that have been revised. |
judgments | object | Count of current rulings per verdict. |
latest_ingested_message_id | integer | null | Highest ingested source message id. |
latest_source_timestamp | string (date-time) | null | Timestamp of the most recent source post. |
4.10 GET /v1/rulings/{ruling_uid} 🔑
Fetch a single ruling by its stable ID (ruling_uid). Returns a single StatusItem (§4.11).
Path parameters:
| Param | Type | Required | Description |
|---|---|---|---|
ruling_uid | string | yes | The stable ruling identifier (the ruling_uid field, e.g. rul_01HXY...). |
Example request:
curl -H "X-API-Key: $KEY" \
"https://api.cryptohalalstatus.com/v1/rulings/rul_01HXY8M4QF7N2C5K3B9D6E0PQR"
Example response: a single StatusItem object (see §4.11).
Errors: 404 not_found if no ruling matches the ID.
Compatibility alias:
GET /v1/mentions/{mention_id}accepts a numeric id and resolves to the same ruling object. Prefer/v1/rulings/{ruling_uid}with the stableruling_uidfor new integrations.
4.11 The StatusItem object
The core ruling object returned (singly or in lists) by the status/ruling endpoints. All fields are always present; nullable fields appear as null when not applicable. In v1, every row also carries a source label distinguishing the issuing source (see §4.14). The set of populated text fields depends on the source — see "Source-aware fields" below.
| Field | Type | Description |
|---|---|---|
ruling_id | integer | Internal numeric id. Not stable across dataset rebuilds — do not persist; use ruling_uid. |
ruling_uid | string | Stable ruling identifier. Namespaced per source (e.g. crypoislam:52:0, cr_halal:1234:0). Use this for stored references and /v1/rulings/{ruling_uid}. |
current_status_uid | string | Stable identifier of the asset's current-status record. |
source | string | Issuing source label. "Crypto Islam" (@crypoislam) or "Crypto Halal" (@cr_halal, display CRYPTOHALAL كريبتو حلال). |
source_image_sha256 | string | null | SHA-256 of the source image. |
source_post_url | string | null | Permalink to the source post (e.g. https://t.me/crypoislam/52, https://t.me/cr_halal/9876). |
source_timestamp | string (date-time) | When the source published the ruling. |
observed_name | string | null | Asset name exactly as observed in the source. |
observed_ticker | string | null | Ticker exactly as observed in the source. |
canonical_asset_id | string | null | Canonical asset id (nullable; not yet authoritative). |
canonical_name | string | null | Canonical asset name (nullable). |
canonical_ticker | string | null | Canonical ticker (nullable). |
judgment | string | The verdict: halal | haram | suspicious | unknown. |
verdict_text_en | string | null | Verdict text (English) as on the card. |
verdict_text_ar | string | null | Verdict text (Arabic). |
verdict_panel_color | string | null | Color of the verdict panel on the source card (green / red / orange / unknown). |
| i18n translations (verdict) | The 6-language translations of the verdict text: verdict_text_ru, verdict_text_fa, verdict_text_tr, verdict_text_uz. Translated for Crypto Islam only; null for CryptoHalal. | |
reason_summary_en | string | null | Reason summary (English). Populated for Crypto Islam; null for CryptoHalal. |
reason_summary_ar | string | null | Reason summary (Arabic). Populated for Crypto Islam; null for CryptoHalal. |
| i18n translations (reason summary) | The 6-language translations: reason_summary_ru, reason_summary_fa, reason_summary_tr, reason_summary_uz. Translated for Crypto Islam only; null for CryptoHalal. | |
reason_points_en | string[] | Bullet-point reasons (English). Crypto Islam: populated; CryptoHalal: []. |
reason_points_ar | string[] | Bullet-point reasons (Arabic). Crypto Islam: populated; CryptoHalal: []. |
| i18n translations (reason points) | The 6-language translations: reason_points_ru, reason_points_fa, reason_points_tr, reason_points_uz (all string[]). Translated for Crypto Islam only; [] for CryptoHalal. | |
services_en | string[] | CryptoHalal-only. The "Services" section in English (e.g. ["Staking", "NFTs"]). Crypto Islam: []. |
services_ar | string[] | CryptoHalal-only. The "Services" section in Arabic. Crypto Islam: []. |
services_ru | string[] | CryptoHalal-only. Russian translation of the Services section. Crypto Islam: []. |
services_fa | string[] | CryptoHalal-only. Persian (Farsi) translation of the Services section. Crypto Islam: []. |
services_tr | string[] | CryptoHalal-only. Turkish translation of the Services section. Crypto Islam: []. |
services_uz | string[] | CryptoHalal-only. Uzbek translation of the Services section. Crypto Islam: []. |
use_cases_en | string[] | CryptoHalal-only. The "Use of <coin>" section in English (e.g. ["Payments"]). No ru/fa/tr/uz translation is produced; bilingual en+ar is the public shape. Crypto Islam: []. |
use_cases_ar | string[] | CryptoHalal-only. The "Use of <coin>" section in Arabic. Crypto Islam: []. |
observed_chain | string | null | Chain observed in the source (e.g. Ethereum). |
observed_contract_address | string | null | Contract address observed in the source. |
verdict_date_gregorian | string | null | Verdict date (Gregorian). |
verdict_date_hijri | string | null | Verdict date (Hijri). |
is_changed_ruling | boolean | true if this ruling is a revision of a prior verdict. |
previous_ruling_uid | string | null | ruling_uid of the prior ruling, when changed. |
previous_judgment | string | null | The prior verdict value, when changed. |
changed_from | string | null | Verdict before the change. |
changed_to | string | null | Verdict after the change. |
changed_at | string (date-time) | null | When the change took effect. |
confidence | number | Extraction confidence, 0.0–1.0. |
requires_review | boolean | true if the extraction was flagged for human review. |
publication_status | string | Publication state (e.g. published). |
extracted_at | string (date-time) | null | When the ruling was extracted. |
notes | string | null | Free-form extraction notes (raw mixed-language text). CryptoHalal carries the project description here. |
| i18n translations (notes) | The 6-language translations: notes_en, notes_ar, notes_ru, notes_fa, notes_tr, notes_uz. Translated for Crypto Islam only; for CryptoHalal, only notes / notes_en / notes_ar are populated and the rest are null. |
Source-aware fields (summary):
| Field group | Crypto Islam (source: "Crypto Islam") | CryptoHalal (source: "Crypto Halal") |
|---|---|---|
verdict_text_* | en/ar from card; ru/fa/tr/uz translated | en/ar from card; ru/fa/tr/uz are null (NOT translated) |
reason_summary_* | en/ar from card; ru/fa/tr/uz translated | null in all 6 langs (no "reason" section on the card) |
reason_points_* | bullets in en/ar; ru/fa/tr/uz translated | [] in all 6 langs |
services_* | [] in all 6 langs (no Services section) | en/ar from card; ru/fa/tr/uz translated |
use_cases_* | [] in both langs | en/ar from card; no ru/fa/tr/uz translation produced |
notes / notes_* | usually null; *_ru/_fa/_tr/_uz translated if non-null | raw notes + bilingual notes_en/notes_ar carry the project description; *_ru/_fa/_tr/_uz are null |
Where to find the "why" for a verdict, by source:
- Crypto Islam:
reason_summary_<lang>(andreason_points_<lang>for bullets). - CryptoHalal:
notes(raw) /notes_en/notes_ar(project description) +services_<lang>(the "Services" section) +use_cases_en/use_cases_ar(the "Use of <coin>" section).
4.12 GET /v1/account 🔑
Describes the calling key's client, plan, effective limits, and entitlements. Useful for surfacing the customer's own quota in your UI.
Parameters: none.
Example request:
curl -H "X-API-Key: $KEY" "https://api.cryptohalalstatus.com/v1/account"
Example response (ApiAccountResponse):
{
"client_id": "cli_01HXY...",
"client_name": "Acme Screener",
"client_status": "active",
"key_prefix": "chal_live_abc123",
"plan_slug": "upper",
"plan_name": "Growth",
"subscription_expires_at": "2026-12-31T00:00:00Z",
"internal": false,
"rate_limit_scope": "client",
"limits": { "minute": 200, "hour": 10000, "day": 50000, "month": 1500000 },
"entitlements": {
"bulk": true,
"max_bulk_size": 100,
"distinct_cap": 1000,
"history": true,
"max_page_size": 100
}
}
Field reference:
| Field | Type | Description |
|---|---|---|
client_id | string | Your API client id. |
client_name | string | Your client's name. |
client_status | string | active | suspended | unpaid | deleted. |
key_prefix | string | First characters of the key in use (safe to log). |
plan_slug | string | Plan slug. |
plan_name | string | Plan display name. |
subscription_expires_at | string (date-time) | null | When the subscription expires. |
internal | boolean | Whether this is an internal (first-party) client. |
rate_limit_scope | string | "client". |
limits | object | Effective per-window limits (minute/hour/day/month); null = unlimited. Reflects any per-client override on top of the plan. |
entitlements | object | bulk, max_bulk_size, distinct_cap, history, max_page_size. For internal clients the size caps are null (unlimited). |
4.13 GET /v1/account/usage 🔑
Your own usage counters, aggregated per window and over the last 24h / 7d.
Parameters: none.
Example request:
curl -H "X-API-Key: $KEY" "https://api.cryptohalalstatus.com/v1/account/usage"
Example response (ApiAccountUsageResponse):
{
"client_id": "cli_01HXY...",
"key_prefix": "chal_live_abc123",
"rate_limit_scope": "client",
"windows": { "minute": 3, "hour": 142, "day": 1890, "month": 41230 },
"client_requests_24h": 1905,
"client_requests_7d": 13280,
"rate_limited_24h": 2,
"unauthorized_24h": 0,
"by_status_24h": { "200": 1903, "429": 2 },
"distinct_coins_month": 412,
"distinct_cap": 1000
}
Field reference:
| Field | Type | Description |
|---|---|---|
client_id | string | Your API client id. |
key_prefix | string | First characters of the key. |
rate_limit_scope | string | "client". |
windows | object | Request counts within the current minute/hour/day/month windows. |
client_requests_24h | integer | Total requests in the last 24 hours. |
client_requests_7d | integer | Total requests in the last 7 days. |
rate_limited_24h | integer | Number of 429 responses in the last 24 hours. |
unauthorized_24h | integer | Number of 401/403 responses in the last 24 hours. |
by_status_24h | object | Map of HTTP status code → count over the last 24 hours. |
distinct_coins_month | integer | null | Distinct coin ids served to this API key in the current UTC calendar month; null when Redis-backed counting is unavailable. |
distinct_cap | integer | null | Monthly distinct-coin cap for the plan; null = unlimited. |
4.14 Multi-source: per (coin, source)
The v1 API publishes rulings per (coin, source). Every StatusItem carries a source field ("Crypto Islam" or "Crypto Halal"), and a single coin can carry a different verdict from each source, returned as separate rows. This section documents the contract and the recommended client pattern.
What changes from pre-v1
- The same
?q=COIN&exact=truecan return up to two rows (one per source) if both have ruled that coin. - When both sources have ruled the same coin, the response sets
ambiguous: trueanditem: null; both per-source verdicts are listed initems[]— clients must show the user every source's verdict, not collapse to one. - The same coin can carry different verdicts from different sources (e.g. Crypto Islam
halal, CryptoHalalsuspicious). Both are independently authoritative per their ownsource. - A first-class source-filter query parameter is not exposed in v1. Filter client-side by string-comparing
items[i].sourceagainst"Crypto Islam"or"Crypto Halal".
Dual-source response shape
Same query that previously returned a single Crypto Islam row now returns two rows when the coin is also covered by CryptoHalal:
{
"query": "BTC",
"exact": true,
"found": true,
"ambiguous": true,
"match_count": 2,
"item": null,
"items": [
{
"ruling_uid": "crypoislam:52:0",
"source": "Crypto Islam",
"observed_name": "Bitcoin", "observed_ticker": "BTC",
"judgment": "halal",
"reason_summary_en": "Decentralized payment network with no interest-bearing mechanism.",
"reason_points_en": ["No riba", "Utility as a medium of exchange"],
"services_en": [], "use_cases_en": [],
"...": "..."
},
{
"ruling_uid": "cr_halal:1234:0",
"source": "Crypto Halal",
"observed_name": "Bitcoin", "observed_ticker": "BTC",
"judgment": "halal",
"reason_summary_en": null, "reason_points_en": [],
"services_en": ["Store of value", "Payments"],
"use_cases_en": ["Payments"],
"...": "..."
}
],
"disclaimer": "This response republishes structured data extracted from Crypto Islam and Crypto Halal source posts. It is not independent religious, financial, or investment advice."
}
The disclaimer lists both sources when both have rows in the response.
Reading the rows
source value | Channel | Verdict semantics | "Why" field set |
|---|---|---|---|
"Crypto Islam" | @crypoislam | Per Crypto Islam's published ruling | reason_summary_* + reason_points_* (translated 6 ways) |
"Crypto Halal" | @cr_halal (display CRYPTOHALAL كريبتو حلال) | Per CryptoHalal's published ruling | notes / notes_en / notes_ar (project description) + services_* (translated 6 ways) + use_cases_en / use_cases_ar (bilingual only) |
Clients MUST surface the source's own "why" — never cite reason_summary_en for a CryptoHalal row (it is null), and never cite services_en for a Crypto Islam row (it is []).
Recommended client pattern (Python)
def check_status(query, exact=True):
resp = requests.get(
f"{BASE}/v1/status/check",
params={"q": query, "exact": str(exact).lower()},
headers=HEADERS, timeout=10,
)
resp.raise_for_status()
body = resp.json()
if not body["found"]:
return {"status": "not_found", "rows": []}
rows = body["items"]
verdicts = {
r["source"]: {
"judgment": r["judgment"],
# source-appropriate "why"
"why": (
r.get("reason_summary_en")
if r["source"] == "Crypto Islam"
else {
"services": r.get("services_en", []),
"use_cases": r.get("use_cases_en", []),
"notes": r.get("notes_en") or r.get("notes"),
}
),
"ruling_uid": r["ruling_uid"],
"confidence": r["confidence"],
}
for r in rows
}
return {
"status": "ambiguous" if body["ambiguous"] else "ok",
"rows": verdicts,
}
Recommended client pattern (JavaScript)
async function checkStatus(query, exact = true) {
const url = new URL(`${BASE}/v1/status/check`);
url.searchParams.set("q", query);
url.searchParams.set("exact", String(exact));
const res = await fetch(url, { headers: { "X-API-Key": KEY } });
if (!res.ok) throw new Error(`Status check failed: ${res.status}`);
const body = await res.json();
if (!body.found) return { status: "not_found", rows: [] };
const rows = Object.fromEntries(
body.items.map((r) => [
r.source,
{
judgment: r.judgment,
why:
r.source === "Crypto Islam"
? r.reason_summary_en
: { services: r.services_en, use_cases: r.use_cases_en, notes: r.notes_en || r.notes },
ruling_uid: r.ruling_uid,
confidence: r.confidence,
},
]),
);
return { status: body.ambiguous ? "ambiguous" : "ok", rows };
}
Caveats
- The same coin can carry DIFFERENT verdicts from different sources. Do not collapse them into a single verdict.
- An
unknownfrom one source does not imply anything about the other source — they are independent rulings. - When
match_count > 1and the items differ only insource(sameobserved_ticker/observed_name), the API setsambiguous: trueanditem: null. Always show the candidateitems[](by ticker/name ANDsource) or ask the user which source to trust. - The CryptoHalal channel posts use a magenta verdict panel internally; the API normalises that to
red(halal/harammapping is unchanged) —verdict_panel_colorwill begreen/red/orange/unknownregardless of source.
5. The verdict model in depth
Every ruling represents what the source published, structured for machines. The source field names the issuer ("Crypto Islam" or "Crypto Halal"); the API itself does not independently rule.
The four verdict values (judgment)
| Verdict | What it means | How to treat it |
|---|---|---|
halal | The source ruled the asset permitted. | Permitted per source. |
haram | The source ruled the asset not permitted. | Not permitted per source. |
suspicious | The source flagged suspicion / doubt. | Caution — present as doubtful, not as approval. |
unknown | The verdict could not be determined from the source card. | Not yet ruled. Do not treat unknown as halal. See §9. |
Source attribution and reasoning fields
source— the issuing source label ("Crypto Islam"or"Crypto Halal").- Crypto Islam "why":
reason_summary_en/reason_summary_ar— a one-line rationale in English / Arabic.reason_points_en/reason_points_ar— itemized rationale bullet points.- All four are translated into
ru/fa/tr/uzand exposed asreason_summary_<lang>/reason_points_<lang>.
- CryptoHalal "why":
notes(raw) /notes_en/notes_ar— the project description (translated only into en+ar;notes_ru/_fa/_tr/_uzstaynull).services_en/services_ar— the "Services" section (e.g.["Staking", "NFTs"]). Translated into 6 langs.use_cases_en/use_cases_ar— the "Use of <coin>" section (e.g.["Payments"]). Bilingual only; noru/fa/tr/uztranslation is produced.
confidence— a0.0–1.0score for how confidently the verdict was extracted from the source card. Lower values, orrequires_review: true, indicate the extraction is less certain.verdict_date_gregorian/verdict_date_hijri— the date the source assigned the verdict;source_timestampis when the source post was published;extracted_atis when the data was extracted.
Ruling-change history
When the source revises a verdict, the newer ruling carries:
is_changed_ruling: trueprevious_ruling_uid— theruling_uidof the prior ruling,previous_judgment— the prior verdict value,changed_from→changed_to— the verdict transition,changed_at— when the change took effect.
The dataset-wide count of revised rulings is exposed as changed_rulings in /v1/stats, the full revision list is available (on entitled plans) via /v1/status/history, and the dataset-wide delta feed is available via /v1/changes. Because verdicts can change, treat any stored verdict as a snapshot and re-check periodically.
6. Rate limits
Per-plan limits
Limits are enforced per API client (rate_limit_scope: "client"), aggregated across all of that client's keys. A null value means no limit for that window.
| Tier | req/min | req/day | page size | bulk size | coverage band | distinct coins/month | history | bulk | webhooks |
|---|---|---|---|---|---|---|---|---|---|
Free (free) | 60 | — | 25 | 10 | Top 50 coins | 50 | no | yes | no |
Growth (standard) | 60 | — | 50 | 25 | Top 250 coins | 250 | no | yes | no |
Pro (upper) | 60 | — | 100 | 50 | Top 1,000 coins | 1,000 | yes | yes | no |
Unlimited (unlimited) | 60 | — | 200 | 100 | All coins | Unlimited | yes | yes | yes |
Free API responses include top-level
_attribution_required: trueand_attribution_htmlfields; display that attribution wherever you show Free-tier data. A custom internal/contracted plan may set broader limits by written agreement. The numbers above are the public plan defaults — always treatGET /v1/plansandGET /v1/accountas the live source of truth, since limits can be adjusted per client.
Rate-limit response headers
Successful authenticated responses include:
| Header | Description |
|---|---|
X-RateLimit-Limit | Capacity of the primary window (the smallest configured window, normally minute). |
X-RateLimit-Remaining | Requests remaining in the primary window. |
X-RateLimit-Reset | Unix timestamp (seconds) when the primary window resets. |
X-RateLimit-Policy | The full policy, e.g. 60;w=60, 3000;w=3600, 10000;w=86400, 300000;w=2678400 (limit;w=window_seconds). For an unlimited plan this is unlimited. |
The 429 response
When any window is exceeded, the API returns 429 with a Retry-After header giving the exact number of seconds to wait:
HTTP/1.1 429 Too Many Requests
Retry-After: 42
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1751720460
X-Request-ID: 9b1c...
Content-Type: application/json
{
"detail": "Rate limit exceeded",
"request_id": "9b1c...",
"code": "minute_limit_exceeded",
"errors": null
}
The code identifies which window tripped: minute_limit_exceeded, hour_limit_exceeded, day_limit_exceeded, or month_limit_exceeded. Respect Retry-After exactly — the value is precise, so blind exponential backoff is unnecessary.
Note: the public/unauthenticated endpoints (
/v1/meta,/v1/plans,/v1/showcase) are rate-limited by IP on a separate per-minute budget and also return429+Retry-Afterwhen exceeded.
7. Error handling
Standard error shape
All /v1 errors return a consistent JSON body (ErrorResponse):
{
"detail": "Invalid API key",
"request_id": "client-or-server-request-id",
"code": "invalid_api_key",
"errors": null
}
| Field | Type | Description |
|---|---|---|
detail | string | Human-readable message. |
request_id | string | null | Correlation id (also returned as the X-Request-ID header). Include it in support requests. |
code | string | null | Stable machine-readable error code (see tables below). |
errors | array | null | Field-level validation details (populated on 422). |
Common HTTP statuses and codes
| HTTP | Typical code values | Meaning |
|---|---|---|
| 400 | bad_request | Malformed request — e.g. blank status-check query, invalid pagination cursor, empty bulk query list. |
| 401 | missing_api_key, invalid_api_key, inactive_api_key, expired_api_key, deleted_client | No key, unrecognized/revoked key, an inactive or expired key, or a deleted client. |
| 403 | suspended_client, unpaid_client, inactive_client, inactive_plan, subscription_not_approved, subscription_missing_expiry, subscription_expired, history_not_allowed, bulk_not_allowed, page_size_exceeded | Authenticated, but the client/plan/subscription or the requested feature/limit is not allowed. |
| 404 | not_found | The requested resource (e.g. a ruling) does not exist. |
| 422 | validation_error | Request body/params failed validation; see the errors array for per-field detail. |
| 429 | rate_limit_exceeded, minute_limit_exceeded, hour_limit_exceeded, day_limit_exceeded, month_limit_exceeded | Rate limit hit — honor the Retry-After header. |
| 500 | http_error (generic) | Unexpected server error — safe to retry with backoff. |
A
422body'serrorsarray contains FastAPI/Pydantic-style entries (each typically hasloc,msg, andtype) describing exactly which field failed.
Example 422 (validation)
{
"detail": "Request validation failed",
"request_id": "a1b2c3",
"code": "validation_error",
"errors": [
{ "type": "greater_than_equal", "loc": ["body", "limit_per_query"], "msg": "Input should be greater than or equal to 1" }
]
}
8. Code examples
The examples below cover the main flows. Replace chal_live_... with your key (prefer reading it from an environment variable / secret manager).
Single status check
curl
curl -H "X-API-Key: $CRYPTOHALAL_KEY" \
"https://api.cryptohalalstatus.com/v1/status/check?q=BTC&exact=true"
JavaScript (fetch)
const BASE = "https://api.cryptohalalstatus.com";
const KEY = process.env.CRYPTOHALAL_KEY;
async function checkStatus(query, exact = true) {
const url = new URL(`${BASE}/v1/status/check`);
url.searchParams.set("q", query);
url.searchParams.set("exact", String(exact));
const res = await fetch(url, { headers: { "X-API-Key": KEY } });
if (res.status === 429) {
const retryAfter = Number(res.headers.get("Retry-After")) || 1;
throw new Error(`Rate limited; retry after ${retryAfter}s`);
}
if (!res.ok) {
const err = await res.json();
throw new Error(`${res.status} ${err.code}: ${err.detail}`);
}
const data = await res.json();
// `item` is null when ambiguous or not found — inspect `items` / `ambiguous`.
return data.item ?? null;
}
const btc = await checkStatus("BTC");
console.log(btc?.judgment); // "halal"
Python (requests)
import os
import requests
BASE = "https://api.cryptohalalstatus.com"
KEY = os.environ["CRYPTOHALAL_KEY"]
HEADERS = {"X-API-Key": KEY}
def check_status(query: str, exact: bool = True):
resp = requests.get(
f"{BASE}/v1/status/check",
params={"q": query, "exact": str(exact).lower()},
headers=HEADERS,
timeout=10,
)
if resp.status_code == 429:
retry_after = int(resp.headers.get("Retry-After", "1"))
raise RuntimeError(f"Rate limited; retry after {retry_after}s")
resp.raise_for_status()
data = resp.json()
return data["item"] # None when ambiguous/not found
item = check_status("BTC")
print(item and item["judgment"]) # "halal"
Bulk status check
curl
curl -X POST "https://api.cryptohalalstatus.com/v1/status/check/bulk" \
-H "X-API-Key: $CRYPTOHALAL_KEY" \
-H "Content-Type: application/json" \
-d '{"queries":["BTC","ETH","XLT"],"exact":true,"limit_per_query":3}'
JavaScript (fetch)
async function bulkCheck(queries) {
const res = await fetch("https://api.cryptohalalstatus.com/v1/status/check/bulk", {
method: "POST",
headers: { "X-API-Key": process.env.CRYPTOHALAL_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ queries, exact: true, limit_per_query: 3 }),
});
if (!res.ok) throw new Error(`Bulk failed: ${res.status}`);
const { results } = await res.json();
return Object.fromEntries(results.map((r) => [r.query, r.item?.judgment ?? "unknown"]));
}
console.log(await bulkCheck(["BTC", "ETH", "XLT"]));
Python (requests)
def bulk_check(queries):
resp = requests.post(
f"{BASE}/v1/status/check/bulk",
json={"queries": queries, "exact": True, "limit_per_query": 3},
headers=HEADERS,
timeout=15,
)
resp.raise_for_status()
results = resp.json()["results"]
return {r["query"]: (r["item"] or {}).get("judgment", "unknown") for r in results}
print(bulk_check(["BTC", "ETH", "XLT"]))
Paginating the full current dataset (cursor pagination)
Python (requests)
def iter_current(query=None, page_size=100):
cursor = None
while True:
params = {"limit": page_size}
if query:
params["q"] = query
if cursor:
params["cursor"] = cursor
resp = requests.get(f"{BASE}/v1/status/current", params=params, headers=HEADERS, timeout=15)
resp.raise_for_status()
body = resp.json()
for item in body["items"]:
yield item
cursor = (body.get("meta") or {}).get("next_cursor")
if not cursor:
break
for ruling in iter_current(page_size=100):
print(ruling["ruling_uid"], ruling["observed_ticker"], ruling["judgment"])
JavaScript (fetch)
async function* iterCurrent(pageSize = 100) {
let cursor = null;
do {
const url = new URL("https://api.cryptohalalstatus.com/v1/status/current");
url.searchParams.set("limit", String(pageSize));
if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, { headers: { "X-API-Key": process.env.CRYPTOHALAL_KEY } });
if (!res.ok) throw new Error(`Page failed: ${res.status}`);
const body = await res.json();
for (const item of body.items) yield item;
cursor = body.meta?.next_cursor ?? null;
} while (cursor);
}
Reading rate-limit headers and backing off
Python (requests)
import time
def get_with_backoff(path, **params):
while True:
resp = requests.get(f"{BASE}{path}", params=params, headers=HEADERS, timeout=10)
if resp.status_code == 429:
time.sleep(int(resp.headers.get("Retry-After", "1")))
continue
resp.raise_for_status()
remaining = resp.headers.get("X-RateLimit-Remaining")
if remaining is not None and int(remaining) < 10:
time.sleep(1) # proactively ease off near the limit
return resp.json()
9. Best practices
Caching
Cacheable GET /v1/* responses include a Cache-Control: public, max-age=<cache_seconds> header (default max-age=60; the exact value is in /v1/meta → cache_seconds). Most ruling data is stable for hours, so cache aggressively at the edge or in your client for at least the advertised max-age to conserve your rate budget. Re-check periodically, since verdicts can change (see §5).
Handle unknown conservatively
unknown means not yet ruled — it is not an implicit halal. In a compliance-sensitive flow, surface unknown (and suspicious) explicitly to the user rather than defaulting to "permitted". A safe default is to treat anything that is not an explicit halal as "do not auto-approve".
Prefer exact matching and handle ambiguity
For bot/wallet flows that act automatically, use exact=true (the default on /v1/status/check). When a response reports ambiguous: true, item is null — present the candidates in items and let the user disambiguate instead of guessing.
Handle multi-source responses (v1)
When a coin is ruled by both sources, the response carries two items[] rows and sets ambiguous: true / item: null. Recommended behaviour:
- Always display the
sourcenext to a verdict. The two sources are independent authorities; the same coin can carry different verdicts from each. - Cite the source-appropriate "why":
- For
"Crypto Islam"rows, citereason_summary_en(andreason_points_enfor bullets). - For
"Crypto Halal"rows, citeservices_en(the "Services" section) anduse_cases_en(the "Use of <coin>" section); anotes_endescription may also carry context. Do not citereason_summary_en(it isnull) orreason_points_en(it is[]).
- For
- Never collapse to a single verdict. If your UI can only show one, present both per-source verdicts side-by-side (or prompt the user to pick a source to trust).
- Group by source client-side. A first-class source-filter query parameter is not exposed in v1; post-filter the
items[]bysourceif you need a single-source view.
Use stable identifiers
Persist and reference ruling_uid (and current_status_uid), not the numeric ruling_id, which can change after a dataset rebuild. The /v1/mentions/{id} numeric route exists only for backward compatibility. In v1 ruling_uid is namespaced per source (e.g. crypoislam:52:0, cr_halal:1234:0) — the prefix is informational, not part of a query path.
Pagination
Always follow meta.next_cursor until it is null; treat the cursor as opaque. For /v1/changes, store the last non-null cursor and resume polling with it after the initial since bootstrap. Keep limit within your plan's max_page_size (exceeding it returns 403 page_size_exceeded). For checking many specific tickers at once, POST /v1/status/check/bulk is far cheaper than N single requests.
Operational hygiene
- Store the key in a secret manager; never commit it. Log only the
key_prefix, never the full key. - Send and log
X-Request-IDon every call — provide it (plus yourkey_prefix) when contacting support. - Monitor
X-RateLimit-Remainingand back off proactively; honorRetry-Afterexactly on429. - Retry
5xxwith backoff; do not blindly retry4xx(fix the request instead). - After upgrading, skim
GET /v1/docs/changelogfor the vN entries newer than your integration; the changelog is the canonical "what changed" feed.
Informational, not financial or religious advice
This API republishes structured data from third-party source rulings and does not constitute independent religious, financial, or investment advice. Verdicts reflect the cited source as of the cited date and can change. Always attribute the source when displaying a verdict, and direct end users to qualified scholars and the original source material for decisions. Every status response carries a disclaimer field reiterating this.
This guide is the v1 (multi-source) baseline. It is generated from, and kept in sync with, the live API. For the always-current machine-readable contract and an interactive schema, see GET /v1/meta and the Swagger UI at https://api.cryptohalalstatus.com/docs. For incremental changes since v1, see changelog.md (also served at GET /v1/docs/changelog).
Want the AI / LLM agent guide?
The machine-readable docs and the agent integration guide are reserved for paying customers and live inside the dashboard.