API Documentation
Complete reference for alAPI's LLM, OCR, Retrieval and Guardrail services
https://dev.alapi.deep.sa/v1
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:
Authorization: Bearer YOUR_API_KEY
API Key: Generate API keys from your Dashboard.
SDK Setup Example:
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
Endpoint:
https://dev.alapi.deep.sa/v1/chat/completions
Request Body:
{
"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):
{
"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:
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
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:
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
Endpoint:
https://dev.alapi.deep.sa/v1/embeddings
Request Body:
{
"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):
{
"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:
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
Endpoint:
https://dev.alapi.deep.sa/v1/models
Required Scope:
This endpoint requires an API key with the
models
Response (200 OK):
{
"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:
| Field | Description |
|---|---|
billing | How 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_in | saudi for models hosted in Saudi Arabia, global for the rest. |
context_length | The most tokens the model handles in one call, or null when unknown. |
supports_tools | true when the model accepts tools (function calling). |
deprecated | A 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.
{
"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:
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
Supports Arabic and English text extraction with automatic language detection
POSTUpload Document
Upload File for OCR
Upload a document and start OCR processing
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. |
Response (200 OK):
{
"token": "abc123xyz...",
"status": "queued",
"progress": 0,
"upload_progress": 100,
"queue_position": 1
}
Code Examples:
Send the file directly as multipart/form-data.
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
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):
{
"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:
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.
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 |
|---|---|
| 404 | Token unknown or not yours |
| 409 | Job is already done / still uploading |
| 422 | Legacy job without a retryable copy — re-upload |
| 429 | Hit retry_count cap (3 by default) |
| 502 | Upstream 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
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):
{
"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:
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
Endpoint:
https://dev.alapi.deep.sa/v1/ocr/thumbnails/{token}?start=0&limit=10
Response:
{
"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)
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):
{
"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):
{
"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:
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.
POST /v1/alRetrieval/collections
Parameters:
| Parameter | Type | Description |
|---|---|---|
name | string | optional — up to 200 characters. |
Response (201 Created):
{
"id": "6a51b4e5c0d24f0e9b1a7c3d2e8f4a10",
"name": "HR policies",
"document_count": 0,
"created_at": "2026-10-03T10:00:00Z"
}
Code Examples:
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.
GET /v1/alRetrieval/collections?limit=100&offset=0
Parameters:
| Parameter | Type | Description |
|---|---|---|
limit | integer | optional — 1 to 500, default 100. |
offset | integer | optional — 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}
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:
| Parameter | Type | Description |
|---|---|---|
file | file | PDF, DOCX, PPTX, TXT or MD, up to 100 MB. |
url | string | Instead of file: a public HTTPS link alAPI downloads the document from (optional filename). |
title | string | optional — defaults to the file name without its extension. |
pages | object[] | 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
}
Code Examples:
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.
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"
}
| status | Description |
|---|---|
processing | Processing is running; ingest.stage names the step. |
ready | Searchable. |
failed | Processing stopped; ingest.error says why. Call retry. |
| deep_tree.status | Description |
|---|---|
not_supported | Text documents: always searched page by page. |
waiting / queued / processing | The section tree is on its way. |
ready | Searches use whole sections. degraded: true means no usable table of contents was found. |
skipped / failed | No 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:
| Parameter | Type | Description |
|---|---|---|
limit | integer | optional — 1 to 500, default 500. |
offset | integer | optional — 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"
}
| field | Description |
|---|---|
source | pdf or text. |
page_count | Pages found; 0 until processing finishes. |
status / ingest / deep_tree / pipeline | As 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:
| Parameter | Type | Description |
|---|---|---|
start | integer | optional — first page, from 1. |
end | integer | optional — last page, inclusive. Default: the last page. |
Response (200 OK):
{ "document_id": "3146366e04ff", "pages": [ { "page_num": 1, "text": "..." } ] }
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
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:
| Parameter | Type | Description |
|---|---|---|
query | string | required — 1 to 4,000 characters, Arabic or English. |
document_ids | string[] | optional — search only these documents; omit to search every ready document. |
top_k | integer | optional — 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.
document_ids is not ready.
document_ids.
Code Examples:
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.
POST /v1/alRetrieval/collections/{collection_id}/answer
Parameters:
| Parameter | Type | Description |
|---|---|---|
query | string | required — 1 to 4,000 characters. |
document_ids | string[] | optional — as in retrieve. |
history | object[] | optional — earlier turns, oldest first: {"role": "user" | "assistant", "content"}. |
stream | boolean | optional — 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"}
document_ids.
Code Examples:
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": {
"code": "document_not_ready",
"message": "Documents not ready: '3146366e04ff' is processing.",
"request_id": "7f3c1a9d0b2e4c6f8a1b3d5e7f9a0b2c",
"document_ids": ["3146366e04ff"],
"statuses": { "3146366e04ff": "processing" }
}
}
| HTTP / code | Description |
|---|---|
400 invalid_json / missing_file | Fix the request. |
402 insufficient_balance | This month's requests or OCR pages are used up; upgrade at /pricing. |
404 collection_not_found / document_not_found | The id does not exist in your account. |
409 document_not_ready / no_ready_documents / nothing_to_retry / document_limit_reached | See the endpoint. |
413 / 415 | File too large or unsupported type. |
422 invalid_request | See details; includes unknown fields and out-of-range values. |
502 / 504 | A 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.
Endpoint:
POST /v1/alGuardrail/detect
Request Body:
{
"text": "جوالي 0551234567 وايميلي sara@example.sa",
"mode": "redact"
}
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
text | string | Yes | Arabic, English or mixed text, of any length. |
mode | string | No | detect (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. |
threshold | number | No | Drop spans whose confidence is below this value, from 0 to 1. |
custom_labels | array | No | Up 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:
| Name | Description |
|---|---|
spans | Each span has label, text, start and end (its character offsets in text) and confidence. |
output | Present in the redact and tokenize modes; the text after processing. |
tokens | Present in the tokenize mode; maps each <LABEL_N> token to its original value. |
latency_ms | The 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.
| 401 | The key is missing, invalid or expired. |
| 402 | The account has no requests left. |
| 403 | The key does not have the alguardrail scope, or the account is not active. |
| 413 | The request body is larger than 1 MiB. |
| 429 | Rate 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. |
| 502 | The 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.
/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"
}
/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 }
]
}
/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": {
"message": "Error description",
"type": "invalid_request_error",
"code": "invalid_api_key"
}
}
model_type_mismatch · A chat model was sent to embeddings, or an embedding model to chat.
tools_not_supported · The request has tools but the model does not support them.
model_not_found · The model name does not exist. The message suggests the closest name; see GET /v1/models.
model_retired · The model was retired and its name no longer works. The message names the replacement.
model_unavailable · The model is temporarily unavailable. Retry shortly.
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>."
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.
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.
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