BETA

API Documentation

Complete reference for alAPI's LLM Models, alGuardrail, alOCR, alEmbedding and alRetrieval

v1 https://alapi.deep.sa/v1
View as Markdown

Getting started

One API key and one base URL for every product. Give the key the scope of each product it calls.

Base URL
https://alapi.deep.sa/v1

Authentication

All API requests require authentication using a Bearer token in the Authorization header.

Request Header:

header
Authorization: Bearer YOUR_API_KEY

API Key: Generate API keys from your Dashboard.

SDK Setup Example:

main.py
from openai import OpenAI

client = OpenAI(
    api_key="YOUR_API_KEY",
    base_url="https://alapi.deep.sa/v1"
)

LLM Models

OpenAI-compatible chat completions and responses. Use Saudi-hosted and global models through one interface.

Path
/v1/chat/completions
Key scope
models

OpenAI Compatible Use the official OpenAI SDK with our base URL. Drop-in replacement for existing applications.

POSTChat Completions

Create Chat Completion

Creates a model response for the given chat conversation

POST

Endpoint:

endpoint
https://alapi.deep.sa/v1/chat/completions

Request Body:

request.json
{
  "model": "llama-3.3-70b-versatile",
  "messages": [
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": "Hello!"}
  ],
  "temperature": 0.7,
  "max_tokens": 1024,
  "stream": false
}

Parameters:

Name Type Required Description
model string Yes ID of the model to use
messages array Yes Array of message objects with role and content
temperature number No Sampling temperature (0-2). Default: 1
max_tokens integer No Maximum tokens to generate
stream boolean No If true, returns a stream of events
top_p number No Nucleus sampling parameter. Default: 1

Response (200 OK):

response.json
{
  "id": "chatcmpl-abc123",
  "object": "chat.completion",
  "created": 1706745000,
  "model": "llama-3.3-70b-versatile",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "Hello! How can I help you today?"
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 20,
    "completion_tokens": 10,
    "total_tokens": 30
  }
}

Code Examples:

main.py
from openai import OpenAI

client = OpenAI(
    api_key="YOUR_API_KEY",
    base_url="https://alapi.deep.sa/v1"
)

response = client.chat.completions.create(
    model="llama-3.3-70b-versatile",
    messages=[
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "What is the capital of Saudi Arabia?"}
    ],
    temperature=0.7,
    max_tokens=1024
)

print(response.choices[0].message.content)

POSTStreaming Responses

Server-Sent Events (SSE)

Stream responses token by token for real-time output

SSE

How it works: Set stream: true in your request. The response will be sent as Server-Sent Events, with each chunk containing a delta of the response content.

Code Examples:

main.py
from openai import OpenAI

client = OpenAI(
    api_key="YOUR_API_KEY",
    base_url="https://alapi.deep.sa/v1"
)

stream = client.chat.completions.create(
    model="llama-3.3-70b-versatile",
    messages=[
        {"role": "user", "content": "Write a short poem about coding"}
    ],
    stream=True
)

for chunk in stream:
    if chunk.choices[0].delta.content:
        print(chunk.choices[0].delta.content, end="", flush=True)

GETAvailable Models

List Models

Returns the list of currently available models

GET

Endpoint:

endpoint
https://alapi.deep.sa/v1/models

Required Scope: This endpoint requires an API key with the models or alembedding

Response (200 OK):

response.json
{
  "object": "list",
  "data": [
    {
      "id": "qwen/qwen3.8-27b",
      "object": "model",
      "created": 1767225600,
      "owned_by": "qwen",
      "type": "llm",
      "pricing": { "currency": "SAR", "input_per_1m": 1.41, "output_per_1m": 14.37 },
      "hosted_in": "saudi",
      "context_length": 131072,
      "supports_tools": true
    },
    {
      "id": "google/gemini-3.8-flash",
      "object": "model",
      "created": 1791277200,
      "owned_by": "google",
      "type": "llm",
      "pricing": { "currency": "SAR", "input_per_1m": 3.52, "output_per_1m": 17.58 },
      "hosted_in": "global",
      "context_length": 1048576,
      "supports_tools": true
    }
  ],
  "deprecated": [
    {
      "id": "qwen/qwen3.6-27b",
      "replacement": "qwen/qwen3.8-27b",
      "sunset": "2026-12-05T00:00:00+03:00",
      "message": "qwen/qwen3.6-27b is retired. Requests are served by qwen/qwen3.8-27b until 2026-12-05; update your model name."
    }
  ]
}

Model fields:

FieldDescription
pricingThe pay-as-you-go price in SAR per 1M tokens, for input and output. An embedding model has no output price. A plan pays a share of this price: see the pricing page. null when the model is not sold by tokens.
hosted_insaudi for models hosted in Saudi Arabia, global for the rest.
context_lengthThe most tokens the model handles in one call, or null when unknown.
supports_toolstrue when the model accepts tools (function calling).
deprecatedA top-level list of retired model names that still work, each with its replacement, sunset date and message.

GET/v1/models/{name}

Returns one model in the same shape. A retired name that still works returns its replacement with a deprecated object; an unknown name returns 404 model_not_found.

