AlQariAlQari
API Reference · REST

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

Authentication

Bearer <API_KEY>
Get an 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.sa

API 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)

POST/services/integration/upload-ocr

Entry 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)

POST/services/integration/extract-fields

Extract 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)

POST/services/integration/human-review

Create 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-mcp

3. 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-mcp

ChatGPT 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

ToolWhat it doesBilling
search_documentsFind documents by the words, names, numbers or dates in themBilled
list_documentsList documents, filtered by file name, status or typeFree
get_document_textRead a document's stored OCR text, in chunksFree
upload_and_ocrUpload a local PDF or image and extract its textBilled
ask_documentAsk a question and get an answer based on the documentBilled
summarize_documentSummarize a document as a brief, bullet points, sections or a narrativeBilled
extract_fieldsExtract named fields, such as invoice number or total, as structured dataBilled
extract_tablesExtract tables as rows and columnsBilled
classify_documentIdentify the document type, optionally from your own list of labelsBilled
validate_documentCheck a document against rules written in plain languageBilled
list_workflowsList your company's workflowsFree
run_workflowRun an "Upload Document" workflow on local filesBilled
get_workflow_runCheck a workflow run's status, results and costFree

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

POST/services/upload-ocr

Upload 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

NameTypeRequiredDescription
skip_vlmbooleanNoSkip the advanced visual analysis step. Default true.
modefast | premiumNoProcessing tier.
languageauto | ar | enNoDocument language hint.
department_idstringNoOptional department association.
process_for_chatbooleanNoEnable 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

GET/services/ocr-output/:id/text

Retrieve 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

GET/services/ocr-output/:id/ocr

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

KeyRangeMeaning
results[].confidence0.0–1.0Per-region OCR recognition confidence
layout.blocks[].conf0.0–1.0Per-layout-block confidence (different key: conf)

Layout output

GET/services/ocr-output/:id/layout

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

EndpointReturns
GET /services/ocr-output/:id/markdownPlain text / Markdown
GET /services/ocr-output/:id/htmlHTML
GET /services/ocr-output/:id/blocksHTML (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

POST/services/ai-validate

Run 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

NameTypeRequiredDescription
document_idstringYesThe already-processed document.
rules_textstringYesNewline-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

POST/services/chat/:id

Ask 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

POST/workflows/ai-builder/sessions

Describe 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

NameTypeRequiredDescription
workflow_idstringNoAn 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.
View pricing
Accepted filesPDF, PNG, JPG, TIFF, Word (.docx), Excel (.xlsx), .txt, .md, .csv, .json
Files per messageUp to 10
Files per conversationUp to 20
Message lengthUp 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

POST/workflows/ai-builder/sessions/:sessionId/messages

Send 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

NameTypeRequiredDescription
messagestringNoWhat the workflow should do, or what to change.
filesfile[]NoA 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 300

Response

{
  "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 }
    ]
  }
}
400The message is too long or there are too many files.
402The company can't pay: not enough credits, an ended subscription, or an overdue invoice. Nothing was processed or charged.
403AI_BUILDER_PAID_PLAN_REQUIRED: the company is on the free trial.
404The conversation was not found.
409The workflow was activated meanwhile; move it back to Draft first.
503The assistant is temporarily unavailable; try again.

Get a conversation

GET/workflows/ai-builder/sessions/:sessionId

The 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

POST/workflows/:id/run-upload

Send 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 300

Response

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.

completedDone; the result is in output→ Use output.
failedEnded with an error (error, error_code). A failed step uses no credits.→ Fix the document or workflow, then resend.
pausedWaiting 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 / pendingThe 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 with Idempotency-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-upload returns the run id as id; /runs returns it as run_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-Key on 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's status.
  • 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 in output.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

200The run ended or paused. Check status (failed runs are 200 too).
400"No file uploaded"No file / files field was sent.
400empty_file, unsupported_file_typeEmpty file, wrong type, or content that doesn't match the extension.
400too_many_filesMore than 100 files in one request.
400"Cannot run workflow" + errors[]The workflow graph is invalid, or the subscription isn't active.
401Missing, wrong, revoked or expired API key, or a login token instead of an API key.
402OUT_OF_CREDITS, BILLING_NOT_CONFIGURED, PAYG_BILLING_RESTRICTEDBilling or credits problem.
404Workflow not found (wrong id, or another company's workflow).
409WORKFLOW_NOT_ACTIVEThe workflow is Draft or Inactive; activate it.
413file_too_largeA file is over 20 MB.

Async runs

POST/workflows/:id/runs

Send 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 each run_id.
  • Related files on (needed for Batch Document Processing): all files go into one run with one run_id. Each file's result is in output.documents[] (file name, document type, confidence, fields). The package summary is in output.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

JSON
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

GET/workflows/runs/:runId

Retrieve 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

pendingQueued, not started yet→ Keep polling.
runningBeing processed→ Keep polling.
completedDone; the result is in output→ Stop. Use output.
failedEnded with an error (error, error_code)→ Stop. Fix or resend.
pausedWaiting 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

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

MethodHTTP POST, JSON body
Timeout30 seconds
RedirectsDisabled
DestinationPublic HTTPS URL, validated before delivery
SignatureNone — there is no HMAC signing header; verify requests with your own controls
RetriesNo automatic retry — a failed delivery is not re-sent
SuccessConsidered 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_id from the body and confirm the result with GET /workflows/runs/{run_id}.
  • It's sent only when the run reaches an Output node set to webhook. A failed run 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 statusprocessing | completed | failed
Run statuspending | running | completed | failed | paused
Execution modetest | manual | api | webhook | s3 | azure_blob
Workflow statusdraft | 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

202Accepted. Poll the run(s).
400missing_fileNo file / files field was sent.
400empty_file, unsupported_file_typeEmpty file, wrong type, or content that doesn't match the extension.
400too_many_filesMore than 100 files in one request.
400"Cannot run workflow" + errors[]The workflow is invalid; fix it in the builder.
400durable_input_failedThe file couldn't be stored. Retry.
401Missing, wrong, revoked or expired API key.
402OUT_OF_CREDITS, BILLING_NOT_CONFIGURED, …Billing or credits problem.
404Workflow not found (wrong id, or another company's workflow).
409WORKFLOW_NOT_ACTIVEThe workflow is Inactive; activate it.
413file_too_largeA file is over the size limit.
422A request field failed validation.

GET /workflows/runs/{run_id}

200The run (check status).
401Bad API key.
404Run 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.