<!--
  alAPI complete API reference for LLMs / coding agents.
  MAINTAINERS: keep this file in sync with web/templates/api_docs.html.
  When you add or change an endpoint in the HTML docs, update this file too.
  It is served at /llms-full.txt with config values (base URL, pricing) substituted.
-->

# alAPI — Complete API Reference

alAPI is an **OpenAI-compatible** API gateway for large language models. It routes
requests to **Saudi-hosted** models (e.g. ALLaM) and **global** models through one
endpoint and one API key. It also offers document **OCR** and **alRetrieval**
(retrieval-augmented search over your documents).

- **Base URL:** `https://dev.alapi.deep.sa/v1`
- **Auth:** `Authorization: Bearer sk-alapi-...` on every request.
- **Human docs:** `/docs` (available in English and Arabic — add `?lang=ar` for Arabic).
- **Last updated:** 2026-08-12

> Billing is **per request, not per token** — see Pricing below before you estimate costs.

---

## Pricing (read this first)

alAPI does **not** bill per token. Do not assume per-token pricing.

- **LLM requests are billed per request.** Every chat/completions, responses, or
  embeddings call costs **1 request**, regardless of model, prompt size, or output size.
- **Rate:** `1 SAR = 10 requests` — the same rate for **every** model
  (Saudi-hosted or global). There is no per-model or per-token multiplier.
- **OCR** is billed per page: `0.15 SAR per page`, with
  `100` free pages for new accounts.
- **VAT:** prices exclude `15%` VAT.

Check remaining balance any time with `GET /me/credits` (see below).

---

## Authentication

Every request must send an API key as a Bearer token:

```
Authorization: Bearer sk-alapi-xxxxxxxxxxxxxxxx
```

- Keys look like `sk-alapi-` followed by a hex string. Only the prefix
  (`sk-alapi-...`) is ever shown after creation — store the full key securely.
- Create and manage keys in the dashboard at `/dashboard`.
- Keys carry **scopes**. A request fails with `403` if the key lacks the scope
  its endpoint requires:

| Scope         | Grants access to                                   |
|---------------|----------------------------------------------------|
| `models`      | `/v1/chat/completions`, `/v1/responses`, `/v1/embeddings`, `/v1/models` |
| `ocr`         | `/v1/ocr/*`                                         |
| `alretrieval` | `/v1/alRetrieval/*`                                 |
| `rag`         | `/v1/rag/*` (deprecated — use alRetrieval)          |

The introspection endpoints (`/v1/me`, `/v1/me/usage`, `/v1/me/credits`) accept
**any** valid key (no specific scope) and keep working even when your balance is
exhausted, without consuming rate-limit budget.

Keys may also carry an **expiry** and **per-minute / daily rate limits** (see Errors).

---

## LLM API

OpenAI-compatible. If you use an OpenAI SDK, set the base URL to `https://dev.alapi.deep.sa/v1` and
your alAPI key — everything else is unchanged.

### Chat Completions

`POST https://dev.alapi.deep.sa/v1/chat/completions` — generate a chat completion. Requires scope `models`.

| Parameter     | Type    | Required | Description                                          |
|---------------|---------|----------|------------------------------------------------------|
| `model`       | string  | yes      | Model id from `GET /v1/models` (e.g. `allam-7b`).    |
| `messages`    | array   | yes      | List of `{role, content}` messages.                  |
| `stream`      | boolean | no       | If `true`, stream tokens as SSE (default `false`).   |
| `temperature` | number  | no       | Sampling temperature.                                |
| `max_tokens`  | integer | no       | Maximum tokens to generate.                          |

```bash
curl https://dev.alapi.deep.sa/v1/chat/completions \
  -H "Authorization: Bearer sk-alapi-xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "allam-7b",
    "messages": [
      {"role": "system", "content": "You are a helpful assistant."},
      {"role": "user", "content": "مرحبا"}
    ]
  }'
```