GET /v1/models/qwen%2Fqwen3.6-27b
{
  "id": "qwen/qwen3.8-27b",
  "object": "model",
  "created": 1767225600,
  "owned_by": "qwen",
  "type": "llm",
  "pricing": { "currency": "SAR", "input_per_1m": 1.41, "output_per_1m": 14.37 },
  "hosted_in": "saudi",
  "context_length": 131072,
  "supports_tools": true,
  "deprecated": {
    "replacement": "qwen/qwen3.8-27b",
    "sunset": "2026-12-05T00:00:00+03:00",
    "message": "qwen/qwen3.6-27b is retired. Requests are served by qwen/qwen3.8-27b until 2026-12-05; update your model name."
  }
}

Code Examples:

main.py
from openai import OpenAI

client = OpenAI(
    api_key="YOUR_API_KEY",
    base_url="https://alapi.deep.sa/v1"
)

models = client.models.list()
for model in models.data:
    print(f"{model.id} ({model.type})")

The following models are currently available through alAPI. Use the model name in your API requests.

Model Name Type Avg Latency
anthropic/claude-sonnet-5.5 9.38 / 46.88 SAR per 1M tokens
llm ~775ms
deep-sa/alEmbedding 0.19 SAR per 1M input tokens
embedding ~158ms
deep-sa/alEmbedding-2.0 0.19 SAR per 1M input tokens
embedding ~81ms
google/gemini-3.1-pro 9.38 / 56.25 SAR per 1M tokens
llm ~2874ms
google/gemini-3.8-flash 3.52 / 17.58 SAR per 1M tokens
llm ~4120ms
gpt-oss-120b 0.84 / 3.37 SAR per 1M tokens
llm ~3559ms
gpt-oss-20b 0.42 / 1.69 SAR per 1M tokens
llm ~494ms
llama-3.3-70b 0.56 / 1.80 SAR per 1M tokens
llm ~446ms
minimaxai/minimax-m2.7 1.18 / 4.72 SAR per 1M tokens
llm ~577ms
openai/gpt-6-luna 0.47 / 2.34 SAR per 1M tokens
llm ~17977ms
qwen/qwen3.8-27b 1.41 / 14.37 SAR per 1M tokens
llm ~1297ms
z-ai/glm-5.3-flash 0.70 / 2.34 SAR per 1M tokens
llm ~936ms

alGuardrail

One endpoint: send text and get back the personal data in it, or the same text with that data redacted. Use an API key with the alguardrail scope; each call counts as one request.

Path
/v1/alGuardrail
Key scope
alguardrail

POSTDetect Personal Data

Detect

Find personal data in Arabic, English or mixed text.

POST

Endpoint:

POST /v1/alGuardrail/detect

Request Body:

{
  "text": "جوالي 0551234567 وايميلي sara@example.sa",
  "mode": "redact"
}

Parameters:

NameTypeRequiredDescription
textstringYesArabic, English or mixed text, of any length.
modestringNodetect (default) returns the spans only. redact adds output, with every span replaced by [REDACTED]. tokenize adds output with <LABEL_N> tokens and a tokens map, to put the original values back after the model call.
thresholdnumberNoDrop spans whose confidence is below this value, from 0 to 1.
custom_labelsarrayNoUp to three extra labels, each with a short description, with no retraining.

Response (200 OK):

{
  "text": "جوالي 0551234567 وايميلي sara@example.sa",
  "spans": [
    {"label": "phone number", "text": "0551234567", "start": 6, "end": 16, "confidence": 1.0},
    {"label": "email", "text": "sara@example.sa", "start": 25, "end": 40, "confidence": 1.0}
  ],
  "output": "جوالي [REDACTED] وايميلي [REDACTED]",
  "latency_ms": 1467.4
}

Response fields:

NameDescription
spansEach span has label, text, start and end (its character offsets in text) and confidence.
outputPresent in the redact and tokenize modes; the text after processing.
tokensPresent in the tokenize mode; maps each <LABEL_N> token to its original value.
latency_msThe model's processing time for this request.

Code Examples:

curl -s -X POST "https://alapi.deep.sa/v1/alGuardrail/detect" \
  -H "Authorization: Bearer $ALAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "جوالي 0551234567 وايميلي sara@example.sa", "mode": "redact"}'

Errors:

Errors are returned as JSON with a message in the error field. Usage limits follow the key's settings.

401The key is missing, invalid or expired.
402The account has no requests left.
403The key does not have the alguardrail scope, or the account is not active.
413The request body is larger than 1 MiB.
429Rate limit or daily limit exceeded. See the Retry-After and X-RateLimit-Remaining headers; the body's code field is rate_limited or daily_limit_exceeded.
502The detection service could not be reached.

alOCR

Extract text from documents with advanced Arabic and English OCR. Supports PDFs and images with automatic deskewing and layout detection.

Path
/v1/ocr
Key scope
ocr

Supported file formats

PDF, JPG, PNG, TIFF, BMP, WEBP

Supports Arabic and English text extraction with automatic language detection

POSTUpload Document

Upload File for OCR

Upload a document and start OCR processing

POST

Endpoint:

endpoint
https://alapi.deep.sa/v1/ocr/upload

Request Format:

