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://dev.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://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.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://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)

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://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)

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://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)

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://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

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://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

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://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

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://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.

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://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.

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://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

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://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

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://dev.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://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 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://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

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://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

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.