Example response:

```json
{
  "id": "chatcmpl-abc123",
  "object": "chat.completion",
  "created": 1737000000,
  "model": "allam-7b",
  "choices": [
    {
      "index": 0,
      "message": { "role": "assistant", "content": "مرحبا! كيف يمكنني مساعدتك؟" },
      "finish_reason": "stop"
    }
  ],
  "usage": { "prompt_tokens": 20, "completion_tokens": 12, "total_tokens": 32 }
}
```

> `usage` token counts are reported for information only. Billing is still **1 request**.

### Streaming

Set `"stream": true`. The response is Server-Sent Events: `data:` lines each carrying a
partial `chat.completion.chunk`, terminated by `data: [DONE]`.

```bash
curl https://dev.alapi.deep.sa/v1/chat/completions \
  -H "Authorization: Bearer sk-alapi-xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"model": "allam-7b", "stream": true, "messages": [{"role": "user", "content": "Count to 3"}]}'
```

```
data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"1"}}]}
data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":", 2"}}]}
data: [DONE]
```

### Embeddings

`POST https://dev.alapi.deep.sa/v1/embeddings` — create embedding vectors. Requires scope `models`.

| Parameter | Type            | Required | Description                                  |
|-----------|-----------------|----------|----------------------------------------------|
| `model`   | string          | yes      | An embedding-type model id from `/v1/models`.|
| `input`   | string or array | yes      | Text (or list of texts) to embed.            |

```bash
curl https://dev.alapi.deep.sa/v1/embeddings \
  -H "Authorization: Bearer sk-alapi-xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"model": "gte-large", "input": "مرحبا بالعالم"}'
```

Example response:

```json
{
  "object": "list",
  "data": [
    { "object": "embedding", "index": 0, "embedding": [0.0123, -0.0456, 0.0789] }
  ],
  "model": "gte-large",
  "usage": { "prompt_tokens": 4, "total_tokens": 4 }
}
```

### List Models

`GET https://dev.alapi.deep.sa/v1/models` — list active models. **Requires auth** (scope `models`).

```bash
curl https://dev.alapi.deep.sa/v1/models \
  -H "Authorization: Bearer sk-alapi-xxxxxxxxxxxxxxxx"
```

Example response:

```json
{
  "object": "list",
  "data": [
    { "id": "allam-7b", "object": "model", "created": 1737000000, "owned_by": "deepcloud", "type": "llm" },
    { "id": "gte-large", "object": "model", "created": 1737000000, "owned_by": "deepcloud", "type": "embedding" }
  ]
}
```

The `id` is what you pass as `model`. `type` is `llm` or `embedding`.

---

## OCR API

Extract text from PDFs and images. Asynchronous: upload a document, then poll the job
until `status` is `done`. All routes require scope `ocr`. Billed per page.

### Upload a document

`POST https://dev.alapi.deep.sa/v1/ocr/upload` — submit a document for OCR.

Send either a file (`multipart/form-data`, part name `file`) or a URL (`url` form field).

| Field  | Type   | Required | Description                                  |
|--------|--------|----------|----------------------------------------------|
| `file` | file   | one of   | The document (PDF/PNG/JPEG/TIFF/BMP/WEBP).    |
| `url`  | string | one of   | A URL to fetch the document from instead.    |

```bash
curl https://dev.alapi.deep.sa/v1/ocr/upload \
  -H "Authorization: Bearer sk-alapi-xxxxxxxxxxxxxxxx" \
  -F "file=@invoice.pdf"
```

Example response:

```json
{
  "token": "abc123xyz",
  "status": "pending",
  "progress": 0,
  "upload_progress": 100,
  "queue_position": 2
}
```

Save `token` — it identifies the job.

### Poll job status

`GET https://dev.alapi.deep.sa/v1/ocr/jobs/{token}` — get the OCR job status and, once complete, results.