Two input modes are accepted: upload a file directly (multipart/form-data) or supply a public HTTPS URL and let the server fetch the PDF for you (application/x-www-form-urlencoded).

Mode A — File Upload: multipart/form-data

Name Type Description
file file Document file: PDF, JPG, PNG, TIFF, BMP or WEBP (a TIFF is read from its first page)

Mode B — URL Fetch: application/x-www-form-urlencoded

Name Type Description
url string Public HTTPS URL of a PDF or image file. The server fetches and validates the content.
filename string Optional filename hint. Defaults to the URL path basename.
Security: Only HTTPS URLs to public hosts are accepted. Private IPs, cloud-metadata addresses (169.254.169.254, etc.), CGNAT, loopback, and link-local ranges are blocked. The server does not forward your Authorization header when fetching the remote file. Maximum file size: 100 MiB.

Response (200 OK):

response.json
{
  "token": "abc123xyz...",
  "status": "queued",
  "progress": 0,
  "upload_progress": 100,
  "queue_position": 1
}

Code Examples:

Send the file directly as multipart/form-data.

upload.py
import requests

url = "https://alapi.deep.sa/v1/ocr/upload"
headers = {"Authorization": "Bearer YOUR_API_KEY"}

with open("document.pdf", "rb") as f:
    files = {"file": f}
    response = requests.post(url, headers=headers, files=files)

result = response.json()
token = result["token"]
print(f"Job started, token: {token}")

GETJob Status

Get Job Status

Retrieve the current status and results of an OCR job

GET

Endpoint:

endpoint
https://alapi.deep.sa/v1/ocr/jobs/{token}

Status Values:

Status Description
pending
Job is waiting in the processing queue
processing
Document is being processed
done
Processing complete. Results are in the pages array; a page that could not be read carries an error and is not charged
failed
Processing failed. Check the error field for details

Response (when status is done):

response.json
{
  "filename": "document.pdf",
  "status": "done",
  "progress": 100,
  "total_pages": 5,
  "pages": [
    {
      "page_num": 1,
      "text": "Extracted text from page 1..."
    }
  ],
  "pages_failed": 0
}

Code Examples:

get_status.sh
curl "https://alapi.deep.sa/v1/ocr/jobs/YOUR_TOKEN_HERE" \
  -H "Authorization: Bearer YOUR_API_KEY"

POSTRetry a Job

Resubmit a failed or stuck job without re-uploading the file. alAPI keeps a copy of every upload in object storage for the lifetime of the job, so retries reuse that copy. The client-facing token stays the same across retries.

Retry OCR Job

Safe to call on any job in failed state with retryable: true.

POST

Endpoint:

endpoint
https://alapi.deep.sa/v1/ocr/jobs/{token}/retry

Response (200 OK):

{
  "token": "…same token you already have…",
  "status": "pending",
  "retry_count": 1
}

Error responses:

Status Meaning
404Token unknown or not yours
409Job is already done / still uploading
422Legacy job without a retryable copy — re-upload
429Hit retry_count cap (3 by default)
502Upstream alOCR transport failure — safe to retry

Tip: a background worker automatically retries failed-retryable jobs with exponential backoff. You only need to call /retry if you want to fail fast or surface an error to a user.

cURL:

curl -X POST "https://alapi.deep.sa/v1/ocr/jobs/YOUR_TOKEN_HERE/retry" \
  -H "Authorization: Bearer YOUR_API_KEY"

GETGet Single Page

Get Page by Number

Retrieve one page: its text in reading order, lines, layout regions, tables, extracted fields and image links. Add ?markdown=1 for a Markdown rendering

GET

Endpoint:

endpoint
https://alapi.deep.sa/v1/ocr/jobs/{token}/{page_num}

Request Format:

Name Type Description
token String Access token received from upload
page_num Integer Page number (1-indexed)

Response (200 OK):

response.json
{
  "page_num": 1,
  "text": "Extracted text from this page...",
  "direction": "rtl",
  "lines": [{ "text": "...", "box": [40, 52, 980, 88], "conf": 0.98 }],
  "regions": [{ "cls": "paragraph", "box": [40, 52, 980, 300] }],
  "tables": [{ "rows": [["cell", "cell"]] }],
  "fields": { "dates": [], "totals": [] },
  "thumbnail": {
    "url": "https://...presigned-url..."
  },
  "image": {
    "url": "https://...presigned-s3-url..."
  },
  "status": "done"
}

thumbnail and image are short-lived links (valid for about an hour); request the page again for fresh ones.

Code Examples:

get_page.py
import requests

API_BASE = "https://alapi.deep.sa/v1"
API_KEY = "YOUR_API_KEY"
headers = {"Authorization": f"Bearer {API_KEY}"}

token = "YOUR_TOKEN_HERE"
page_num = 3

response = requests.get(
    f"{API_BASE}/ocr/jobs/{token}/{page_num}",
    headers=headers
)

page = response.json()
print(f"Page {page['page_num']}: {page['status']}")
print(page["text"])

Error Handling:

Status Description
400 page_num must be >= 1
404 Job or page not found
410 The job's results are no longer available

GETThumbnails

Get Page Thumbnails

Retrieve page thumbnails with pagination

