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

  1. Introduction
  2. Authentication
  3. Quickstart
  4. Full endpoint reference
  5. The verdict model in depth
  6. Rate limits
  7. Error handling
  8. Code examples
  9. 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 valueTelegram channelCard shapeWhere the "why" lives
"Crypto Islam"@crypoislamVerdict panel + reasonreason_summary_<lang> + reason_points_<lang> (translated into 6 languages)
"Crypto Halal"@cr_halal (display CRYPTOHALAL كريبتو حلال)Header + Project + Services + Use-of + Judgmentnotes / 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):

VerdictMeaning
halalPermitted according to the source ruling card.
haramNot permitted according to the source ruling card.
suspiciousContains suspicion / doubt according to the source ruling card. Treat as caution, not approval.
unknownThe 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:

  1. Free: create an account, verify your email, then claim one Free API key from the dashboard. Free responses require visible CryptoHalalStatus attribution.
  2. Paid: choose a plan (Growth, Pro, or Unlimited) in the customer portal.
  3. Pay in stablecoin (USDT / USDC) and submit your payment proof.
  4. An administrator reviews and approves the subscription.
  5. 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/meta
  • GET /v1/plans
  • GET /v1/showcase
  • GET /v1/docs (integration docs index)
  • GET /v1/docs/developer-guide
  • GET /v1/docs/integrators-quickstart
  • GET /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.

AuthMethodPathPurpose
🔓GET/v1/metaAPI contract / discovery metadata.
🔓GET/v1/plansPublic plans, limits, entitlements, prices.
🔓GET/v1/showcaseOne example ruling per verdict (for landing pages).
🔓GET/v1/termsTerms of Service.
🔓GET/v1/docsIndex of integration docs.
🔓GET/v1/docs/developer-guideThis guide.
🔓GET/v1/docs/integrators-quickstartQuickstart for new integrators.
🔓GET/v1/docs/changelogVersioned changelog. v1 is the baseline.
🔑GET/v1/docs/llms.txtMachine-readable API index (AI agents).
🔑GET/v1/docs/llm-agent-guideDense AI-agent guide.
🔑GET/v1/status/checkSingle status lookup by ticker/name. May return up to 2 rows (one per source).
🔑POST/v1/status/check/bulkBulk status lookup (entitled plans).
🔑GET/v1/status/currentPaginated current rulings; q is REQUIRED.
🔑GET/v1/status/latestAlias of /v1/status/current; q is REQUIRED.
🔑GET/v1/status/historyRuling-change history (entitled plans).
🔑GET/v1/changesDataset-wide delta feed since a timestamp/cursor (requires history entitlement).
🔑GET/v1/statsDataset-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/accountYour client, plan, limits, entitlements.
🔑GET/v1/account/usageYour usage counters per window.

Common response headers. Every /v1 response carries X-Request-ID (echo it in support requests; you may supply your own via the X-Request-ID request header — letters/digits/_/./:/-, up to 128 chars). Successful authenticated responses also include X-RateLimit-* headers (§6), and cacheable GET responses include Cache-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:

FieldTypeDescription
namestringAPI product name.
versionstringAPI version.
channelstringSource channel slug.
source_channel_urlstringURL of the source channel.
documentation_urlstringPath to the Swagger UI (/docs).
plans_endpointstringPath to the plans endpoint.
disclaimerstringInformational-not-advice disclaimer.
preferred_status_endpointstringRecommended lookup pattern for integrators.
stable_id_fieldstringThe field to use as a stable identifier (ruling_uid).
judgment_valuesobjectMap of each verdict value to its meaning.
endpointsobjectMap of logical endpoint name → path.
cache_secondsintegermax-age applied to cacheable GET /v1 responses (0 = caching disabled).
rate_limit_per_minuteintegerPer-IP minute limit for the public/legacy path (not your plan's client limit).
rate_limit_scopestringScope of plan rate limiting; "client".
api_key_requiredbooleanWhether protected endpoints require a key (normally true).
api_key_headerstringThe header name to send your key in (X-API-Key).
cors_originsstring[]Allowed browser CORS origins.
cursor_paginationbooleanWhether 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):

FieldTypeDescription
slugstringPlan identifier. Note: Growth uses slug upper; Pro uses slug unlimited.
namestringHuman-readable plan name.
descriptionstring | nullPlan description.
price_usd_monthlynumber | nullMonthly price in USD.
time_limitsobjectRequest limits per window: minute, hour, day, month. A null value means no limit for that window.
request_limitsobjectmax_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.
entitlementsobjectBooleans: bulk, history.
unlimitedbooleantrue 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:

FieldTypeDescription
namestringObserved asset name (may be empty if only a ticker is known).
tickerstringObserved ticker (may be empty).
judgmentstringVerdict: halal | haram | suspicious | unknown.
reasonstring | nullShort reason summary, or the first reason point.
sourcestringSource label (e.g. "Crypto Islam").
datestring | nullVerdict 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:

ParamTypeRequiredDefaultDescription
qstringyesTicker, observed name, or identity to check (min length 1).
exactbooleannotrueRequire an exact ticker/name/identity match.
limitintegerno3Max matches returned when ambiguous. Range 110. 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:

FieldTypeDescription
querystringThe normalized query that was searched.
exactbooleanWhether exact matching was applied.
foundbooleantrue if at least one match was returned.
ambiguousbooleantrue if more than one match was found (or the search was inherently ambiguous).
match_countintegerNumber of matches in items.
itemStatusItem | nullThe single resolved ruling. Populated only when there is exactly one unambiguous match; null when found=false or ambiguous=true.
itemsStatusItem[]All matches (empty when not found). When ambiguous=true, iterate this and present the choices.
disclaimerstringInformational-not-advice disclaimer.

Handling ambiguous: when ambiguous=true, item is null and you should present the list in items rather than assuming the first. For deterministic bot flows, pass exact=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):

FieldTypeRequiredDefaultDescription
queriesstring[]yesList of tickers/names to check. 1–1000 items (further capped by your plan's max_bulk_size). Blank entries are dropped.
exactbooleannotrueExact matching for all queries.
limit_per_queryintegerno3Max matches per query. Range 110.

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:

FieldTypeDescription
countintegerNumber of results (one per non-blank query).
exactbooleanWhether exact matching was applied to all queries.
resultsStatusCheckResponse[]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):

ParamTypeRequiredDefaultDescription
qstringyesRequired search term by observed name or ticker.
limitintegerno20Page size, 15000. Capped by your plan's max_page_size (exceeding it returns 403 page_size_exceeded).
cursorstringnonullOpaque pagination cursor; pass back the meta.next_cursor from the previous page.
exactbooleannofalseRequire 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:

FieldTypeDescription
itemsStatusItem[]The page of rulings.
metaSearchMeta | nullPagination metadata (see below).

SearchMeta fields:

FieldTypeDescription
countintegerNumber of items in this page.
limitintegerThe page size that was applied.
next_cursorstring | nullCursor for the next page; null when there are no more results.
querystring | nullThe search term that was applied.
exactbooleanWhether exact matching was applied.
ambiguousbooleanWhether the search term matched ambiguously.

Pagination contract: keep requesting with the returned next_cursor until it is null. The cursor is opaque — do not parse or construct it. An invalid cursor returns 400.


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:

ParamTypeRequiredDefaultDescription
qstringnonullFilter by observed name or ticker.
limitintegerno100Page size, 15000. Capped by your plan's max_page_size.
cursorstringnonullOpaque pagination cursor from meta.next_cursor.
exactbooleannofalseRequire 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:

FieldTypeDescription
channelstringSource channel slug.
current_statusesintegerCount of distinct assets with a current ruling.
published_rulingsintegerTotal published rulings.
total_source_postsintegerTotal source posts ingested from the channel.
candidate_postsintegerPosts identified as ruling candidates.
pending_jobsintegerExtraction jobs queued.
failed_jobsintegerExtraction jobs that failed.
review_jobsintegerJobs flagged for human review.
changed_rulingsintegerNumber of rulings that have been revised.
judgmentsobjectCount of current rulings per verdict.
latest_ingested_message_idinteger | nullHighest ingested source message id.
latest_source_timestampstring (date-time) | nullTimestamp 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:

ParamTypeRequiredDescription
ruling_uidstringyesThe 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 stable ruling_uid for 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.

