Developer API Reference

Everything you need to drive the QIYAS Compliance Mapper without using the UI. All backend features are exposed through same-origin proxy routes on qiyas.velents.ai: /api/db/* (PostgREST), /api/storage/* (Storage), and /api/analyze-file (AI analysis).

1. Base URL & auth

All endpoints are served from this app's own domain. The server injects the publishable (anon) key on your behalf — you do not send any auth headers. RLS still applies exactly as if you called the database directly.

BASE     = https://qiyas.velents.ai
HEADERS  = { "Content-Type": "application/json" }   // no apikey, no Authorization

Tables in this POC use permissive RLS, so any caller can read/write standards, clauses, projects, files, and mappings. For production use, tighten RLS on the backend.

2. Standards & clauses

Discover available standards and the clauses they contain.

GET https://qiyas.velents.ai/api/db/standards?select=*&order=code

Response:

[
  { "id": "uuid", "code": "5.11.1", "title_ar": "..." , "created_at": "..." }
]

List clauses for a standard, ordered by their canonical sort order:

GET https://qiyas.velents.ai/api/db/clauses?standard_id=eq.<standardId>&select=*&order=sort_order

Each clause has: id, number (e.g. 5.11.1.3), title_ar, text_ar, special_rule_key (nullable), sort_order.

3. Projects

A project ties an entity + cycle to one standard. Create one:

POST https://qiyas.velents.ai/api/db/projects
Headers: Content-Type: application/json, Prefer: return=representation
Body:
{
  "standard_code": "5.11.1",
  "entity_name":   "وكالة التحول الرقمي",   // optional, has a default
  "cycle":         "2026"                    // optional, has a default
}

List recent projects:

GET https://qiyas.velents.ai/api/db/projects?select=*&order=created_at.desc&limit=50

Fetch a single project:

GET https://qiyas.velents.ai/api/db/projects?id=eq.<projectId>&select=*

Delete a project (cascades to its files and mappings if FK is set):

DELETE https://qiyas.velents.ai/api/db/projects?id=eq.<projectId>

4. Files (evidence upload)

Uploading evidence is a two-step flow: upload the binary to the evidence storage bucket, then insert a row in files pointing at the stored path.

4.1 Upload to Storage

POST https://qiyas.velents.ai/api/storage/object/evidence/<projectId>/<uuid>-<filename>
Headers:
  Content-Type: <mime>          // e.g. application/pdf, image/png
Body: <binary file content>

The storage path you used is what you save in files.storage_path.

4.2 Create the files row

POST https://qiyas.velents.ai/api/db/files
Headers: Content-Type: application/json, Prefer: return=representation
Body:
{
  "project_id":   "uuid",
  "name":         "policy.pdf",
  "mime":         "application/pdf",
  "size":         123456,
  "storage_path": "<projectId>/<uuid>-policy.pdf",
  "status":       "uploaded"
}

4.3 status lifecycle

uploaded  -> initial state, ready to analyze
analyzing -> analyze-file (suggest mode) is running
suggested -> AI proposed a clause; awaiting confirmation
confirmed -> verdict mode ran; mapping row written
error     -> see files.error_message

Useful read:

GET https://qiyas.velents.ai/api/db/files?project_id=eq.<projectId>&select=*&order=created_at.desc

5. analyze-file

Single endpoint, two modes. No auth headers required.

POST https://qiyas.velents.ai/api/analyze-file

5.1 Mode: suggest

Ask the AI which clause the file most likely proves.

Request:
{ "mode": "suggest", "fileId": "<uuid>" }

Response 200:
{
  "suggestion": {
    "clause_number": "5.11.1.3",
    "confidence":    0.87,
    "reason_ar":     "...",
    "alternatives":  ["5.11.1.4", "5.11.2.1"]
  },
  "suggested_clause_id": "uuid | null"
}

Side effects on the files row:
  status                  = "analyzing" then "suggested"
  suggested_clause_id     = matched clause uuid (or null)
  suggestion_confidence   = number 0..1
  suggestion_alternatives = string[]

5.2 Mode: verdict

Score a file against a specific clause and write the mapping.

Request:
{ "mode": "verdict", "fileId": "<uuid>", "clauseId": "<uuid>" }

Response 200:
{
  "verdict": {
    "verdict":           "accepted" | "accepted_with_reservations" | "rejected",
    "acceptance_score":  0..100,
    "reason_ar":         "...",
    "quote_ar":          "..." | null,
    "recommendations": [
      {
        "severity":   "must_fix" | "nice_to_have",
        "priority":   1,                 // 1 = highest
        "action_ar":  "...",             // full sentence, ≥ 12 words
        "example_ar": "..."              // concrete snippet to paste in
      }
    ],
    "missing_artifacts": ["..."]
  }
}

Score banding

accepted                     -> acceptance_score >= 85
accepted_with_reservations   -> 40 <= acceptance_score <= 84
rejected                     -> acceptance_score < 40

Side effects

UPSERT public.mappings (onConflict: file_id, clause_id) with:
  project_id, clause_id, file_id,
  verdict, verdict_reason_ar, quote_ar,
  recommendations, missing_artifacts, acceptance_score
UPDATE public.files SET status = "confirmed" WHERE id = fileId

Special clause rules

When a clause has special_rule_key, the model is given an extra constraint:

sla_plus_three_screenshots
  Requires an approved SLA document + 3 ITSM screenshots
  (SLA definition, linked ticket, compliance report).
  Partial coverage -> accepted_with_reservations.

policy_plus_dissemination
  Requires an approved policy + evidence of dissemination
  to staff. Either missing -> accepted_with_reservations.

quality_total_wording
  Document must say "الجودة الشاملة" specifically.
  If it only says "الجودة" -> accepted_with_reservations
  with a recommendation to fix the wording.

Errors

{ "error": "<message>" }
HTTP 429 -> AI rate limit
HTTP 402 -> AI credit / billing issue
HTTP 500 -> anything else (see message)

6. Mappings (verdicts)

Each confirmed file-to-clause pair lives in mappings. Read all verdicts for a project:

GET https://qiyas.velents.ai/api/db/mappings
  ?project_id=eq.<projectId>
  &select=*,clauses(*),files(*)
  &order=created_at.desc

Manually override the AI verdict (e.g. a human reviewer overrides to accepted):

PATCH https://qiyas.velents.ai/api/db/mappings?id=eq.<mappingId>
{
  "verdict":          "accepted",
  "acceptance_score": 100,
  "manual_override":  true,
  "override_reason":  "Reviewed by compliance lead"
}

Delete a mapping (un-link a file from a clause):

DELETE https://qiyas.velents.ai/api/db/mappings?id=eq.<mappingId>

7. Change notifications

For headless integrations, poll the proxied REST endpoints to detect changes — no direct backend connection required:

// Poll every few seconds while a project is active
setInterval(async () => {
  const files = await fetch(
    `${BASE}/api/db/files?project_id=eq.${projectId}&select=*&order=created_at.desc`,
  ).then((r) => r.json());
  // diff against previous snapshot and react to status changes
}, 4000);

8. End-to-end recipe

One self-contained script — no SDK needed — that creates a project, uploads a PDF, runs both AI modes, and reads the resulting mapping. Everything targets https://qiyas.velents.ai.

import { readFile } from "node:fs/promises";
import { randomUUID } from "node:crypto";

const BASE = "https://qiyas.velents.ai";
const H = { "Content-Type": "application/json" };

// 1) Create a project
const project = await fetch(`${BASE}/api/db/projects`, {
  method: "POST",
  headers: { ...H, Prefer: "return=representation" },
  body: JSON.stringify({ standard_code: "5.11.1", entity_name: "Acme" }),
}).then((r) => r.json()).then((rows) => rows[0]);

// 2) Upload a PDF to the evidence bucket
const bytes = await readFile("./policy.pdf");
const path  = `${project.id}/${randomUUID()}-policy.pdf`;
await fetch(`${BASE}/api/storage/object/evidence/${path}`, {
  method:  "POST",
  headers: { "Content-Type": "application/pdf" },
  body:    bytes,
});

// 3) Register the file row
const file = await fetch(`${BASE}/api/db/files`, {
  method: "POST",
  headers: { ...H, Prefer: "return=representation" },
  body: JSON.stringify({
    project_id:   project.id,
    name:         "policy.pdf",
    mime:         "application/pdf",
    size:         bytes.byteLength,
    storage_path: path,
    status:       "uploaded",
  }),
}).then((r) => r.json()).then((rows) => rows[0]);

// 4) Ask the AI which clause this file proves
const suggest = await fetch(`${BASE}/api/analyze-file`, {
  method: "POST",
  headers: H,
  body: JSON.stringify({ mode: "suggest", fileId: file.id }),
}).then((r) => r.json());

// 5) Score the file against that clause
const verdict = await fetch(`${BASE}/api/analyze-file`, {
  method: "POST",
  headers: H,
  body: JSON.stringify({
    mode:     "verdict",
    fileId:   file.id,
    clauseId: suggest.suggested_clause_id,
  }),
}).then((r) => r.json());

console.log(verdict);
// { verdict: { verdict: "accepted_with_reservations", acceptance_score: 72, ... } }

// 6) Read the mapping that was written
const mappings = await fetch(
  `${BASE}/api/db/mappings?project_id=eq.${project.id}` +
    `&select=*,clauses(*),files(*)`,
).then((r) => r.json());

console.log(mappings);