GET

Endpoint:

endpoint
https://alapi.deep.sa/v1/ocr/thumbnails/{token}?start=0&limit=10

Response:

response.json
{
  "thumbnails": [
    {
      "page_num": 1,
      "url": "https://...presigned-url..."
    }
  ],
  "start": 0,
  "end": 10,
  "total_pages": 25,
  "has_more": true
}

GETPage Images

Get Full-Resolution Page Images

Retrieve paginated full-resolution page images (same pattern as thumbnails)

GET

Endpoint:

endpoint
https://alapi.deep.sa/v1/ocr/images/{token}?start=0&limit=10

Parameters:

Name Type Required Description
token String Yes Access token received from upload
start Integer No Offset to start from (default: 0)
limit Integer No Number of images to return (default: 10, max: 50)

Response (200 OK):

response.json
{
  "images": [
    {
      "page_num": 1,
      "url": "https://...presigned-s3-url..."
    }
  ],
  "start": 0,
  "end": 10,
  "total_pages": 25,
  "has_more": true
}

Image links are short-lived (valid for about an hour); request the list again for fresh ones. Only pages that have been read are listed.

DELETEDelete a job

alOCR keeps your uploaded file only until the job is done (a failed job keeps it while it can be retried, at most 12 hours) and a finished job's results for 12 hours after it finished. After that both are deleted, along with alOCR's own copy, and the job's routes answer 410 job_expired. Jobs made through alRetrieval or alPlatform keep their data, since those products store what you upload, until you delete them.

To delete a job sooner, call DELETE on it, even while it is still being read: the reading stops and the job is not charged. Its file, results and file name are deleted at once. The job stays in your usage and billing, without its content.

Endpoint:

curl
curl -X DELETE https://alapi.deep.sa/v1/ocr/jobs/{token} \
  -H "Authorization: Bearer YOUR_API_KEY"

Response:

{
  "job_id": "550e8400-e29b-41d4-a716-446655440000",
  "token": "abc123xyz",
  "status": "deleted",
  "deleted_at": "2026-10-25T10:12:00Z"
}
  • 410 job_deleted · every route for a deleted job answers this afterwards; deleting it again answers 200
  • 409 job_in_progress · the file is still being uploaded; delete the job once the upload has finished
  • 503 delete_incomplete · storage did not respond; the job is deleted and its data is removed automatically, or call DELETE again

alEmbedding

Turn text into vectors for search and retrieval, through the OpenAI-compatible embeddings endpoint. Needs a key with the alembedding scope.

POSTEmbeddings

Path
/v1/embeddings
Key scope
alembedding

Create Embeddings

Creates an embedding vector representing the input text

POST

Endpoint:

endpoint
https://alapi.deep.sa/v1/embeddings

Request Body:

request.json
{
  "model": "deep-sa/alEmbedding",
  "input": "The quick brown fox jumps over the lazy dog"
}

Parameters:

Name Type Required Description
model string Yes ID of the embedding model to use
input string | array Yes Text to embed: a non-empty string, or an array of non-empty strings (an empty one fails the whole request)
encoding_format string No Format for the embeddings: 'float' or 'base64'. Default: float
dimensions integer No Number of dimensions for the output embeddings (model-dependent)
input_type string No deep-sa/alEmbedding-2.0 only: "query" (default) for search text, "passage" for text you store. Exactly one of the two; the response echoes it

Response (200 OK):

response.json
{
  "object": "list",
  "data": [
    {
      "object": "embedding",
      "index": 0,
      "embedding": [0.0023064255, -0.009327292, ...]
    }
  ],
  "model": "deep-sa/alEmbedding",
  "usage": {
    "prompt_tokens": 9,
    "total_tokens": 9
  }
}

Code Examples:

main.py
from openai import OpenAI

client = OpenAI(
    api_key="YOUR_API_KEY",
    base_url="https://alapi.deep.sa/v1"
)

response = client.embeddings.create(
    model="deep-sa/alEmbedding",
    input="The quick brown fox jumps over the lazy dog"
)

embedding = response.data[0].embedding
print(f"Embedding dimension: {len(embedding)}")

alRetrieval

Ingest documents and query them with natural-language semantic retrieval. Supports PDF, DOCX, PPTX, TXT, and Markdown files.

Path
/v1/alRetrieval
Key scope
alretrieval

Supported file formats

PDF, DOCX, PPTX, TXT, MD

PDFs are processed via OCR; DOCX, PPTX, TXT, and MD have text extracted directly — no OCR cost

Scope Required: All alRetrieval endpoints require an API key with the alretrieval scope. Pass it as a Bearer token in the Authorization header.

How it works

Every request works inside a collection that belongs to your account: create a collection, upload documents, wait until they are ready, then retrieve passages or generate an answer that cites them. Lists are paged with limit, offset, total and has_more. Every response carries an X-Request-Id header; quote it when you contact support.

Code Examples:

retrieval_quickstart.py
import time, requests

BASE = "https://alapi.deep.sa/v1/alRetrieval"
headers = {"Authorization": "Bearer YOUR_API_KEY"}

# 1. Create a collection
cid = requests.post(f"{BASE}/collections", json={"name": "HR policies"}, headers=headers).json()["id"]

