Developer API Reference

Everything you need to drive the QIYAS Compliance Mapper without using the UI. All backend features are exposed through Lovable Cloud (PostgREST + Storage + Realtime) plus a single edge function, analyze-file.

1. Auth & base URL

The backend is a managed Supabase project. All endpoints share the same base URL and require the publishable (anon) key on every request:

BASE       = https://wpgncpfazaataldpznte.supabase.co
HEADERS    = {
  apikey:        <SUPABASE_PUBLISHABLE_KEY>,
  Authorization: Bearer <SUPABASE_PUBLISHABLE_KEY>,
  Content-Type:  application/json
}

The publishable key is safe to ship in clients. Tables in this POC use permissive RLS, so the publishable key is enough for full CRUD against standards, clauses, projects, files, and mappings. For production use, tighten RLS and call analyze-file from a trusted backend.

2. Standards & clauses

Discover available standards and the clauses they contain.

GET https://wpgncpfazaataldpznte.supabase.co/rest/v1/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://wpgncpfazaataldpznte.supabase.co/rest/v1/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://wpgncpfazaataldpznte.supabase.co/rest/v1/projects
Headers: ...auth, 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://wpgncpfazaataldpznte.supabase.co/rest/v1/projects?select=*&order=created_at.desc&limit=50

Fetch a single project:

GET https://wpgncpfazaataldpznte.supabase.co/rest/v1/projects?id=eq.<projectId>&select=*

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

DELETE https://wpgncpfazaataldpznte.supabase.co/rest/v1/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://wpgncpfazaataldpznte.supabase.co/storage/v1/object/evidence/<projectId>/<uuid>-<filename>
Headers:
  apikey:        <SUPABASE_PUBLISHABLE_KEY>
  Authorization: Bearer <SUPABASE_PUBLISHABLE_KEY>
  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://wpgncpfazaataldpznte.supabase.co/rest/v1/files
Headers: ...auth, 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://wpgncpfazaataldpznte.supabase.co/rest/v1/files?project_id=eq.<projectId>&select=*&order=created_at.desc

5. analyze-file edge function

Single endpoint, two modes. Always call with the publishable key in both apikey and Authorization headers.

POST https://wpgncpfazaataldpznte.supabase.co/functions/v1/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://wpgncpfazaataldpznte.supabase.co/rest/v1/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://wpgncpfazaataldpznte.supabase.co/rest/v1/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://wpgncpfazaataldpznte.supabase.co/rest/v1/mappings?id=eq.<mappingId>

7. Realtime subscriptions

React to verdict updates from your own systems. Both files and mappingspublish Postgres changes filtered by project_id.

import { createClient } from "@supabase/supabase-js";
const supabase = createClient("https://wpgncpfazaataldpznte.supabase.co", "<PUBLISHABLE_KEY>");

supabase
  .channel("project:" + projectId)
  .on("postgres_changes",
    { event: "*", schema: "public", table: "files",
      filter: `project_id=eq.${projectId}` },
    (payload) => console.log("file changed", payload))
  .on("postgres_changes",
    { event: "*", schema: "public", table: "mappings",
      filter: `project_id=eq.${projectId}` },
    (payload) => console.log("mapping changed", payload))
  .subscribe();

8. End-to-end recipe

One self-contained script that creates a project, uploads a PDF, runs both AI modes, and reads the resulting mapping.

import { createClient } from "@supabase/supabase-js";
import { readFile } from "node:fs/promises";
import { randomUUID } from "node:crypto";

const supabase = createClient(
  "https://wpgncpfazaataldpznte.supabase.co",
  process.env.SUPABASE_PUBLISHABLE_KEY!,
);

// 1) Create a project
const { data: project } = await supabase
  .from("projects")
  .insert({ standard_code: "5.11.1", entity_name: "Acme" })
  .select()
  .single();

// 2) Upload a PDF to the evidence bucket
const bytes = await readFile("./policy.pdf");
const path  = `${project!.id}/${randomUUID()}-policy.pdf`;
await supabase.storage.from("evidence").upload(path, bytes, {
  contentType: "application/pdf",
});

// 3) Register the file row
const { data: file } = await supabase
  .from("files")
  .insert({
    project_id:   project!.id,
    name:         "policy.pdf",
    mime:         "application/pdf",
    size:         bytes.byteLength,
    storage_path: path,
    status:       "uploaded",
  })
  .select()
  .single();

// 4) Ask the AI which clause this file proves
const { data: suggest } = await supabase.functions.invoke("analyze-file", {
  body: { mode: "suggest", fileId: file!.id },
});

// 5) Score the file against that clause
const { data: verdict } = await supabase.functions.invoke("analyze-file", {
  body: {
    mode:     "verdict",
    fileId:   file!.id,
    clauseId: suggest.suggested_clause_id,
  },
});

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

// 6) Read the mapping that was written
const { data: mappings } = await supabase
  .from("mappings")
  .select("*, clauses(*), files(*)")
  .eq("project_id", project!.id);

console.log(mappings);