API Documentation

Complete reference for alAPI's LLM, OCR, Retrieval and Guardrail services

v1 https://dev.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://dev.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://dev.alapi.deep.sa/v1"
)

Models

OpenAI-compatible API for chat completions and embeddings. Use your favorite models through a unified 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://dev.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://dev.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://dev.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)

POSTEmbeddings

Create Embeddings

Creates an embedding vector representing the input text

POST

Endpoint:

endpoint
https://dev.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. Can be a string or array of strings
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)

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://dev.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)}")

GETAvailable Models

List Models

Returns the list of currently available models

GET

Endpoint:

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

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

Response (200 OK):

response.json
{
  "object": "list",
  "data": [
    {
      "id": "qwen/qwen3.8-27b",
      "object": "model",
      "created": 1767225600,
      "owned_by": "qwen",
      "type": "llm",
      "billing": { "mode": "request" },
      "hosted_in": "saudi",
      "context_length": 131072,
      "supports_tools": true
    },
    {
      "id": "google/gemini-3.8-flash",
      "object": "model",
      "created": 1791277200,
      "owned_by": "google",
      "type": "llm",
      "billing": { "mode": "credits", "credits_per_message": 8, "tokens_per_credit": { "input": 500, "output": 100 } },
      "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
billingHow calls are charged. mode is request (one request per call) or credits, with credits_per_message for a typical message and tokens_per_credit for input and output.
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",
  "billing": { "mode": "request" },
  "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://dev.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 20 credits / message
llm ~11014ms
deep-sa/alEmbedding 1 request
embedding ~290ms
google/gemini-3.1-pro 23 credits / message
llm -
google/gemini-3.8-flash 8 credits / message
llm -
gpt-oss-120b 1 request
llm ~6671ms
gpt-oss-20b 1 request
llm ~568ms
llama-3.3-70b 1 request
llm ~731ms
minimaxai/minimax-m2.7 1 request
llm ~2024ms
openai/gpt-6-luna 1 credit / message
llm -
qwen/qwen3.8-27b 1 request
llm ~135ms
z-ai/glm-5.3 12 credits / message
llm -

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

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://dev.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)

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

Name Type Description
url string Public HTTPS URL of a PDF 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://dev.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://dev.alapi.deep.sa/v1/ocr/jobs/{token}

Status Values:

Status Description
queued
Job is waiting in the processing queue
processing
Document is being processed
done
Processing complete. Results available in the pages array
error
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...",
      "num_segments": 12,
      "elapsed": 2.45
    }
  ],
  "expires_at": "2026-02-05T18:30:00Z"
}

Code Examples:

get_status.sh
curl "https://dev.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://dev.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://dev.alapi.deep.sa/v1/ocr/jobs/YOUR_TOKEN_HERE/retry" \
  -H "Authorization: Bearer YOUR_API_KEY"

GETGet Single Page

Get Page by Number

Retrieve OCR text and thumbnail for a single page

GET

Endpoint:

endpoint
https://dev.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...",
  "thumbnail": {
    "url": "https://objectstorage.me-jeddah-1.oraclecloud.com/..."
  },
  "image": {
    "url": "https://...presigned-s3-url..."
  },
  "status": "done",
  "num_segments": 12,
  "elapsed": 3.45
}

The image field is null if the image is not yet available (job in progress or images not uploaded).

Code Examples:

get_page.py
import requests

API_BASE = "https://dev.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 Job has expired

GETThumbnails

Get Page Thumbnails

Retrieve page thumbnails with pagination

GET

Endpoint:

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

Response:

response.json
{
  "thumbnails": [
    {
      "page_num": 1,
      "data": "data:image/png;base64,iVBORw0KGgo..."
    }
  ],
  "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://dev.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
}

When S3 is not configured, images are returned as inline base64 data URIs instead of presigned URLs.

Base64 Fallback (no S3):

fallback.json
{
  "images": [
    {
      "page_num": 1,
      "data": "data:image/png;base64,..."
    }
  ],
  // ...pagination fields same as above
}

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://dev.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.

endpoint
POST /v1/alRetrieval/collections

Parameters:

ParameterTypeDescription
namestringoptional — up to 200 characters.

Response (201 Created):

response.json
{
  "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://dev.alapi.deep.sa/v1/alRetrieval/collections",
                           json={"name": "HR policies"}, headers=headers).json()
print(collection["id"])

GETList collections

Your collections, newest first.

endpoint
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):

response.json
{
  "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.

endpoint
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.

endpoint
DELETE /v1/alRetrieval/collections/{collection_id}

This cannot be undone.

Response (200 OK):

response.json
{ "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.

endpoint
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):

response.json
{
  "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://dev.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.

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

Response (200 OK):

response.json
{
  "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.

endpoint
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):

response.json
{
  "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.

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

Response (200 OK):

response.json
{
  "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.

endpoint
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):

response.json
{ "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.

endpoint
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.

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

This cannot be undone.

Response (200 OK):

response.json
{ "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.

endpoint
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):

response.json
{
  "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://dev.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.

endpoint
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):

response.json
{
  "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.

events
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://dev.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.json
{
  "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.

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://dev.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.

Reference

Limits, headers and error codes shared by every product.

Usage & Limits

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

GET /v1/me

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

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

{
  "key": {
    "name": "Production",
    "prefix": "sk-alapi-abc",
    "scopes": ["models"],
    "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://dev.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, "credits": 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, "credits": 16.0 }
  ]
}
GET /v1/me/credits

Returns your plan, your credits (plan and bought), and your request and OCR balances. A team member on a team with a plan sees the team's shared pool.

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

{
  "plan": { "name": "Pro", "period_end": "2026-11-06T09:00:00Z", "auto_renews": false, "cancelled_at": null, "requests_at_once": 6, "requests_per_minute": 360 },
  "credits": { "plan": 149984.0, "bought": 0, "total": 149984.0, "period_end": "2026-11-06T09:00:00Z" },
  "requests": { "balance": 0, "used": 0, "remaining": 0, "month_used": 1520, "month_limit": null },
  "ocr_pages": { "balance": 0, "used": 0, "remaining": 0 },
  "team": null,
  "global_credits": { "used_this_month": 16.0 }
}

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

credits in /v1/me/usage and global_credits in /v1/me/credits count the credits used on global models. An account without a plan returns "plan": null and its prepaid request balance.

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 account moves to the Free plan. 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_credits · A global model was called with no credits left. Without a plan: "This model uses credits. Subscribe to a plan at /pricing." On a plan: "This model uses credits; you have 0 left this month. Buy credits or wait for your allowance to reset on <date>."
402 request_limit_exceeded · No requests left. On the Free plan: "You have used this month's 100 requests. Upgrade at /pricing or wait until <date>." With a prepaid balance from before the plans, the message says the request balance is used up and points to /pricing.
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?