# 2. Upload a PDF
with open("leave-policy.pdf", "rb") as f:
    doc = requests.post(f"{BASE}/collections/{cid}/documents",
                        files={"file": f}, data={"title": "Leave policy"}, headers=headers).json()

# 3. Wait until it is searchable and its section tree is settled
while True:
    s = requests.get(f"{BASE}/collections/{cid}/documents/{doc['id']}/status", headers=headers).json()
    if s["status"] == "failed":
        raise RuntimeError(s["ingest"]["error"])
    if s["status"] == "ready" and s["deep_tree"]["status"] not in ("waiting", "queued", "processing"):
        break
    time.sleep(3)

# 4. Ask a question
answer = requests.post(f"{BASE}/collections/{cid}/answer",
                       json={"query": "كم مدة الإجازة السنوية؟"}, headers=headers).json()
print(answer["answer"])

POSTCreate a collection

Returns 201 with the new collection and a Location header. The body is optional.

POST /v1/alRetrieval/collections

Parameters:

ParameterTypeDescription
namestringoptional — up to 200 characters.

Response (201 Created):

{
  "id": "6a51b4e5c0d24f0e9b1a7c3d2e8f4a10",
  "name": "HR policies",
  "document_count": 0,
  "created_at": "2026-10-03T10:00:00Z"
}

Code Examples:

retrieval_create.py
import requests

headers = {"Authorization": "Bearer YOUR_API_KEY"}
collection = requests.post("https://alapi.deep.sa/v1/alRetrieval/collections",
                           json={"name": "HR policies"}, headers=headers).json()
print(collection["id"])

GETList collections

Your collections, newest first.

GET /v1/alRetrieval/collections?limit=100&offset=0

Parameters:

ParameterTypeDescription
limitintegeroptional — 1 to 500, default 100.
offsetintegeroptional — collections to skip, default 0.

Response (200 OK):

{
  "collections": [
    { "id": "6a51b4e5c0d24f0e9b1a7c3d2e8f4a10", "name": "HR policies", "document_count": 3, "created_at": "..." }
  ],
  "limit": 100,
  "offset": 0,
  "total": 1,
  "has_more": false
}

GETGet a collection

Returns the collection object.

GET /v1/alRetrieval/collections/{collection_id}
404 collection_not_found The collection does not exist in your account.

DELETEDelete a collection

Deletes the collection and every document in it.

DELETE /v1/alRetrieval/collections/{collection_id}

This cannot be undone.

Response (200 OK):

{ "id": "6a51b4e5c0d24f0e9b1a7c3d2e8f4a10", "deleted": true, "deleted_documents": 3 }

POSTUpload a document

Send a file or a URL as multipart/form-data, or text you have already extracted as JSON pages. Returns 202 with the document in status: "processing" and a Location header pointing at its status endpoint. A collection holds up to 500 documents.

POST /v1/alRetrieval/collections/{collection_id}/documents

Parameters:

ParameterTypeDescription
filefilePDF, DOCX, PPTX, TXT or MD, up to 100 MB.
urlstringInstead of file: a public HTTPS link alAPI downloads the document from (optional filename).
titlestringoptional — defaults to the file name without its extension.
pagesobject[]JSON body instead of a file: 1 to 5,000 entries of {"page_num", "text"}, with optional title and filename.

PDFs are OCR'd and charged per page from your OCR balance; the upload is refused with 402 when the file has more pages than you have available. Once processed, PDFs also get section-aware retrieval.

Response (202 Accepted):

{
  "id": "3146366e04ff",
  "collection_id": "6a51b4e5c0d24f0e9b1a7c3d2e8f4a10",
  "title": "Leave policy",
  "filename": "leave-policy.pdf",
  "source": "pdf",
  "page_count": 0,
  "created_at": "2026-10-03T10:01:12Z",
  "status": "processing",
  "ingest": { "stage": "queued", "error": null },
  "deep_tree": { "status": "waiting", "degraded": false, "error": null },
  "pipeline": null
}
400 missing_file The upload had no file, url or pages.
402 insufficient_balance Not enough OCR pages for this PDF.
409 document_limit_reached The collection already holds 500 documents.
413 file_too_large Files are limited to 100 MB.
415 unsupported_file_type Send PDF, DOCX, PPTX, TXT or MD.

Code Examples:

retrieval_upload.py
import requests

headers = {"Authorization": "Bearer YOUR_API_KEY"}
url = "https://alapi.deep.sa/v1/alRetrieval/collections/COLLECTION_ID/documents"

# A file
with open("leave-policy.pdf", "rb") as f:
    doc = requests.post(url, files={"file": f}, data={"title": "Leave policy"}, headers=headers).json()

# A public URL
doc = requests.post(url, data={"url": "https://example.com/handbook.pdf"}, headers=headers).json()

# Text you already extracted
doc = requests.post(url, json={
    "title": "دليل الموظف",
    "pages": [{"page_num": 1, "text": "سياسة الإجازات: ..."}],
}, headers=headers).json()
print(doc["id"], doc["status"])

GETGet document status