```bash
curl https://dev.alapi.deep.sa/v1/ocr/jobs/abc123xyz \
  -H "Authorization: Bearer sk-alapi-xxxxxxxxxxxxxxxx"
```

While processing:

```json
{ "token": "abc123xyz", "status": "processing", "progress": 40 }
```

When finished (`status` is `done`), the response includes the extracted pages:

```json
{
  "token": "abc123xyz",
  "status": "done",
  "progress": 100,
  "pages": [
    { "page_num": 1, "text": "Extracted text from page 1..." }
  ]
}
```

`status` is one of `pending`, `processing`, `done`, `failed`.

### Retry a failed job

`POST https://dev.alapi.deep.sa/v1/ocr/jobs/{token}/retry` — re-run a failed job. Returns the job with
`status` reset to `pending` (same token). Poll it as above.

### Other OCR routes

- `GET https://dev.alapi.deep.sa/v1/ocr/jobs/{token}/{page_num}` — a single page's result.
- `GET https://dev.alapi.deep.sa/v1/ocr/thumbnails/{token}` — page thumbnails.
- `GET https://dev.alapi.deep.sa/v1/ocr/images/{token}` — rendered page images.

---

## alRetrieval API

Retrieval-augmented search over your own documents. Lifecycle: create a collection,
ingest documents into it, wait until each document is `ready`, then query. All routes
require scope `alretrieval`.

### Create a collection

`POST https://dev.alapi.deep.sa/v1/alRetrieval/collections` — create a new (empty) collection. No body.

```bash
curl -X POST https://dev.alapi.deep.sa/v1/alRetrieval/collections \
  -H "Authorization: Bearer sk-alapi-xxxxxxxxxxxxxxxx"
```

```json
{ "collection_id": "col_a1b2c3" }
```

### List collections

`GET https://dev.alapi.deep.sa/v1/alRetrieval/collections`

```json
{ "collections": [ { "collection_id": "col_a1b2c3", "created_at": "2026-08-01T10:00:00Z" } ] }
```

### Ingest a document

`POST https://dev.alapi.deep.sa/v1/alRetrieval/collections/{collection_id}/documents/ingest`

Two content types are accepted:

**A) A file** (`multipart/form-data`, part `file`, or a `url` field):

```bash
curl -X POST https://dev.alapi.deep.sa/v1/alRetrieval/collections/col_a1b2c3/documents/ingest \
  -H "Authorization: Bearer sk-alapi-xxxxxxxxxxxxxxxx" \
  -F "file=@handbook.pdf"
```

**B) Pre-extracted text pages** (`application/json`):

| Parameter  | Type   | Required | Description                        |
|------------|--------|----------|------------------------------------|
| `title`    | string | no       | Document title.                    |
| `filename` | string | no       | Original filename.                 |
| `pages`    | array  | yes      | List of `{ "page_num", "text" }`.  |

```bash
curl -X POST https://dev.alapi.deep.sa/v1/alRetrieval/collections/col_a1b2c3/documents/ingest \
  -H "Authorization: Bearer sk-alapi-xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Employee Handbook",
    "filename": "handbook.txt",
    "pages": [ { "page_num": 1, "text": "Welcome to the company..." } ]
  }'
```

Response:

```json
{ "id": "doc_9f8e7d", "status": "processing", "pages": 1, "collection": "col_a1b2c3" }
```

### Document status

`GET https://dev.alapi.deep.sa/v1/alRetrieval/collections/{collection_id}/documents/{doc_id}/status`

```json
{ "doc_id": "doc_9f8e7d", "status": "ready" }
```

`status` is `processing`, `ready`, or `error` (with an `error` string when `error`).
Only query a collection once its documents are `ready`.

### List documents in a collection

`GET https://dev.alapi.deep.sa/v1/alRetrieval/collections/{collection_id}/documents`