FieldTypeDescription
ruling_idintegerInternal numeric id. Not stable across dataset rebuilds — do not persist; use ruling_uid.
ruling_uidstringStable 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_uidstringStable identifier of the asset's current-status record.
sourcestringIssuing source label. "Crypto Islam" (@crypoislam) or "Crypto Halal" (@cr_halal, display CRYPTOHALAL كريبتو حلال).
source_image_sha256string | nullSHA-256 of the source image.
source_post_urlstring | nullPermalink to the source post (e.g. https://t.me/crypoislam/52, https://t.me/cr_halal/9876).
source_timestampstring (date-time)When the source published the ruling.
observed_namestring | nullAsset name exactly as observed in the source.
observed_tickerstring | nullTicker exactly as observed in the source.
canonical_asset_idstring | nullCanonical asset id (nullable; not yet authoritative).
canonical_namestring | nullCanonical asset name (nullable).
canonical_tickerstring | nullCanonical ticker (nullable).
judgmentstringThe verdict: halal | haram | suspicious | unknown.
verdict_text_enstring | nullVerdict text (English) as on the card.
verdict_text_arstring | nullVerdict text (Arabic).
verdict_panel_colorstring | nullColor 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_enstring | nullReason summary (English). Populated for Crypto Islam; null for CryptoHalal.
reason_summary_arstring | nullReason 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_enstring[]Bullet-point reasons (English). Crypto Islam: populated; CryptoHalal: [].
reason_points_arstring[]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_enstring[]CryptoHalal-only. The "Services" section in English (e.g. ["Staking", "NFTs"]). Crypto Islam: [].
services_arstring[]CryptoHalal-only. The "Services" section in Arabic. Crypto Islam: [].
services_rustring[]CryptoHalal-only. Russian translation of the Services section. Crypto Islam: [].
services_fastring[]CryptoHalal-only. Persian (Farsi) translation of the Services section. Crypto Islam: [].
services_trstring[]CryptoHalal-only. Turkish translation of the Services section. Crypto Islam: [].
services_uzstring[]CryptoHalal-only. Uzbek translation of the Services section. Crypto Islam: [].
use_cases_enstring[]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_arstring[]CryptoHalal-only. The "Use of <coin>" section in Arabic. Crypto Islam: [].
observed_chainstring | nullChain observed in the source (e.g. Ethereum).
observed_contract_addressstring | nullContract address observed in the source.
verdict_date_gregorianstring | nullVerdict date (Gregorian).
verdict_date_hijristring | nullVerdict date (Hijri).
is_changed_rulingbooleantrue if this ruling is a revision of a prior verdict.
previous_ruling_uidstring | nullruling_uid of the prior ruling, when changed.
previous_judgmentstring | nullThe prior verdict value, when changed.
changed_fromstring | nullVerdict before the change.
changed_tostring | nullVerdict after the change.
changed_atstring (date-time) | nullWhen the change took effect.
confidencenumberExtraction confidence, 0.01.0.
requires_reviewbooleantrue if the extraction was flagged for human review.
publication_statusstringPublication state (e.g. published).
extracted_atstring (date-time) | nullWhen the ruling was extracted.
notesstring | nullFree-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 groupCrypto Islam (source: "Crypto Islam")CryptoHalal (source: "Crypto Halal")
verdict_text_*en/ar from card; ru/fa/tr/uz translateden/ar from card; ru/fa/tr/uz are null (NOT translated)
reason_summary_*en/ar from card; ru/fa/tr/uz translatednull 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 langsen/ar from card; no ru/fa/tr/uz translation produced
notes / notes_*usually null; *_ru/_fa/_tr/_uz translated if non-nullraw 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> (and reason_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:

FieldTypeDescription
client_idstringYour API client id.
client_namestringYour client's name.
client_statusstringactive | suspended | unpaid | deleted.
key_prefixstringFirst characters of the key in use (safe to log).
plan_slugstringPlan slug.
plan_namestringPlan display name.
subscription_expires_atstring (date-time) | nullWhen the subscription expires.
internalbooleanWhether this is an internal (first-party) client.
rate_limit_scopestring"client".
limitsobjectEffective per-window limits (minute/hour/day/month); null = unlimited. Reflects any per-client override on top of the plan.
entitlementsobjectbulk, 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:

FieldTypeDescription
client_idstringYour API client id.
key_prefixstringFirst characters of the key.
rate_limit_scopestring"client".
windowsobjectRequest counts within the current minute/hour/day/month windows.
client_requests_24hintegerTotal requests in the last 24 hours.
client_requests_7dintegerTotal requests in the last 7 days.
rate_limited_24hintegerNumber of 429 responses in the last 24 hours.
unauthorized_24hintegerNumber of 401/403 responses in the last 24 hours.
by_status_24hobjectMap of HTTP status code → count over the last 24 hours.
distinct_coins_monthinteger | nullDistinct coin ids served to this API key in the current UTC calendar month; null when Redis-backed counting is unavailable.
distinct_capinteger | nullMonthly 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=true can 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: true and item: null; both per-source verdicts are listed in items[] — 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, CryptoHalal suspicious). Both are independently authoritative per their own source.
  • A first-class source-filter query parameter is not exposed in v1. Filter client-side by string-comparing items[i].source against "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 valueChannelVerdict semantics"Why" field set
"Crypto Islam"@crypoislamPer Crypto Islam's published rulingreason_summary_* + reason_points_* (translated 6 ways)
"Crypto Halal"@cr_halal (display CRYPTOHALAL كريبتو حلال)Per CryptoHalal's published rulingnotes / 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 unknown from one source does not imply anything about the other source — they are independent rulings.
  • When match_count > 1 and the items differ only in source (same observed_ticker/observed_name), the API sets ambiguous: true and item: null. Always show the candidate items[] (by ticker/name AND source) 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/haram mapping is unchanged) — verdict_panel_color will be green / red / orange / unknown regardless 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)