Poll every 2 to 5 seconds after an upload. A PDF first becomes searchable (status: ready), then its section tree is built in the background, usually within a few minutes. You can search as soon as status is ready; waiting for deep_tree as well makes results whole sections instead of pages.

GET /v1/alRetrieval/collections/{collection_id}/documents/{document_id}/status

Response (200 OK):

{
  "id": "3146366e04ff",
  "status": "ready",
  "ingest": { "stage": "ready", "error": null },
  "deep_tree": { "status": "ready", "degraded": false, "error": null },
  "pipeline": "deep_tree"
}
statusDescription
processingProcessing is running; ingest.stage names the step.
readySearchable.
failedProcessing stopped; ingest.error says why. Call retry.
deep_tree.statusDescription
not_supportedText documents: always searched page by page.
waiting / queued / processingThe section tree is on its way.
readySearches use whole sections. degraded: true means no usable table of contents was found.
skipped / failedNo tree; the document still works page by page. Retry rebuilds a failed tree.

pipeline is what a search of this document alone would use: deep_tree (sections) or basic (pages), or null while it is not ready. A search uses deep_tree only when every document in its scope has a ready tree.

GETList documents

Every document in the collection with its current status, newest first.

GET /v1/alRetrieval/collections/{collection_id}/documents?limit=500&offset=0

Parameters:

ParameterTypeDescription
limitintegeroptional — 1 to 500, default 500.
offsetintegeroptional — documents to skip, default 0.

Response (200 OK):

{
  "collection_id": "6a51b4e5c0d24f0e9b1a7c3d2e8f4a10",
  "documents": [ { "id": "3146366e04ff", "title": "Leave policy", "status": "ready", "pipeline": "deep_tree", ... } ],
  "limit": 500,
  "offset": 0,
  "total": 1,
  "has_more": false
}

GETGet a document

Returns the full document object.

GET /v1/alRetrieval/collections/{collection_id}/documents/{document_id}

Response (200 OK):

{
  "id": "3146366e04ff",
  "collection_id": "6a51b4e5c0d24f0e9b1a7c3d2e8f4a10",
  "title": "Leave policy",
  "filename": "leave-policy.pdf",
  "source": "pdf",
  "page_count": 10,
  "created_at": "2026-10-03T10:01:12Z",
  "status": "ready",
  "ingest": { "stage": "ready", "error": null },
  "deep_tree": { "status": "ready", "degraded": false, "error": null },
  "pipeline": "deep_tree"
}
fieldDescription
sourcepdf or text.
page_countPages found; 0 until processing finishes.
status / ingest / deep_tree / pipelineAs in the status endpoint.

GETGet page text

The text extracted from each page, once the document is ready.

GET /v1/alRetrieval/collections/{collection_id}/documents/{document_id}/pages?start=1&end=3

Parameters:

ParameterTypeDescription
startintegeroptional — first page, from 1.
endintegeroptional — last page, inclusive. Default: the last page.

Response (200 OK):

{ "document_id": "3146366e04ff", "pages": [ { "page_num": 1, "text": "..." } ] }
409 document_not_ready Wait until the document is ready.

POSTRetry a failed document

If processing failed, runs it again; otherwise, if the section tree failed, builds the tree again. Returns 202 with the document. If OCR itself failed on the file, upload it again instead.

POST /v1/alRetrieval/collections/{collection_id}/documents/{document_id}/retry
409 nothing_to_retry Nothing failed; no action needed.

DELETEDelete a document

Deletes the document, its text and its index.

DELETE /v1/alRetrieval/collections/{collection_id}/documents/{document_id}

This cannot be undone.

Response (200 OK):

{ "id": "3146366e04ff", "deleted": true }

POSTRetrieve passages

Returns the passages most relevant to a query, without generating an answer. Use it to build your own prompt or show search results. Costs 1 request.

POST /v1/alRetrieval/collections/{collection_id}/retrieve

Parameters:

ParameterTypeDescription
querystringrequired — 1 to 4,000 characters, Arabic or English.
document_idsstring[]optional — search only these documents; omit to search every ready document.
top_kintegeroptional — 1 to 20, default 10.

Response (200 OK):

{
  "pipeline": "deep_tree",
  "document_ids": ["3146366e04ff"],
  "results": [
    {
      "document_id": "3146366e04ff",
      "document_title": "Leave policy",
      "text": "Document: Leave policy\nContext path: Chapter 2 / Article 5\n...",
      "score": 0.5773,
      "page_start": 4,
      "page_end": 5,
      "section": "Chapter 2: Leave / Article 5",
      "section_paths": ["Chapter 2: Leave / Article 5"],
      "is_direct_hit": true
    }
  ]
}

section is the heading path for citations (null for page results). Compare score only within one response.

409 no_ready_documents No document in scope is ready yet.
409 document_not_ready A document in document_ids is not ready.
504 retrieval_timeout Retry, or narrow document_ids.

Code Examples:

retrieval_retrieve.py
import requests

headers = {"Authorization": "Bearer YOUR_API_KEY"}
r = requests.post("https://alapi.deep.sa/v1/alRetrieval/collections/COLLECTION_ID/retrieve",
                  json={"query": "كم مدة الإجازة السنوية؟", "top_k": 5}, headers=headers).json()