```json
{
  "collection_id": "col_a1b2c3",
  "documents": [
    { "doc_id": "doc_9f8e7d", "filename": "handbook.pdf", "status": "ready",
      "mime_type": "application/pdf", "pages": 12, "created_at": "2026-08-01T10:05:00Z" }
  ]
}
```

### Query a collection

`POST https://dev.alapi.deep.sa/v1/alRetrieval/collections/{collection_id}/query`

| Parameter | Type     | Required | Description                                                        |
|-----------|----------|----------|--------------------------------------------------------------------|
| `query`   | string   | yes      | The natural-language query.                                        |
| `doc_ids` | string[] | no       | Restrict to these documents. Omit/empty = all `ready` documents.   |

```bash
curl -X POST https://dev.alapi.deep.sa/v1/alRetrieval/collections/col_a1b2c3/query \
  -H "Authorization: Bearer sk-alapi-xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "query": "What is the vacation policy?" }'
```

Response — `context` is the ranked retrieved chunks:

```json
{
  "context": [
    {
      "title": "Employee Handbook",
      "page_start": 4,
      "page_end": 4,
      "score": 0.83,
      "snippet": "Employees accrue 21 days of paid leave per year...",
      "is_direct_hit": true,
      "page_id": 41,
      "document_id": "doc_9f8e7d"
    }
  ],
  "effective_scope_doc_ids": ["doc_9f8e7d"]
}
```

### Delete a document

`DELETE https://dev.alapi.deep.sa/v1/alRetrieval/collections/{collection_id}/documents/{doc_id}`

> A legacy single-document API also exists (`POST /v1/alRetrieval/documents/ingest`,
> `GET /v1/alRetrieval/documents/{doc_id}/status`, `POST /v1/alRetrieval/query/{doc_id}`).
> Prefer the collection API above for new integrations.

---

## Account, Usage & Credits

These read-only endpoints accept any valid key, ignore balance, and do not count
against rate limits — safe to poll.

### Key info

`GET https://dev.alapi.deep.sa/v1/me` — metadata about the calling key and its owner.

```bash
curl https://dev.alapi.deep.sa/v1/me -H "Authorization: Bearer sk-alapi-xxxxxxxxxxxxxxxx"
```

```json
{
  "key": {
    "name": "Production",
    "prefix": "sk-alapi-abc",
    "scopes": ["models", "ocr"],
    "expires_at": null,
    "rate_limit_per_minute": 60,
    "daily_request_limit": null,
    "created_at": "2026-01-01T00:00:00Z",
    "last_used_at": "2026-08-11T09:30:00Z"
  },
  "email": "you@example.com"
}
```

Nullable fields (`expires_at`, `rate_limit_per_minute`, `daily_request_limit`,
`last_used_at`) are `null` when unset.

### Usage

`GET https://dev.alapi.deep.sa/v1/me/usage` — per-day, per-key-prefix, per-model usage for a date range.

| Query param   | Type   | Required | Description                                                    |
|---------------|--------|----------|----------------------------------------------------------------|
| `start`       | string | no       | `YYYY-MM-DD`. Default: `end` − 30 days.                        |
| `end`         | string | no       | `YYYY-MM-DD`. Default: today (UTC).                            |
| `granularity` | string | no       | Only `day` is supported (default `day`).                       |

The range must not exceed **90 days**, and `end` must be on or after `start`, else `400`.

```bash
curl "https://dev.alapi.deep.sa/v1/me/usage?start=2026-07-01&end=2026-07-31" \
  -H "Authorization: Bearer sk-alapi-xxxxxxxxxxxxxxxx"
```

```json
{
  "start": "2026-07-01",
  "end": "2026-07-31",
  "granularity": "day",
  "data": [
    {
      "date": "2026-07-01",
      "key_prefix": "sk-alapi-abc",
      "model": "allam-7b",
      "requests": 42,
      "tokens_prompt": 5120,
      "tokens_completion": 3300,
      "failed": 1
    }
  ]
}
```

### Credits

