Docs
Send a page. Get the text back.
The Kindi Labs API runs the same models as the playground. Send an image, wait for the job, read the result.
Base URLhttps://api.asnb.sa/v1
Quickstart
- 1
- 2
Create an API key
In your account, under API keys. Copy it when it's shown: it isn't shown again.
API keys - 3
Send a page
Post an image to /v1/jobs, then read the job until it's done.
Authentication
Send your key in the Authorization header on every request. Keep keys on your server: never put one in a web page or a mobile app. You can revoke a key at any time in your account.
Authorization: Bearer asnb_live_…Send a page
Post the image as multipart form data with the model to use. The API answers at once with a job in the queue.
POST/v1/jobs
| Field | Meaning |
|---|---|
model | asnb-ocr for printed Arabic, asnb-htr for handwriting. |
file | The page image: PNG, JPEG or WebP. |
curl https://api.asnb.sa/v1/jobs \
-H "Authorization: Bearer $KINDI_API_KEY" \
-F model=asnb-ocr \
-F [email protected]import os, time, requests
API = "https://api.asnb.sa/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['KINDI_API_KEY']}"}
# 1. Send the page
with open("page.jpg", "rb") as f:
job = requests.post(f"{API}/jobs", headers=HEADERS,
data={"model": "asnb-ocr"},
files={"file": ("page.jpg", f, "image/jpeg")}).json()["job"]
# 2. Wait for the result
while job["status"] in ("queued", "running"):
time.sleep(1.5)
job = requests.get(f"{API}/jobs/{job['id']}", headers=HEADERS).json()["job"]
if job["status"] == "succeeded":
print(job["result"]["text"])
else:
print("failed:", job["error"])import fs from "node:fs";
const API = "https://api.asnb.sa/v1";
const headers = { Authorization: `Bearer ${process.env.KINDI_API_KEY}` };
// 1. Send the page
const form = new FormData();
form.append("model", "asnb-ocr");
form.append("file", new Blob([await fs.promises.readFile("page.jpg")], { type: "image/jpeg" }), "page.jpg");
let { job } = await (await fetch(`${API}/jobs`, { method: "POST", headers, body: form })).json();
// 2. Wait for the result
while (job.status === "queued" || job.status === "running") {
await new Promise((r) => setTimeout(r, 1500));
({ job } = await (await fetch(`${API}/jobs/${job.id}`, { headers })).json());
}
console.log(job.status === "succeeded" ? job.result.text : job.error);Read the result
Read the job until its status is succeeded or failed. Waiting a second or two between reads is plenty.
GET/v1/jobs/{id}
curl https://api.asnb.sa/v1/jobs/job_8f2k1x9q3m7d \
-H "Authorization: Bearer $KINDI_API_KEY"An example response for one of the sample pages (a 1899 magazine page), shortened. Confidence values are for illustration.
{
"job": {
"id": "job_8f2k1x9q3m7d",
"model": "asnb-ocr",
"model_version": "v1.0",
"status": "succeeded",
"created_at": "2026-10-09T09:41:12Z",
"finished_at": "2026-10-09T09:41:15Z",
"file": {
"name": "muqtataf-1899.jpg",
"type": "image/jpeg",
"size": 204803
},
"error": null,
"result": {
"text": "فقلنا وهل تدّعي ان الآلة التي تشير اليها ترفع الماء من نفسها من غير قوة تساعدها على رفعه.\nفقال كلاً بل اننا نضع ماءً في جانب منها فتدور به وتغرف الماء من النيل وترفعه. فقلنا ومن اين\n…",
"pages": [
{
"index": 0,
"width": 940,
"height": 772,
"lines": [
{
"text": "فقلنا وهل تدّعي ان الآلة التي تشير اليها ترفع الماء من نفسها من غير قوة تساعدها على رفعه.",
"bbox": [8, 106, 920, 43],
"confidence": 0.98
},
{
"text": "فقال كلاً بل اننا نضع ماءً في جانب منها فتدور به وتغرف الماء من النيل وترفعه. فقلنا ومن اين",
"bbox": [8, 155, 920, 44],
"confidence": 0.96
}
]
}
]
}
}
}The job object
| Field | Meaning |
|---|---|
id | The job's ID. |
model, model_version | Which model read the page, and which version. |
status | queued, running, succeeded or failed. |
created_at, finished_at | When the job was created and finished (ISO 8601, UTC). |
file | The name, type and size you uploaded. |
error | When failed: a code and a message. |
result.text | All the text, one line per line, pages separated by a blank line. |
result.pages[].lines[] | Each line's text, its box [x, y, width, height] in the image's pixels, and a confidence from 0 to 1. |
Errors
Errors come back with an HTTP status and a JSON body. Use the code, not the message, in your own logic.
{ "error": { "code": "file_too_large", "message": "The file is over 10 MB." } }| HTTP | code | Meaning |
|---|---|---|
| 401 | unauthorized | The key is missing, wrong, or revoked. |
| 400 | invalid_request | A field is missing or the model is unknown. |
| 413 | file_too_large | The file is over the size limit. |
| 415 | unsupported_type | The file isn't PNG, JPEG or WebP. |
| 403 | quota_exceeded | This month's pages are used up. |
| 404 | not_found | No such job on your account. |
| 429 | rate_limited | Too many requests. Wait for Retry-After seconds. |
| 500 | server_error | Something failed on our side. Try again. |
Limits
Images up to 10 MB, in PNG, JPEG or WebP. Each image counts as one page. Your monthly pages and what you've used are shown in your account.
Models
model | Name | For |
|---|---|---|
asnb-ocr | Kindi OCR v1.0 | Printed Arabic: newspapers, books, documents. |
asnb-htr | Kindi HTR v0.12 | Handwritten Arabic in five scripts. In development. |