for p in r["results"]:
    print(p["document_title"], p["section"] or f"page {p['page_start']}", p["score"])

POSTGenerate an answer

Retrieves passages the same way as retrieve, then writes an answer grounded in them, in the language of the question, with inline citations: ((مسار: …)) for sections, ((صفحة N)) for pages. Use the structured sources array in your interface. Costs 2 requests (retrieval + generation). The API keeps no conversation state: send earlier turns in history.

POST /v1/alRetrieval/collections/{collection_id}/answer

Parameters:

ParameterTypeDescription
querystringrequired — 1 to 4,000 characters.
document_idsstring[]optional — as in retrieve.
historyobject[]optional — earlier turns, oldest first: {"role": "user" | "assistant", "content"}.
streambooleanoptional — stream the answer as server-sent events.

Response (200 OK):

{
  "answer": "الإجازة السنوية مدتها ثلاثون يوماً ((صفحة 1)).",
  "pipeline": "basic",
  "document_ids": ["3146366e04ff"],
  "sources": [ { "document_id": "3146366e04ff", "document_title": "Leave policy", "text": "...", "score": 1.0,
                 "page_start": 1, "page_end": 1, "section": null, "section_paths": [], "is_direct_hit": true } ]
}

Streaming:

With "stream": true the response is text/event-stream: one sources event, many delta events, then done. An error event replaces done if generation fails mid-stream. Lines starting with : are keep-alive comments; ignore them.

event: sources
data: {"pipeline": "deep_tree", "document_ids": ["3146366e04ff"], "sources": [ ... ]}

event: delta
data: {"text": "الإجازة السنوية مدتها "}

event: delta
data: {"text": "ثلاثون يوماً ..."}

event: done
data: {"finish_reason": "stop"}
502 generation_failed A temporary failure; retry after a few seconds.
504 generation_timeout Retry, or narrow document_ids.

Code Examples:

retrieval_answer.py
import json, requests

headers = {"Authorization": "Bearer YOUR_API_KEY"}
url = "https://alapi.deep.sa/v1/alRetrieval/collections/COLLECTION_ID/answer"

answer = requests.post(url, json={"query": "كم مدة الإجازة السنوية؟"}, headers=headers).json()
print(answer["answer"])

# Streaming
with requests.post(url, json={"query": "وماذا بعد عشر سنوات؟", "stream": True,
                               "history": [{"role": "user", "content": "كم مدة الإجازة السنوية؟"},
                                           {"role": "assistant", "content": answer["answer"]}]},
                   headers=headers, stream=True) as res:
    event = None
    for line in res.iter_lines(decode_unicode=True):
        if line.startswith("event: "):
            event = line[7:]
        elif line.startswith("data: ") and event == "delta":
            print(json.loads(line[6:])["text"], end="", flush=True)

Errors

Errors use standard HTTP status codes and one JSON shape. Branch on error.code, which is stable; message is for people and may change. Some errors also carry document_ids, statuses or details.

{
  "error": {
    "code": "document_not_ready",
    "message": "Documents not ready: '3146366e04ff' is processing.",
    "request_id": "7f3c1a9d0b2e4c6f8a1b3d5e7f9a0b2c",
    "document_ids": ["3146366e04ff"],
    "statuses": { "3146366e04ff": "processing" }
  }
}
HTTP / codeDescription
400 invalid_json / missing_fileFix the request.
402 insufficient_balanceThis month's requests or OCR pages are used up; upgrade at /pricing.
404 collection_not_found / document_not_foundThe id does not exist in your account.
409 document_not_ready / no_ready_documents / nothing_to_retry / document_limit_reachedSee the endpoint.
413 / 415File too large or unsupported type.
422 invalid_requestSee details; includes unknown fields and out-of-range values.
502 / 504A temporary upstream failure or timeout; retry.

Reference

Limits, headers and error codes shared by every product.

Usage & Limits

Read-only endpoints to inspect your key, your usage and your balance. They are authenticated with your API key (any scope), keep working when your balance is used up, and do not count against your rate limit.

GET /v1/me

Returns the authenticating key's metadata and the owner's email.

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

{
  "key": {
    "name": "Production",
    "prefix": "sk-alapi-abc",
    "scopes": ["models", "alembedding"],
    "expires_at": null,
    "rate_limit_per_minute": 60,
    "daily_request_limit": null,
    "created_at": "2026-01-01T00:00:00Z",
    "last_used_at": "2026-01-02T12:00:00Z"
  },
  "email": "you@example.com"
}
GET /v1/me/usage

Returns per-day, per-key, per-model usage for a date range.

Query parameters: start and end (YYYY-MM-DD, default the last 30 days), granularity=day. The range must not exceed 90 days.

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

{
  "start": "2026-01-01",
  "end": "2026-01-31",
  "granularity": "day",
  "data": [
    { "date": "2026-01-01", "key_prefix": "sk-alapi-abc", "model": "allam-7b",
      "requests": 12, "tokens_prompt": 800, "tokens_completion": 1500, "failed": 0, "wallet_sar": 0 },
    { "date": "2026-01-01", "key_prefix": "sk-alapi-abc", "model": "google/gemini-3.8-flash",
      "requests": 2, "tokens_prompt": 3000, "tokens_completion": 1000, "failed": 0, "wallet_sar": 0.0162 }
  ]
}
GET /v1/me/balance