`GET https://dev.alapi.deep.sa/v1/me/credits` — remaining request and OCR-page balances (team-aware).

```bash
curl https://dev.alapi.deep.sa/v1/me/credits -H "Authorization: Bearer sk-alapi-xxxxxxxxxxxxxxxx"
```

```json
{
  "requests": { "balance": 1000, "used": 240, "remaining": 760 },
  "ocr_pages": { "balance": 100, "used": 12, "remaining": 88 },
  "team": null
}
```

If the key's owner is a member of a team, `team` is an object with `request_credit`,
`request_used`, `request_remaining`, `ocr_credit`, `ocr_used`.

---

## Errors

alAPI uses two error body shapes depending on the service family. **Check the HTTP
status AND the `type`/`code` field**, not the human message.

**LLM / account endpoints** (`/v1/chat/completions`, `/v1/responses`, `/v1/embeddings`,
`/v1/models`, `/v1/me*`) return a **nested** error object:

```json
{ "error": { "message": "API key expired", "type": "auth_error", "code": "api_key_expired" } }
```

**OCR and alRetrieval endpoints** (`/v1/ocr/*`, `/v1/alRetrieval/*`) return a **flat**
error with a string message and optional `code`:

```json
{ "error": "API key expired", "code": "api_key_expired" }
```

| HTTP | `type` (LLM) | `code` (when present) | Meaning                                                        |
|------|--------------|-----------------------|----------------------------------------------------------------|
| 400  | `invalid_request_error` | —          | Malformed request (bad params/body); `/me/usage` range errors. |
| 401  | `auth_error` | —                     | Missing/invalid `Authorization` header or unknown key.         |
| 401  | `auth_error` | `api_key_expired`     | The key's expiry has passed.                                   |
| 403  | `auth_error` | —                     | Account inactive, or key missing the required scope.           |
| 402  | `billing_error` | —                  | Request balance exhausted (top up in the dashboard).           |
| 429  | `rate_limit_error` | `rate_limited`  | Per-minute rate limit exceeded.                                |
| 429  | `rate_limit_error` | `daily_limit_exceeded` | Daily request limit exceeded.                           |

Notes:
- On LLM/account endpoints the 429 body uses `"type": "rate_limit_error"` (no `code`).
  On OCR/alRetrieval the 429 body uses `"code": "rate_limited"` or
  `"code": "daily_limit_exceeded"`. Both cases send the same rate-limit headers below.
- `402` (balance exhausted) is only enforced on request-consuming endpoints; the
  `/v1/me*` endpoints keep working when the balance is 0.

### Rate-limit headers (on every 429)

| Header                  | Meaning                                                        |
|-------------------------|----------------------------------------------------------------|
| `Retry-After`           | Seconds to wait before retrying (rounded up, ≥ 1).             |
| `X-RateLimit-Limit`     | The configured limit that was hit.                             |
| `X-RateLimit-Remaining` | Remaining calls in the window (0 on a trip).                  |
| `X-RateLimit-Reset`     | Unix timestamp (seconds) when the window/day resets.          |

---

## Practical notes

- **Base URL:** `https://dev.alapi.deep.sa/v1`. The same endpoints are also mounted under `/api/v1/...`.
- **OpenAI SDKs work unchanged** — set `base_url` to `https://dev.alapi.deep.sa/v1` and use your alAPI key.
- **`GET /v1/models` requires authentication** (unlike some providers).
- **Model ids can contain `/`** (e.g. `deep-sa/allam`). URL-encode the slash as `%2F`
  when a model id appears in a path segment (it is fine as-is inside a JSON body).
- **Flat per-request billing**: 1 request = 1 unit, any model, any size. `1 SAR = 10 requests`.
- **Usage & logs** are queryable via `GET /v1/me/usage`; the dashboard (`/usage`) also
  offers a CSV export of raw request logs.
- **Arabic docs:** the human documentation is bilingual at `/docs?lang=ar`. This
  machine-readable file is English only.
