Rafiq for developers

Errors

Errors always have the same shape, so you can handle them in one place.

{
  "error": {
    "type": "rate_limit_exceeded",
    "message": "You have exceeded 30 requests per minute on this key...",
    "docs": "https://dev.rafiqai.app/docs/errors#rate_limit_exceeded",
    "limit_per_minute": 30,
    "retry_after_seconds": 42
  }
}

Branch on error.type, never on the message — messages are written for humans and will be improved over time.

StatusTypeWhat to do
400invalid_jsonThe body was not valid JSON. Check your serialisation and Content-Type.
400missing_parameterA required field is absent. error.parameter names it.
400invalid_parameterA field was the wrong shape or out of range.
400parameter_too_longShorten it; error.max gives the limit.
400range_too_largeAsk for fewer verses per call.
401missing_api_keyAdd the Authorization: Bearer header.
401invalid_api_keyThe key is wrong or revoked. Create a new one.
403account_suspendedContact us from the portal.
404not_foundThe thing you asked for does not exist — a surah/ayah out of range, or an unresolvable city.
404unknown_endpointCheck the path against this reference.
429rate_limit_exceededToo many requests this minute. Back off and retry — see below.
429quota_exceededMonthly quota spent. It resets on the 1st, or ask us to raise it.
503service_unavailableA dependency is down. Retry with backoff.
503mode_unavailableThat mode is not enabled on this deployment.
500internal_errorOur fault. It is not charged against your quota. Retry once; if it persists, tell us.

Retrying properly

Retry 429, 500 and 503. Do not retry 4xx errors caused by your own request — they will fail identically every time.

On a 429, honour the Retry-After header. Otherwise use exponential backoff with jitter: a fleet of clients all retrying after exactly two seconds is a second outage arriving on schedule.

async function callRafiq(body, attempt = 0) {
  const res = await fetch("https://api.rafiqai.app/v1/chat", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.RAFIQ_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
  });

  if (res.status === 429 || res.status >= 500) {
    if (attempt >= 4) throw new Error("Rafiq unavailable after retries");
    const retryAfter = Number(res.headers.get("Retry-After")) || 0;
    const backoff = retryAfter * 1000 || (2 ** attempt * 500 + Math.random() * 500);
    await new Promise((r) => setTimeout(r, backoff));
    return callRafiq(body, attempt + 1);
  }

  const json = await res.json();
  if (!res.ok) throw new Error(json.error?.message ?? "Request failed");
  return json;
}