AlQari Document Intelligence API
AlQari exposes a REST API over HTTPS to upload documents, run OCR with structured text, coordinates and confidence, validate documents, ask questions, and automate workflows — for Arabic and English documents. Response formats vary by endpoint (JSON, plain text, or HTML). Requests authenticate with a JWT access token or a qari_ API key.
Base URL
https://api.alqari.saAuthentication
Bearer <API_KEY>Authentication
Create an API key (starts with qari_) from your dashboard, then send it in the Authorization header on every request. The full key is shown only once at creation. Never expose your key in client-side code or public repositories.
curl https://api.alqari.sa/documents \
-H "Authorization: Bearer qari_••••••••••"Treat your API key like a password. If it leaks, revoke it immediately from the dashboard and issue a new one.
Alternative: session token via sign-in
For end-user apps, sign in via POST /auth/login with email_or_phone and password to receive an access_token, used in the Authorization header the same way. Session tokens are short-lived — use a qari_ key for long-running server integrations.
curl -X POST https://api.alqari.sa/auth/login \
-H "Content-Type: application/json" \
-d '{ "email_or_phone": "you@company.sa", "password": "••••••••" }'A successful response returns: access_token, refresh_token, token_type, user_name, email, phone_number.
Base URL
All endpoints are relative to the base URL below. Always use HTTPS. Paths are currently unversioned (there is no /v1).
Production
https://api.alqari.saAPI Keys
Create an API key from your dashboard. Creating, listing, and revoking API keys requires an organization Admin. Keys start with qari_ followed by 40 hex characters, are shown in full only once at creation, then stored hashed — keep it safe. Keys have no scopes and no create-time expiration.
The create request body contains only a name. Management endpoints: POST /api-keys to create, GET /api-keys to list (metadata only, never the secret), and DELETE /api-keys/:id to revoke.
curl -X POST https://api.alqari.sa/api-keys \
-H "Authorization: Bearer <JWT>" \
-H "Content-Type: application/json" \
-d '{ "name": "Production server" }'Errors
The API uses conventional HTTP status codes. Domain errors carry a human-readable detail plus a stable machine-readable code — branch on code, not on message text. Validation errors are returned as 422.
// DomainError (fields may be optional depending on the error)
{
"detail": "Document not found",
"code": "DOCUMENT_NOT_FOUND",
"violations": null,
"context": null
}
// 422 Validation error
{
"detail": [
{ "loc": ["body", "file"], "msg": "field required", "type": "missing" }
]
}
// 500 Unexpected
{
"detail": "An unexpected error occurred. Please try again."
}See the Error Codes section for the list of stable machine-readable codes.
n8n / Odoo integrations
To build pipelines outside the Al-Qari canvas (n8n, Odoo, or any HTTP client), use /services/integration/* endpoints. Each call runs one workflow node (OCR, extract, validate, human review, …). Chain steps by passing document_id from the previous response.
Authentication
Same qari_ API key as workflow runs. Reviewers use the in-app Review Queue — no separate approval UI needed.
Request and response details for each endpoint are in the sections below.
Upload & OCR (integration)
/services/integration/upload-ocrEntry point for n8n/Odoo pipelines. Send multipart/form-data with file. output_format: plain-text (default) | json-layout | markdown.
Request
curl -X POST "https://api.alqari.sa/services/integration/upload-ocr?output_format=plain-text" \
-H "Authorization: Bearer qari_••••••••••" \
-F "file=@invoice.pdf" \
-F "language=auto"Response
{
"document_id": "uuid",
"filename": "invoice.pdf",
"output_format": "plain-text",
"text": "...",
"pages": 1,
"confidence": 0.97
}Extract fields (integration)
/services/integration/extract-fieldsExtract fields from a document after OCR. Pass document_id from upload. Optional state carries prior step output.
Request
curl -X POST https://api.alqari.sa/services/integration/extract-fields \
-H "Authorization: Bearer qari_••••••••••" \
-H "Content-Type: application/json" \
-d '{
"document_id": "uuid",
"fields": ["invoice_number", "total", "date"]
}'Response
{
"document_id": "uuid",
"output": {
"fields": { "invoice_number": "INV-001", "total": 124500 }
}
}Human review (integration)
/services/integration/human-reviewCreate a review task in the in-app Review Queue. Poll status via GET /workflows/reviews/{review_task_id}.
Request
curl -X POST https://api.alqari.sa/services/integration/human-review \
-H "Authorization: Bearer qari_••••••••••" \
-H "Content-Type: application/json" \
-d '{
"document_id": "uuid",
"assignee_email": "reviewer@company.com",
"instructions": "Verify vendor name",
"send_email": true
}'Response
{
"document_id": "uuid",
"output": {
"status": "awaiting_review",
"review_task_id": "uuid",
"assignee_email": "reviewer@company.com",
"review_reasons": ["..."],
"workflow_run_id": "uuid"
}
}MCP server for AI assistants
The official Model Context Protocol (MCP) server connects AlQari to Claude, ChatGPT, Codex, Cursor and other MCP-compatible assistants. Ask in plain language to OCR a file, search your documents, summarize a document, extract its fields or tables, classify or validate it, or run one of your workflows. The server runs on your computer and calls the AlQari API with your company's API key.
1. Create an API key
Sign in with an Admin account and open Settings → API Keys. Create a key, copy it and keep it somewhere safe: it starts with qari_ and is shown only once.
2. Install
Requires Python 3.10 or newer. This installs the alqari-mcp command; find its full path with where alqari-mcp on Windows or which alqari-mcp on macOS and Linux.
pip install alqari-mcp3. Connect your assistant
In Claude Desktop, open Settings → Developer → Edit Config and add the server to claude_desktop_config.json. Set command to the full path of alqari-mcp, then restart the app. Cursor uses the same block in ~/.cursor/mcp.json.
{
"mcpServers": {
"alqari": {
"command": "C:\\path\\to\\alqari-mcp.exe",
"env": {
"ALQARI_API_KEY": "qari_your_api_key"
}
}
}
}In Claude Code, and in the ChatGPT desktop app and Codex, add it with one command:
# Claude Code
claude mcp add --transport stdio alqari --env ALQARI_API_KEY=qari_your_api_key -- alqari-mcp
# ChatGPT desktop app and Codex
codex mcp add alqari --env ALQARI_API_KEY=qari_your_api_key -- alqari-mcpChatGPT and Claude on the web and on mobile can only connect to MCP servers hosted on the internet, so use a desktop app or a coding tool. OCR of long files and workflow runs can take minutes: raise your app's tool timeout (in Codex, tool_timeout_sec = 300), and ALQARI_TIMEOUT if needed (default 300 seconds).
Tools
| Tool | What it does | Billing |
|---|---|---|
| search_documents | Find documents by the words, names, numbers or dates in them | Billed |
| list_documents | List documents, filtered by file name, status or type | Free |
| get_document_text | Read a document's stored OCR text, in chunks | Free |
| upload_and_ocr | Upload a local PDF or image and extract its text | Billed |
| ask_document | Ask a question and get an answer based on the document | Billed |
| summarize_document | Summarize a document as a brief, bullet points, sections or a narrative | Billed |
| extract_fields | Extract named fields, such as invoice number or total, as structured data | Billed |
| extract_tables | Extract tables as rows and columns | Billed |
| classify_document | Identify the document type, optionally from your own list of labels | Billed |
| validate_document | Check a document against rules written in plain language | Billed |
| list_workflows | List your company's workflows | Free |
| run_workflow | Run an "Upload Document" workflow on local files | Billed |
| get_workflow_run | Check a workflow run's status, results and cost | Free |
Billed tools are charged to the company that owns the API key, at the same credit prices as the AlQari app and API. Each billed result includes credits_consumed and remaining_credits (workflow runs report a billing breakdown), and every charge appears in your usage history. A call is refused before any work starts if the company can't be billed (HTTP 402).
Need a workflow to run from your assistant? Build one with AI, then run it with run_workflow. Package page on PyPI
Upload & run OCR
/services/upload-ocrUpload a file and run OCR in one step. Send as multipart/form-data with a file field and do not set the Content-Type header manually. Usually answers HTTP 200 with the result. If OCR is not finished when the request returns (e.g. very large documents), it answers HTTP 202 with status processing — poll included_outputs.text_url until it returns 200 (404 means not ready yet).
Optional header: Idempotency-Key can be supplied for upload requests. A 202 answer has the same document_id and output URLs as below but no text yet. extraction_url is the structured OCR-region output (text, confidence, bounding boxes and page references) — it is not a generic custom-field extraction endpoint. Next steps: text_url → plain text, extraction_url → structured OCR regions, layout_url → layout analysis.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| skip_vlm | boolean | No | Skip the advanced visual analysis step. Default true. |
| mode | fast | premium | No | Processing tier. |
| language | auto | ar | en | No | Document language hint. |
| department_id | string | No | Optional department association. |
| process_for_chat | boolean | No | Enable later Document Q&A. Default false. |
Request
curl -X POST "https://api.alqari.sa/services/upload-ocr?mode=premium&language=ar&process_for_chat=true" \
-H "Authorization: Bearer qari_••••••••••" \
-F "file=@invoice_2024.pdf"Response
{
"document_id": "doc_9xKpL3mN",
"file_name": "invoice_2024.pdf",
"pages": 3,
"processing_time": 4.21,
"total_words": 512,
"document_language": "ar",
"text": "فاتورة ضريبية ...",
"credits_consumed": 3,
"remaining_credits": 4977,
"included_outputs": {
"text_url": "https://api.alqari.sa/services/ocr-output/doc_9xKpL3mN/text",
"markdown_url": "https://api.alqari.sa/services/ocr-output/doc_9xKpL3mN/markdown",
"html_url": "https://api.alqari.sa/services/ocr-output/doc_9xKpL3mN/html",
"blocks_url": "https://api.alqari.sa/services/ocr-output/doc_9xKpL3mN/blocks"
},
"premium_outputs": {
"layout_url": "https://api.alqari.sa/services/ocr-output/doc_9xKpL3mN/layout",
"extraction_url": "https://api.alqari.sa/services/ocr-output/doc_9xKpL3mN/ocr"
}
}OCR text output
/services/ocr-output/:id/textRetrieve the full extracted text as plain text. This is the canonical plain-text endpoint and returns text/plain.
Request
curl https://api.alqari.sa/services/ocr-output/doc_9xKpL3mN/text \
-H "Authorization: Bearer qari_••••••••••"Response
Content-Type: text/plain
فاتورة ضريبية
Invoice No: INV-0001
...OCR structured output
/services/ocr-output/:id/ocrRetrieve the structured OCR regions as JSON. Each element carries the text, its confidence, a 4-point bounding box, and the page number.
Request
curl https://api.alqari.sa/services/ocr-output/doc_9xKpL3mN/ocr \
-H "Authorization: Bearer qari_••••••••••"Response
{
"results": [
{
"text": "Invoice No: INV-0001",
"confidence": 0.984,
"bbox": [
[72.0, 118.0],
[268.0, 118.0],
[268.0, 138.0],
[72.0, 138.0]
],
"page": 1
}
],
"credits_consumed": 0,
"remaining_credits": 4980
}Bounding Boxes
OCR regions return results[].bbox as a 4-corner polygon in order: top-left, top-right, bottom-right, bottom-left. The origin is the top-left of the page.
// results[].bbox — 4-corner polygon
"bbox": [
[x1, y1],
[x2, y2],
[x3, y3],
[x4, y4]
]Coordinates are page-relative and use the native page units of that input — they can depend on the input type. Coordinates are not normalized.
Note: the OCR box (results[].bbox) is a 4-point polygon, while the layout box (layout.blocks[].bbox) is a rectangle [left, top, right, bottom]. The two shapes are different.
Confidence Scores
results[].confidence is OCR recognition confidence for the region, in the range 0.0–1.0. This is not a business-field confidence score. Layout block confidence is returned under a different key: layout.blocks[].conf.
| Key | Range | Meaning |
|---|---|---|
| results[].confidence | 0.0–1.0 | Per-region OCR recognition confidence |
| layout.blocks[].conf | 0.0–1.0 | Per-layout-block confidence (different key: conf) |
Layout output
/services/ocr-output/:id/layoutRetrieve the document layout analysis as JSON: blocks, tables, figures, and page sizes. The layout block box is a rectangle [left, top, right, bottom] — different from the OCR 4-point polygon.
Request
curl https://api.alqari.sa/services/ocr-output/doc_9xKpL3mN/layout \
-H "Authorization: Bearer qari_••••••••••"Response
{
"source": "invoice_2024.pdf",
"pages": 1,
"page_sizes": { "1": [826, 1280] },
"blocks": [
{
"id": 0,
"text": "Invoice No: INV-0001",
"conf": 0.984,
"bbox": [72, 118, 268, 138],
"block_type": "paragraph",
"page": 1,
"raw_label": "paragraph"
}
],
"tables": [],
"figures": [],
"credits_consumed": 0,
"remaining_credits": 4980
}Tables & Figures
The layout response may contain detected table structures and figures, as part of document layout analysis. This is not a generic or custom-schema table extraction API.
// layout.tables[] — table structures detected in layout output
{
"id": "table_1",
"page": 1,
"bbox": [72, 300, 760, 640],
"markdown": "| Item | Qty | Total |\n| --- | --- | --- |\n| ... | ... | ... |",
"rows": [ ["Item", "Qty", "Total"] ]
}
// layout.figures[]
{
"id": "figure_1",
"page": 1,
"bbox": [499, 30, 772, 278],
"caption": ""
}HTML / Markdown / Blocks Outputs
Beyond text and structured JSON, additional rendered formats derived from the same OCR job are available.
| Endpoint | Returns |
|---|---|
| GET /services/ocr-output/:id/markdown | Plain text / Markdown |
| GET /services/ocr-output/:id/html | HTML |
| GET /services/ocr-output/:id/blocks | HTML (interactive blocks viewer) — not JSON |
The blocks endpoint returns HTML for an interactive blocks viewer — not JSON. For structured JSON use the ocr or layout endpoints.
AI validation
/services/ai-validateRun validation rules against an already-processed document. Pass document_id and rules_text as query params. Results are not always perfect — have a human review critical outcomes.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| document_id | string | Yes | The already-processed document. |
| rules_text | string | Yes | Newline-numbered validation rules. |
Request
curl -X POST "https://api.alqari.sa/services/ai-validate?document_id=doc_9xKpL3mN&rules_text=1.%20Check%20required%20fields" \
-H "Authorization: Bearer qari_••••••••••"Response
{
"document_id": "doc_9xKpL3mN",
"overall_verdict": "FAIL",
"summary": "1 of 2 rules failed.",
"results": [
{
"rule_number": 1,
"rule_text": "Check required fields",
"verdict": "PASS",
"calculation": null,
"detail": "All required fields present.",
"evidence": "Invoice No: INV-0001",
"bounding_boxes": [
{ "page": 1, "bbox": [[72,118],[268,118],[268,138],[72,138]] }
]
},
{
"rule_number": 2,
"rule_text": "Validate dates",
"verdict": "FAIL",
"calculation": null,
"detail": "Issue date is after due date.",
"evidence": null,
"bounding_boxes": null
}
],
"credits_consumed": 1,
"remaining_credits": 4976
}Per-rule verdict values are PASS, FAIL, WARNING, or N/A. bounding_boxes may be empty (null or []), so do not assume every rule has coordinates. Coordinate elements are 4-point polygons like OCR.
There is no generic field-extraction or custom-schema endpoint in the public API. AI-validate checks rules; it is not structured field extraction.
Document Q&A
/services/chat/:idAsk a natural-language question about a document that has been processed for chat. Upload the document with process_for_chat=true to enable chat on it.
The public chat response returns an answer and token usage only. It does not include page citations, source arrays, or bounding-box citations.
Request
curl -X POST https://api.alqari.sa/services/chat/doc_9xKpL3mN \
-H "Authorization: Bearer qari_••••••••••" \
-H "Content-Type: application/json" \
-d '{ "message": "ما إجمالي الفاتورة؟" }'Response
{
"document_id": "doc_9xKpL3mN",
"question": "ما إجمالي الفاتورة؟",
"answer": "إجمالي الفاتورة هو SAR 124,500.00",
"tokens_used": { "input": 812, "output": 24 },
"credits_consumed": 1,
"remaining_credits": 4975
}Build workflows with AI
/workflows/ai-builder/sessionsDescribe what a workflow should do, and optionally attach a use-case document, sample documents or both, and the AI assistant builds it for you as a Draft. Keep chatting to change it until it's right. Start a conversation with this endpoint (pass workflow_id to work on an existing workflow), then send your messages to it.
Available on subscription and Pay-as-you-go plans. On the free trial, every AI builder endpoint answers 403 with code AI_BUILDER_PAID_PLAN_REQUIRED.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| workflow_id | string | No | An existing workflow to work on. Leave it out to build a new one. |
Request
curl -X POST https://api.alqari.sa/workflows/ai-builder/sessions \
-H "Authorization: Bearer qari_••••••••••" \
-H "Content-Type: application/json" \
-d '{}'Response
HTTP/1.1 201 Created
{
"id": "aib_3Fk9",
"workflow_id": null,
"workflow": null,
"messages": [],
"sources": [],
"created_at": "2026-10-17T09:00:00Z",
"updated_at": "2026-10-17T09:00:00Z"
}In the app
Open Workflows → New workflow → Build with AI, or click Edit with AI in the workflow builder. A preview of the workflow sits next to the chat; open it in the builder to review and activate it.
Pricing
- Attached PDFs and images are read with OCR and charged per page at the standard Document OCR price; Word and Excel files are charged in page units at the same price (at least one page per file). Text files (.txt, .md, .csv, .json) aren't charged for OCR.
- The assistant's answer to each message is charged for its AI usage, priced by the tokens used with a minimum per message. If the assistant can't produce an answer, that message's AI usage isn't charged.
- Starting and reading a conversation are free. The company must be able to pay before a message is processed; otherwise the API answers 402. Every charge appears in your usage history.
| Accepted files | PDF, PNG, JPG, TIFF, Word (.docx), Excel (.xlsx), .txt, .md, .csv, .json |
| Files per message | Up to 10 |
| Files per conversation | Up to 20 |
| Message length | Up to 8,000 characters |
The assistant saves what it builds as a Draft, never changes an Active workflow, and never fills in credentials such as storage keys or webhook secrets. A conversation belongs to the user who started it.
Send a message to the assistant
/workflows/ai-builder/sessions/:sessionId/messagesSend multipart/form-data with message, files, or both. The assistant answers, asks a question back, or builds the workflow. When it builds, the workflow is saved as a Draft and workflow_changed is true. A file that can't be read is listed in unreadable instead of failing the message, and isn't charged. Each message is billed: billing breaks down the charge (OCR of the files and AI usage) and reply.credits is its total. Set your HTTP timeout to a few minutes.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| message | string | No | What the workflow should do, or what to change. |
| files | file[] | No | A use-case or specification document, samples of the documents to process, or both. Repeat the field for each file. |
Request
curl -X POST https://api.alqari.sa/workflows/ai-builder/sessions/aib_3Fk9/messages \
-H "Authorization: Bearer qari_••••••••••" \
-F "message=Extract the invoice number, date, VAT number and total from each invoice and export them to Excel" \
-F "files=@invoice_sample.pdf" \
--max-time 300Response
{
"reply": {
"role": "assistant",
"content": "Your workflow is ready.",
"action": "build",
"workflow_version": 1,
"notes": [],
"attachments": [],
"unreadable": [],
"credits": 77
},
"workflow_changed": true,
"billing": {
"ocr_pages": 3, "ocr_credits": 60,
"llm_input_tokens": 20000, "llm_output_tokens": 3000, "ai_credits": 17,
"credits_consumed": 77, "remaining_credits": 4923
},
"session": {
"id": "aib_3Fk9",
"workflow_id": "wf_7dR2",
"workflow": {
"id": "wf_7dR2",
"name": "Invoice extraction",
"status": "draft",
"graph": { "nodes": [ … ], "edges": [ … ] }
},
"messages": [ … ],
"sources": [
{ "filename": "invoice_sample.pdf", "chars": 1840, "truncated": false }
]
}
}| 400 | The message is too long or there are too many files. |
| 402 | The company can't pay: not enough credits, an ended subscription, or an overdue invoice. Nothing was processed or charged. |
| 403 | AI_BUILDER_PAID_PLAN_REQUIRED: the company is on the free trial. |
| 404 | The conversation was not found. |
| 409 | The workflow was activated meanwhile; move it back to Draft first. |
| 503 | The assistant is temporarily unavailable; try again. |
Get a conversation
/workflows/ai-builder/sessions/:sessionIdThe messages so far, the names of the attached files, and the workflow the assistant is building, with its graph.
Request
curl https://api.alqari.sa/workflows/ai-builder/sessions/aib_3Fk9 \
-H "Authorization: Bearer qari_••••••••••"Run a workflow and wait for the result
/workflows/:id/run-uploadSend a document (multipart) and get the result in the same response, without polling. The request stays open while the workflow runs (OCR, extraction, AI steps), then answers 200 with the whole run: the same object GET /workflows/runs/:runId returns, including output, billing and node_runs. The run id is id here, not run_id. Only Active workflows run, and there is no Idempotency-Key. Set your HTTP timeout to at least 5 minutes. For several files, repeat file or send several files fields; the answer is { related_files, run_count, runs: [...] } with a full run per file.
run-upload needs a qari_ API key (a login token is refused with 401) and an Active workflow (Draft or Inactive answers 409 WORKFLOW_NOT_ACTIVE). Resending after a timeout runs and bills the document again; for safe retries use Async runs below.
Request
curl -X POST https://api.alqari.sa/workflows/wf_7dR2/run-upload \
-H "Authorization: Bearer qari_••••••••••" \
-F "file=@invoice_2024.pdf" \
--max-time 300Response
HTTP/1.1 200 OK
{
"id": "run_5Qm1",
"workflow_id": "wf_7dR2",
"status": "completed",
"execution_mode": "api",
"trigger_type": "upload_document",
"output": {
"filename": "invoice.pdf",
"fields": { "invoice_number": "INV-2041", "invoice_date": "2026-09-28", "total_amount": "1150.00" },
"ocr_text": "…",
"_workflow": {
"run_id": "run_5Qm1",
"workflow_id": "wf_7dR2",
"workflow_name": "Invoice extraction"
}
},
"error": null,
"error_code": null,
"created_at": "2026-10-04T08:48:09Z",
"started_at": "2026-10-04T08:48:10Z",
"finished_at": "2026-10-04T08:48:14Z",
"billing": {
"ocr_pages": 1, "ocr_credits": 1,
"llm_input_tokens": 2140, "llm_output_tokens": 180, "llm_credits": 1,
"credits_consumed": 2, "remaining_credits": 4998
},
"node_runs": [
{ "node_id": "upload-1", "node_type": "upload_document", "label": "Upload Document", "status": "completed", "output": { } },
{ "node_id": "ocr-1", "node_type": "ocr", "label": "OCR", "status": "completed", "output": { } },
{ "node_id": "extract-1", "node_type": "extract_fields", "label": "Extract Fields", "status": "completed", "output": { } },
{ "node_id": "output-1", "node_type": "output", "label": "Output", "status": "completed", "output": { } }
]
}Run status in the answer
A 200 means the request worked, not that the run succeeded. Always check status.
completed | Done; the result is in output→ Use output. |
failed | Ended with an error (error, error_code). A failed step uses no credits.→ Fix the document or workflow, then resend. |
paused | Waiting for a Human Review decision; the final result isn't ready yet→ Read it later with GET /workflows/runs/{id}, or get it by webhook. |
running / pending | The server stopped waiting before the run ended (running or pending)→ Read it later with GET /workflows/runs/{id}. |
Today the server waits until the run ends or pauses, so running doesn't come back yet. The wait may later be limited to 5 minutes; a run still going then comes back as running. Handle that case now.
Which one should I use?
- Get a run id (
POST …/runs) for live systems. It answers at once, can be retried safely withIdempotency-Key, runs Draft workflows, and handles long documents and many files without holding a connection open. - Wait for the result (
POST …/run-upload) when you want the result in the same response: scripts, one-off jobs, short documents. It needs an API key and an Active workflow. - Either way, a run that needs Human Review isn't finished until someone decides. Its final result is read later.
/run-uploadreturns the run id asid;/runsreturns it asrun_id.
Timeouts and retries
- Set your HTTP client's timeout to at least 5 minutes. A short document takes seconds; a long one can take minutes.
- There's no
Idempotency-Keyon this endpoint. If the request times out, the connection drops, or you get a 5xx answer, the run may still finish and be billed. Resending runs the document again and bills it again. - Before resending, look for the run in
GET /workflows/{workflow_id}/runs(newest first, same API key). If you need safe retries, use Get a run id instead.
Several documents and Batch
- Related files off (default): each file gets its own run, but they run one after another while you wait. The answer lists them in
runs[]; check each run'sstatus. - If one of several files is refused (for example
402 OUT_OF_CREDITS), the whole request answers with that error, even though earlier files already ran and were billed. Send one document per request, or use Get a run id for several. - Related files on (needed for Batch Document Processing): all files go into one run. Each file's result is in
output.documents[]; the package summary is inoutput.package. The run succeeds or fails as a whole.
Errors: wait for the result
Errors come back as JSON with a detail field: either a message string, or an object with message plus code or error_code. Some errors (402) put code next to detail. Read whichever is present.
POST /workflows/{workflow_id}/run-upload
| 200 | The run ended or paused. Check status (failed runs are 200 too). |
| 400 | "No file uploaded"No file / files field was sent. |
| 400 | empty_file, unsupported_file_typeEmpty file, wrong type, or content that doesn't match the extension. |
| 400 | too_many_filesMore than 100 files in one request. |
| 400 | "Cannot run workflow" + errors[]The workflow graph is invalid, or the subscription isn't active. |
| 401 | Missing, wrong, revoked or expired API key, or a login token instead of an API key. |
| 402 | OUT_OF_CREDITS, BILLING_NOT_CONFIGURED, PAYG_BILLING_RESTRICTEDBilling or credits problem. |
| 404 | Workflow not found (wrong id, or another company's workflow). |
| 409 | WORKFLOW_NOT_ACTIVEThe workflow is Draft or Inactive; activate it. |
| 413 | file_too_largeA file is over 20 MB. |
Async runs
/workflows/:id/runsSend now, collect later — recommended for integrations and high volume. Same files, rules and billing as run-upload, but it answers immediately with HTTP 202 and a run_id in status pending. Then poll GET /workflows/runs/:runId every ~30 s until status is completed or failed.
Each file starts its own run; several files return { related_files, run_count, runs: [{ run_id, ... }] }. If a request fails or times out, poll GET /workflows/runs/:runId before retrying so you do not start a duplicate run.
Request
curl -X POST https://api.alqari.sa/workflows/wf_7dR2/runs \
-H "Authorization: Bearer qari_••••••••••" \
-F "file=@invoice_2024.pdf"Response
HTTP/1.1 202 Accepted
{
"run_id": "run_5Qm1",
"workflow_id": "wf_7dR2",
"status": "pending",
"execution_mode": "api"
}Several documents and Batch
- Related files off (default): each file gets its own run. The answer lists them in
runs[]; poll eachrun_id. - Related files on (needed for Batch Document Processing): all files go into one run with one
run_id. Each file's result is inoutput.documents[](file name, document type, confidence, fields). The package summary is inoutput.package(types found, required types missing, unclear files). - A Batch run succeeds or fails as a whole: if one file can't be read, the run is
failed. - With many files, set the Output node's payload template to return only what you need. Otherwise the response includes every file's OCR data.
Several files
202 Accepted
{
"related_files": false,
"run_count": 2,
"runs": [
{ "run_id": "run_5Qm1", "status": "pending", … },
{ "run_id": "run_8Lp4", "status": "pending", … }
]
}By default each file gets its own run. If the workflow's Upload node has Related files on, all files go into one run and you get the single-run answer.
Run status
/workflows/runs/:runIdRetrieve the status and result of a workflow run — this is the endpoint to poll. Statuses: pending (queued, not started yet), running (being processed), completed (result in output), failed (see error and error_code), paused (waiting for a Human Review decision in AlQari — keep checking every few minutes; the run continues after the review). Stop polling on completed or failed. A run can stay pending or running for a while.
Request
curl https://api.alqari.sa/workflows/runs/run_5Qm1 \
-H "Authorization: Bearer qari_••••••••••"Response
{
"id": "run_5Qm1",
"workflow_id": "wf_7dR2",
"status": "completed",
"execution_mode": "api",
"output": { "...": "the workflow result" },
"error": null,
"error_code": null,
"started_at": "2026-10-04T09:12:03Z",
"finished_at": "2026-10-04T09:12:41Z",
"created_at": "2026-10-04T09:12:02Z",
"node_runs": [
{ "node_id": "ocr_1", "status": "completed", "output": {} }
]
}Run statuses
pending | Queued, not started yet→ Keep polling. |
running | Being processed→ Keep polling. |
completed | Done; the result is in output→ Stop. Use output. |
failed | Ended with an error (error, error_code)→ Stop. Fix or resend. |
paused | Waiting for a person to approve it in AlQari's Review screen→ Poll slowly (every few minutes). It continues after the decision, then ends completed or failed. |
A run can stay pending for a while.
Response examples
Completed
200 OK
{
"id": "run_5Qm1",
"workflow_id": "wf_7dR2",
"status": "completed",
"execution_mode": "api",
"trigger_type": "upload_document",
"output": {
"filename": "invoice.pdf",
"fields": {
"invoice_number": "INV-2041",
"invoice_date": "2026-09-28",
"total_amount": "1150.00"
},
"ocr_text": "…",
"_workflow": {
"run_id": "run_5Qm1",
"workflow_id": "wf_7dR2",
"workflow_name": "Invoice extraction"
}
},
"error": null,
"error_code": null,
"created_at": "2026-10-04T08:48:09Z",
"started_at": "2026-10-04T08:48:10Z",
"finished_at": "2026-10-04T08:48:14Z",
"billing": {
"ocr_pages": 1, "ocr_credits": 1,
"llm_input_tokens": 2140, "llm_output_tokens": 180, "llm_credits": 1,
"credits_consumed": 2, "remaining_credits": 4998
},
"node_runs": [
{ "node_id": "upload-1", "node_type": "upload_document", "status": "completed", "output": { … } },
{ "node_id": "output-1", "node_type": "output", "status": "completed", "output": { … } }
]
}The result is in output. Its keys depend on the workflow's nodes and the Output node's payload template. _workflow is included unless Include metadata is off on the Output node.
Still going: the same shape with "status": "pending" or "running" and "output": null.
Failed: "status": "failed", with a readable message in error and a machine code in error_code. A failed step uses no credits.
Exported PDF/DOCX/Excel: if the Output node exports files, output.exported_files[] lists them. Download each with GET {BASE}{download_path} and the same API key.
Webhooks
Add a Webhook Output node inside a workflow to deliver run results to your own HTTPS endpoint via an HTTP POST with a JSON payload.
| Method | HTTP POST, JSON body |
| Timeout | 30 seconds |
| Redirects | Disabled |
| Destination | Public HTTPS URL, validated before delivery |
| Signature | None — there is no HMAC signing header; verify requests with your own controls |
| Retries | No automatic retry — a failed delivery is not re-sent |
| Success | Considered delivered when the response status is < 400 |
POST https://your-app.com/webhooks/alqari
Content-Type: application/json
{
"workflow_id": "wf_7dR2",
"run_id": "run_5Qm1",
"status": "completed"
}Design your receiver to be idempotent and validate incoming requests with your own controls.
Get the result by webhook instead
Instead of polling, set the workflow's Output node to Output type: webhook and enter your URL. When a run reaches the Output node, AlQari POSTs the output JSON to that URL (30-second timeout, redirects not followed).
- Treat the webhook as a notification: read
_workflow.run_idfrom the body and confirm the result withGET /workflows/runs/{run_id}. - It's sent only when the run reaches an Output node set to webhook. A
failedrun sends nothing. After a Human Review decision, it's sent only if the path taken after the decision leads to that Output node. Keep a slow poll as a fallback.
File Support
Accepted upload file types are governed by an allowlist: .pdf .png .jpg .jpeg .tif .tiff .bmp .webp .docx .doc .txt .md .csv .json .xml .html. Not all types behave identically for OCR.
| Upload file-size limit (upload-ocr) | 20 MB |
| Oversized file (upload-ocr) | FILE_TOO_LARGE / HTTP 413 |
| Workflow trigger file-count (separate from upload-ocr) | Default max 20 files, hard max 100 files |
The 20 MB limit is the upload-ocr file size. The 20/100 file limits are workflow-trigger file counts — not an upload-ocr batch limit.
Workflow run limits
- File types: PDF, PNG, JPG/JPEG, TIFF, WEBP, DOCX, XLSX
- Up to 20 MB per file
- Up to 100 files per request (the Upload node may set a lower limit)
Status Enums
Document processing status is persisted as one of the values below. Keep the enum families (document, run, execution mode, workflow) separate.
| Document status | processing | completed | failed |
| Run status | pending | running | completed | failed | paused |
| Execution mode | test | manual | api | webhook | s3 | azure_blob |
| Workflow status | draft | active | inactive | archived |
Idempotency
An Idempotency-Key can be supplied for upload requests: POST /services/upload-ocr and POST /services/ai-validate. Its scope is billing de-duplication only — a technical retry with the same key is billed once, while a new request with a different or absent key is billed normally. It is not an OCR/validation response re-send. Use a new key for a new upload.
curl -X POST https://api.alqari.sa/services/upload-ocr \
-H "Authorization: Bearer qari_••••••••••" \
-H "Idempotency-Key: 9f2b7c4a-••••" \
-F "file=@invoice_2024.pdf"Billing Fields
Processing responses return credits_consumed and remaining_credits to reflect credit usage. For current pricing, see the pricing page.
Error Codes
Branch your logic on the code field, not the message text. The stable codes are below:
| DOCUMENT_NOT_FOUND |
| OCR_NOT_READY |
| CHAT_NOT_READY |
| CHAT_SERVICE_UNAVAILABLE |
| PREMIUM_OCR_UNAVAILABLE |
| UNSUPPORTED_FILE_TYPE |
| CORRUPTED_FILE |
| FILE_TOO_LARGE |
| WORKFLOW_NOT_ACTIVE |
| AI_BUILDER_PAID_PLAN_REQUIRED |
AlQari does not currently publish an application-level rate-limit as a guaranteed public contract, and the app does not emit guaranteed X-RateLimit-* or Retry-After headers. Excessive request volumes may be limited internally.
Workflow run errors
Errors come back as JSON with a detail field: either a message string, or an object with message plus code or error_code. Some errors (402) put code next to detail. Read whichever is present.
POST /workflows/{workflow_id}/runs
| 202 | Accepted. Poll the run(s). |
| 400 | missing_fileNo file / files field was sent. |
| 400 | empty_file, unsupported_file_typeEmpty file, wrong type, or content that doesn't match the extension. |
| 400 | too_many_filesMore than 100 files in one request. |
| 400 | "Cannot run workflow" + errors[]The workflow is invalid; fix it in the builder. |
| 400 | durable_input_failedThe file couldn't be stored. Retry. |
| 401 | Missing, wrong, revoked or expired API key. |
| 402 | OUT_OF_CREDITS, BILLING_NOT_CONFIGURED, …Billing or credits problem. |
| 404 | Workflow not found (wrong id, or another company's workflow). |
| 409 | WORKFLOW_NOT_ACTIVEThe workflow is Inactive; activate it. |
| 413 | file_too_largeA file is over the size limit. |
| 422 | A request field failed validation. |
GET /workflows/runs/{run_id}
| 200 | The run (check status). |
| 401 | Bad API key. |
| 404 | Run not found (wrong id, or another company's run). |
Versioning
Paths are currently unversioned: there is no /v1/ or /v2/ prefix, no version header, and no versioned host. This may change in the future.