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://dev.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://dev.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://dev.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://dev.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://dev.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://dev.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://dev.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://dev.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"
}
}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://dev.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://dev.api.alqari.sa/services/ocr-output/doc_9xKpL3mN/text",
"markdown_url": "https://dev.api.alqari.sa/services/ocr-output/doc_9xKpL3mN/markdown",
"html_url": "https://dev.api.alqari.sa/services/ocr-output/doc_9xKpL3mN/html",
"blocks_url": "https://dev.api.alqari.sa/services/ocr-output/doc_9xKpL3mN/blocks"
},
"premium_outputs": {
"layout_url": "https://dev.api.alqari.sa/services/ocr-output/doc_9xKpL3mN/layout",
"extraction_url": "https://dev.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://dev.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://dev.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://dev.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://dev.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://dev.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
}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://dev.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://dev.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://dev.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://dev.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 |
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.