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
| Method | Endpoint |
|---|---|
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`);
}
}
| Class | When |
|---|---|
AuthenticationError | 401 — key missing, revoked or wrong |
PermissionError | 403 — account suspended |
InvalidRequestError | 400 — your parameters |
NotFoundError | 404 — no such verse, city or endpoint |
RateLimitError | 429 per-minute, per-key. Retryable |
QuotaExceededError | 429 per-month, per-account. Not retryable |
ServiceUnavailableError | 503 — a mode or the pipeline is down |
InternalError | 5xx — not charged against your quota |
ConnectionError | never 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.