VerdictWhat it meansHow to treat it
halalThe source ruled the asset permitted.Permitted per source.
haramThe source ruled the asset not permitted.Not permitted per source.
suspiciousThe source flagged suspicion / doubt.Caution — present as doubtful, not as approval.
unknownThe 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/uz and exposed as reason_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/_uz stay null).
    • 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; no ru/fa/tr/uz translation is produced.
  • confidence — a 0.01.0 score for how confidently the verdict was extracted from the source card. Lower values, or requires_review: true, indicate the extraction is less certain.
  • verdict_date_gregorian / verdict_date_hijri — the date the source assigned the verdict; source_timestamp is when the source post was published; extracted_at is when the data was extracted.

Ruling-change history

When the source revises a verdict, the newer ruling carries:

  • is_changed_ruling: true
  • previous_ruling_uid — the ruling_uid of the prior ruling,
  • previous_judgment — the prior verdict value,
  • changed_fromchanged_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.

Tierreq/minreq/daypage sizebulk sizecoverage banddistinct coins/monthhistorybulkwebhooks
Free (free)602510Top 50 coins50noyesno
Growth (standard)605025Top 250 coins250noyesno
Pro (upper)6010050Top 1,000 coins1,000yesyesno
Unlimited (unlimited)60200100All coinsUnlimitedyesyesyes

Free API responses include top-level _attribution_required: true and _attribution_html fields; 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 treat GET /v1/plans and GET /v1/account as the live source of truth, since limits can be adjusted per client.

Rate-limit response headers

Successful authenticated responses include:

HeaderDescription
X-RateLimit-LimitCapacity of the primary window (the smallest configured window, normally minute).
X-RateLimit-RemainingRequests remaining in the primary window.
X-RateLimit-ResetUnix timestamp (seconds) when the primary window resets.
X-RateLimit-PolicyThe 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 return 429 + Retry-After when 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
}
FieldTypeDescription
detailstringHuman-readable message.
request_idstring | nullCorrelation id (also returned as the X-Request-ID header). Include it in support requests.
codestring | nullStable machine-readable error code (see tables below).
errorsarray | nullField-level validation details (populated on 422).

Common HTTP statuses and codes

HTTPTypical code valuesMeaning
400bad_requestMalformed request — e.g. blank status-check query, invalid pagination cursor, empty bulk query list.
401missing_api_key, invalid_api_key, inactive_api_key, expired_api_key, deleted_clientNo key, unrecognized/revoked key, an inactive or expired key, or a deleted client.
403suspended_client, unpaid_client, inactive_client, inactive_plan, subscription_not_approved, subscription_missing_expiry, subscription_expired, history_not_allowed, bulk_not_allowed, page_size_exceededAuthenticated, but the client/plan/subscription or the requested feature/limit is not allowed.
404not_foundThe requested resource (e.g. a ruling) does not exist.
422validation_errorRequest body/params failed validation; see the errors array for per-field detail.
429rate_limit_exceeded, minute_limit_exceeded, hour_limit_exceeded, day_limit_exceeded, month_limit_exceededRate limit hit — honor the Retry-After header.
500http_error (generic)Unexpected server error — safe to retry with backoff.

A 422 body's errors array contains FastAPI/Pydantic-style entries (each typically has loc, msg, and type) 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/metacache_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 source next 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, cite reason_summary_en (and reason_points_en for bullets).
    • For "Crypto Halal" rows, cite services_en (the "Services" section) and use_cases_en (the "Use of <coin>" section); a notes_en description may also carry context. Do not cite reason_summary_en (it is null) or reason_points_en (it is []).
  • 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[] by source if 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-ID on every call — provide it (plus your key_prefix) when contacting support.
  • Monitor X-RateLimit-Remaining and back off proactively; honor Retry-After exactly on 429.
  • Retry 5xx with backoff; do not blindly retry 4xx (fix the request instead).
  • After upgrading, skim GET /v1/docs/changelog for 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.

Open dashboard