Rafiq for developers

TypeScript SDK

A typed client for this API, with no dependencies. It runs anywhere there is a fetch — Node 18+, Bun, Deno, Cloudflare Workers, and the browser.

Prefer Python? There is a Python SDK with the same behaviour.

npm install rafiq-sdk
import { Rafiq } from "rafiq-sdk";

const rafiq = new Rafiq({ apiKey: process.env.RAFIQ_API_KEY });

const res = await rafiq.ask({ message: "Can I combine prayers while travelling?" });

if (res.answered) {
  console.log(res.answer.summary);
  for (const s of res.sources) console.log(`— ${s.title} (${s.citation})`);
} else {
  console.log("Rafiq had nothing on this.");
}

With RAFIQ_API_KEY set in the environment you can write new Rafiq().

Why use it rather than fetch

The API is plain HTTP and you lose nothing by calling it directly. The SDK earns its place on three things that are genuinely easy to get wrong.

It never retries an abstention

A refusal is a successful response. Retrying it re-runs the same retrieval over the same corpus, returns the same refusal, and burns quota until there is none left. The SDK retries transient failures only, and answered: false is never one of them.

The return type is a discriminated union, so the compiler makes you check before you reach for sources.

It tells the two 429s apart

rate_limit_exceeded and quota_exceeded share a status code and need opposite handling — the first clears in seconds, the second not until the 1st of the month. A client that retries every 429 spins uselessly for weeks. They are separate error classes here, and only the per-minute one is retried.

It types da'wah mode's different shape

Da'wah replies carry answer.text and sources with id/source, where the scholarly ones carry answer.summary and referenceId/citation. The method overloads narrow on the mode you pass, so you cannot read the wrong field.

Methods

MethodEndpoint
ask({ message, mode?, … })POST /v1/chat
me()GET /v1/me
quran(surah, ayah)GET /v1/quran/{surah}/{ayah}
prayerTimes({ city, country, method?, date? })GET /v1/prayer-times
prayerTimeMethods()GET /v1/prayer-times/methods
hijri()GET /v1/hijri

It is ask rather than chat because that is what it does: there is no conversation to continue, and the answer comes from what was retrieved.

await rafiq.quran(2, 255);        // one verse
await rafiq.quran(2, "255-257");  // a range, 50 verses at most

Errors

Every failure throws a typed error carrying this API's own message and its documentation link.

import { RateLimitError, QuotaExceededError } from "rafiq-sdk";

try {
  await rafiq.ask({ message });
} catch (err) {
  if (err instanceof QuotaExceededError) {
    // Out of calls for the month. Waiting will not help.
    console.log(`used ${err.used} of ${err.quota} in ${err.month}`);
  } else if (err instanceof RateLimitError) {
    // Too fast. The SDK already backed off and retried before you saw this.
    console.log(`retry in ${err.retryAfterSeconds}s`);
  }
}
ClassWhen
AuthenticationError401 — key missing, revoked or wrong
PermissionError403 — account suspended
InvalidRequestError400 — your parameters
NotFoundError404 — no such verse, city or endpoint
RateLimitError429 per-minute, per-key. Retryable
QuotaExceededError429 per-month, per-account. Not retryable
ServiceUnavailableError503 — a mode or the pipeline is down
InternalError5xx — not charged against your quota
ConnectionErrornever reached us: DNS, TLS, timeout, abort

Retries

Transient failures — 429 rate_limit_exceeded, 5xx, and network errors — are retried twice by default, honouring Retry-After when we send one and otherwise backing off exponentially with full jitter. Nothing else is retried: not your 4xx, not quota_exceeded, and not an abstention.

Rate limits

Every result carries what the headers said, so you never have to reach for them yourself.

const res = await rafiq.hijri();
res.rateLimit; // { limit, remaining, quotaLimit, quotaRemaining }

Errors carry the same on err.rateLimit. Any field is null when the header was absent.

Options

new Rafiq({
  apiKey: "rk_live_...",   // or RAFIQ_API_KEY
  baseUrl: "https://api.rafiqai.app/v1",
  timeout: 60_000,         // ms, per attempt
  maxRetries: 2,
  headers: {},             // added to every request
  fetch: myFetch,          // for tests, or a runtime with its own
});

The timeout defaults to 60 seconds because ask runs retrieval, a reranker and a model. A ten-second default would time out on perfectly healthy calls.