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=codeResponse:
[
{ "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_orderEach 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=50Fetch 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_messageUseful read:
GET https://wpgncpfazaataldpznte.supabase.co/rest/v1/files?project_id=eq.<projectId>&select=*&order=created_at.desc5. 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-file5.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 < 40Side 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 = fileIdSpecial 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.descManually 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);