Returns what pays for your calls, in the order it is used: the requests you bought before plans (any model) and old alOCR pages, then your plan's usage, then your wallet. A team member sees the team's shared balance.

curl https://alapi.deep.sa/v1/me/balance -H "Authorization: Bearer sk-alapi-..."

{
  "legacy": {
    "requests": { "balance": 500, "used": 120, "remaining": 380 },
    "ocr_pages": { "balance": 300, "used": 50, "remaining": 250 }
  },
  "plan": {
    "name": "Starter", "period_end": "2026-11-13T09:00:00Z", "trial_ends_at": null,
    "auto_renews": true, "cancelled_at": null, "requests_at_once": 1, "requests_per_minute": 60,
    "usage": {
      "month": { "used_percent": 3, "resets_at": "2026-11-13T09:00:00Z" }
    }
  },
  "wallet": { "currency": "SAR", "balance": 42.5 },
  "team": null
}

Plan usage is shown as a percentage of the month's limit, with the time it resets. usage is null on a plan without a usage limit, and plan is null without a plan. wallet.balance is in SAR before VAT.

GET /v1/me/credits Deprecated

Use /v1/me/balance. This endpoint returns your old prepaid balance only and will be removed. Responses carry Deprecation and Link headers.

Returns your old prepaid request and OCR page balances.

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

{
  "requests": { "balance": 500, "used": 120, "remaining": 380 },
  "ocr_pages": { "balance": 300, "used": 50, "remaining": 250 },
  "team": null
}

The team object is null unless you are an active member of a team.

wallet_sar in /v1/me/usage is what the calls took from your wallet, in SAR.

Billing and automatic renewal

A plan is paid a month at a time. When you pay with “Save this card for automatic renewal” ticked, the card is saved once the payment is confirmed and the plan renews itself at the end of each month: we email you three days before with the amount and the last four digits of the card. If the card is declined we try again on each of the next two days, and the plan stays usable meanwhile; after the third decline the plan ends and calls are paid from your wallet. Without a saved card, renew by hand from the dashboard.

To change plan, choose “Switch at renewal” on the pricing page or the dashboard: the new plan starts when the current month ends and nothing is charged before then. Undo the change from the dashboard at any time before that date.

We never see or store your card number: the card is entered in our payment provider's form and we keep only its reference. To remove the card, open the dashboard's Payment method card and choose Remove card; automatic renewal turns off and the reference is deleted at the provider.

Rate limits & expiry

Keys may carry a per-minute and a daily request limit. When exceeded, requests return 429 with these headers:

HTTP/1.1 429 Too Many Requests
Retry-After: 42
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1893456000

{ "error": { "message": "Rate limit exceeded...", "type": "rate_limit_error" } }

A key past its expiry returns 401 with the api_key_expired code:

HTTP/1.1 401 Unauthorized

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

CSV export

Signed-in users can download their request logs as CSV from the Usage and Dashboard pages (GET /logs/export?start=&end=).

Error Handling

The API uses standard HTTP status codes to indicate success or failure of requests.

Error Response Format:

error.json
{
  "error": {
    "message": "Error description",
    "type": "invalid_request_error",
    "code": "invalid_api_key"
  }
}
400 Bad Request - Invalid request parameters
401 Unauthorized - Missing or invalid API key
403 Forbidden - API key lacks required permissions
404 Not Found - Resource doesn't exist
429 Too Many Requests - Rate limit exceeded
500 Internal Server Error - Server-side error
400 model_type_mismatch · A chat model was sent to embeddings, or an embedding model to chat.
400 tools_not_supported · The request has tools but the model does not support them.
404 model_not_found · The model name does not exist. The message suggests the closest name; see GET /v1/models.
410 model_retired · The model was retired and its name no longer works. The message names the replacement.
503 model_unavailable · The model is temporarily unavailable. Retry shortly.
402 insufficient_balance · Nothing left to pay for the call: your old balance is used up and you have no plan or wallet balance. "Your balance is used up. Subscribe to a plan or top up your wallet at /pricing."
402 usage_limit_exceeded · Your plan's monthly usage is used up and your wallet is empty. The message says when the usage resets, for example: "This month's usage is used up. It resets on 2026-11-13. Top up your wallet or upgrade your plan at /pricing to keep going."
429 concurrency_limit_exceeded · More requests at once than your plan allows: "Too many requests at once for your plan (limit N). Retry shortly." Retry after the Retry-After header.

Retired model names

When a model is retired, its old name keeps working until a sunset date and is served by its replacement. Those responses carry the headers below, so you can find and update the calls before the name answers 410 model_retired.

response headers
Deprecation: @1791277200
Sunset: Fri, 04 Dec 2026 21:00:00 GMT
Link: </models/qwen/qwen3.8-27b>; rel="successor-version"
X-Alapi-Warning: qwen/qwen3.6-27b is retired. Requests are served by qwen/qwen3.8-27b until 2026-12-05; update your model name.

Ready to Get Started?

Generate an API key from your dashboard and start building

Activate account?

Activate this account?