oorr / api platform
.open {API}

DEVELOPER PLATFORM07 — API Reference

API Reference Live

OORR API Platform Sargon 1.5

Request parameters

FieldTypeRequiredNotes
userIdstringYesNon-empty, ≤ 128 chars. Keys rate limit, daily allowance and usage rows. Not authenticated.
messagestringYesNon-empty, ≤ 12,000 characters.
planstringYesnormal or premium. Validated, then normalised to normal server-side — it cannot grant a paid tier.
modelTierstringNobasic | pro | ultra. Defaults to basic.
subjectstringNo≤ 100 chars. Scopes the tutor prompt.
gradestringNo≤ 100 chars. The student's stage.
conversationIdstringNo≤ 128 chars. Groups turns of one chat.
complexitystringNonormal or complex. Legacy; prefer modelTier.
historyarrayNoPrior turns of the same chat: { role: "user" | "assistant", content: string }. Bounded and validated; never stored or logged. Excludes the current question.
imageobjectNoSee Vision.

Two quirks worth knowing

plan is required by the validator even though the server immediately normalises it to normal. Omitting it is a 400, so send "plan": "normal" and select capability with modelTier.

Unrecognised fields in the request body are ignored. There is no field that selects or influences how Sargon 1.5 executes your request.

Response structure

FieldTypeNotes
answerstringThe tutor's answer, in Arabic unless an English-book policy applies.
modelstringThe public model identifier. Always sargon-1.5.
inputTokensnumberOptional — present when a token count is reported for the request.
outputTokensnumberOptional — present when a token count is reported for the request.
totalTokensnumberOptional. The figure used for daily allowance accounting.
latencyMsnumberServer-side duration.
requestIdstringUUID. Quote it in support requests.
modelTierstringOptional. The tier that served the request.
proTrialobjectOptional. Remaining Pro trial quota, for pro requests.

Token usage

Token counts come back on every successful response and, for streaming, on the terminal done event. Each request is also metered server-side — inputTokens, outputTokens, totalTokens, latencyMs, success, and the request type (text, vision, math_ocr, premium_reasoning).

Usage rows never contain prompts, questions, answers, image data or credentials. Aggregation resets daily on the Asia/Baghdad calendar day.

Token fields on a response
{
  "inputTokens": 412,
  "outputTokens": 735,
  "totalTokens": 1147
}

These counts are recorded, not yet priced

Token accounting is real and persisted. Converting it into a monetary amount is not implemented in the service — no cost field is computed, stored or returned. See Pricing.

Error responses

Every failure returns a stable code, an Arabic error message safe to show a student, and a requestId.

429 Too Many Requests
{
  "error": "لقد أرسلت سؤالاً للتو. انتظر قليلاً قبل إرسال سؤال جديد.",
  "code": "RATE_LIMITED",
  "retryAfterSeconds": 27,
  "requestId": "0f4c1b86-27a1-4f1e-9d0a-2b7c6e5a1d33"
}
CodeHTTPCause
INVALID_REQUEST400Validation failed: missing or malformed field, message too long, unsupported image type, image too large.
CONTENT_BLOCKED422The request was refused on safety grounds. Rephrase.
RATE_LIMITED429Per-caller cooldown not yet elapsed. Honour Retry-After.
DAILY_LIMIT_REACHED429Daily token allowance exhausted. Resets 00:00 Asia/Baghdad.
PRO_TRIAL_LIMIT_REACHED429Pro trial quota exhausted. Includes proTrial and retryAfterSeconds.
PRO_UNAVAILABLE503Pro tier cannot be served right now. Fall back to basic.
ULTRA_UNAVAILABLE503ultra is not released. Never downgraded silently.
PROVIDER_UNAVAILABLE503The service could not produce an answer for this request. Retry with backoff.
INTERNAL_ERROR500Unexpected server fault. Retry with backoff; quote the requestId.

Rate limits

LimitValueScopeOn breach
Request cooldown1 request / 30 sPer userId429 RATE_LIMITED + Retry-After
Daily tokens100,000 (default)Per userId, per Baghdad day429 DAILY_LIMIT_REACHED
Message length12,000 charactersPer request400 INVALID_REQUEST
Image size10 MiB decodedPer request400 INVALID_REQUEST
Pro tierTrial quotaPer userId429 PRO_TRIAL_LIMIT_REACHED

The cooldown is enforced atomically and persists across restarts. It is reserved once per accepted request, so an internal retry still counts as a single request. The server is the single source of truth for the pace — mirror Retry-After rather than hard-coding an interval, and note that a cooldown storage fault denies the request rather than letting it through.

Handling 429 correctly

async function askWithRetry(body: unknown, attempts = 3) {
  for (let attempt = 1; attempt <= attempts; attempt++) {
    const res = await fetch("https://api.oorr.ai/api/tutor", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    });

    if (res.ok) return res.json();

    const err = await res.json();

    // Never retry a validation or safety failure — it will fail identically.
    if (err.code === "INVALID_REQUEST" || err.code === "CONTENT_BLOCKED") {
      throw new Error(`${err.code}: ${err.error}`);
    }

    // Obey the server's pace; fall back to the header, then a sane default.
    const wait = err.retryAfterSeconds
      ?? Number(res.headers.get("Retry-After"))
      ?? 30;

    if (attempt === attempts) throw new Error(`${err.code} after ${attempts} attempts`);
    await new Promise((r) => setTimeout(r, wait * 1000));
  }
}