امتثال
واجهة البرمجة

Jobs

POST
/v1/jobs

Creates a new compliance job for the given project + document set, starts the ComplianceJobWorkflow on Temporal, and returns the job_id immediately. The job runs asynchronously; poll GET /jobs/{job_id} or stream GET /jobs/{job_id}/events for progress.

Authorization

BearerAuth
headerAuthorizationBearer <token>

JWT access token. Obtain via POST /v1/auth/login.

Header Parameters

accept-language?string|null

Request Body

application/json
  1. body

Body for POST /v1/jobs — create a new compliance check job.

Examples: { "project_id": "550e8400-e29b-41d4-a716-446655440000", "document_ids": ["6ba7b810-9dad-11d1-80b4-00c04fd430c8"], "query": "مشروع مكتب 5 طوابق في الرياض", "query_scope": { "building_type": "office", "stories": 5, "occupancies": ["occ:B"] }, "region": "RUH", "codes": ["801"] }

codes*array<string>

Multi-code platform input (Phase 5 / Phase 7 contract): the list of design codes to evaluate this job against. Each entry is a code identifier matching compliance.code_editions.code (e.g. '801' for SBC 801 Fire Code, '201' for SBC 201 General Building). At least one code is required. The caller must be entitled to every code in this list (platform codes are visible to all subscribers; tenant BYO codes are visible only to the owning tenant — see Phase 4). Verdicts in the response will be tagged with code_id + code_edition_id so the workbench can group them by code.

Items1 <= items
confirm_classification?Confirm Classification

Reserved. Recorded with the job but not acted on: the workflow never pauses for confirmation, whatever the value. The building's classification is decided by the platform (founder ruling 2026-09-24: no manual review step).

Defaulttrue
document_ids*array<>

One or more UUIDs of projects.project_documents rows to include in this job. Each document must belong to the given project_id. At least one document is required.

Items1 <= items
plot_key?|

Billing project key — the survey decision number (رقم القرار المساحي) for this plot. Identifies the project for the project-as-unit charge: a re-upload to the same plot attaches to the existing (already-charged) project and reruns are free. When omitted the server falls back to 'project:{project_id}'. NOT read from the drawing — it comes from the project settings the user enters.

project_id*Project Id

UUID of the projects.projects row this job belongs to.

Formatuuid
query*Query

Free-text description of the project scope in Arabic or English. Used by ScopeClassificationActivity to determine occupancy class and building type. Example: 'مشروع مكتب 5 طوابق في الرياض، استخدام تجاري مختلط'.

Length10 <= length <= 2000
query_scope?|

Structured scope for the job. REQUIRED key: occupancies, a non-empty list of SBC 201 occupancy ids such as 'occ:A-1', 'R-2' or 'B', each a division or a group without divisions (B, E, M, U); a bare group with divisions such as 'A' or 'R' is refused with the divisions to choose from. The declared occupancy is the building's occupancy for rule selection (founder ruling D6a: no analysis runs on a guessed one). Optional keys: building_type (str), stories (int), total_area_sqm (float), height_m (float). NOTE: height_m and total_area_sqm are not hints only — they are carried into evaluation as USER_STATED facts on the building (building.height_m, building.gross_floor_area_sqm), so rules such as the SBC high-rise threshold return a real PASS/FAIL instead of NOT_APPLICABLE. A value that is absent or not a positive number is ignored, and those rules abstain as before. NOTE: For region, prefer the explicit region field below — it's machine-checkable and feeds directly into the wind-load + temperature rule applicability filters.

region?|

Optional KSA region code where the building is located. When set, scope_classification trusts this value and skips region inference from drawings. Wind-load + temperature/humidity rules in SBC are region-specific so explicit input reduces classification risk. Accepted: RUH (Riyadh), JED (Jeddah), DMM (Dammam), MEC (Makkah), MED (Madinah), TUU (Tabuk), AHB (Abha/Asir). Other ISO codes accepted but trigger a warning.

Response Body

Job created.

application/json
  1. response

Response for GET /v1/jobs/{job_id} and POST /v1/jobs.

The response shape is the same for both endpoints. On POST, status='pending' and all verdict/element fields are null. On GET after completion, the full verdict + element sets are populated.

Examples: # POST /jobs response (pending) { "job_id": "550e8400-e29b-41d4-a716-446655440000", "status": "pending", "estimated_cost_credits": 12.5, "eta_seconds": 90 }

# GET /jobs/{id} response (completed){  "job_id": "550e8400-e29b-41d4-a716-446655440000",  "status": "completed",  "verdict_summary": {"total": 34, "pass_count": 28, ...},  "verdicts": [...],  "elements": [...]}
document_coverage?array<>

Per-document result for the files in this job: whether each was READ, and when it was not, why and what would fix it. A document missing from this list was not assessed, which is different from being read.

elements?array<>|

Visual elements extracted from the drawings. Null until job status='completed'.

error_code?|

Machine-readable error code. Only present when status='failed'.

error_message_ar?|

Arabic error message. Only present when status='failed'.

error_message_en?|

English error message. Only present when status='failed'.

estimated_cost_credits?|

Estimated credit cost for this job. Set at job creation based on document count and rule set size. Null until scope classification completes.

eta_seconds?|

Estimated seconds until completion. Null for completed/failed/cancelled jobs.

job_id*Job Id

UUID of the compliance job.

Formatuuid
result_pdf_url?|

S3 presigned URL to download the verdict PDF. Valid for 15 minutes after generation. Null until job status='completed'. Use GET /v1/jobs/{job_id}/result.pdf to get a fresh URL.

status*JobStatus

Current lifecycle status of the job.

Value in"pending""running""completed""failed""cancelled""expired"
verdict_summary?|

Aggregate verdict counts. Null until job status='completed'.

verdicts?array<>|

Per-rule compliance verdicts. Null until job status='completed'. Each entry is the latest-version verdict per rule_id.

verdicts_inline_count?|

Number of verdict rows in the inline verdicts slice. Present only when verdicts_truncated is True.

verdicts_truncated?Verdicts Truncated

True when the inline verdicts list is a bounded slice of the full result set. Use verdicts_url to retrieve all verdicts via pagination.

Defaultfalse
verdicts_url?|

URL to the paginated verdict list for this job (GET /v1/jobs/{job_id}/verdicts). Present when status='completed'. Use this endpoint rather than the inline verdicts field for large jobs.

curl -X POST 'https://api.aisc.sa/v1/jobs' \  -H 'Content-Type: application/json' \  -d '{  "codes": [    "801"  ],  "document_ids": [    "6ba7b810-9dad-11d1-80b4-00c04fd430c8"  ],  "project_id": "550e8400-e29b-41d4-a716-446655440000",  "query": "مشروع مكتب 5 طوابق في الرياض، استخدام تجاري مختلط"}'
{  "document_coverage": [    {      "document_id": "b792e8ae-2cb4-4209-85b9-32be4c2fcdd6",      "filename": "A-101-GROUND-FLOOR.dwg",      "layer_convention": "iso_13567",      "matched_count": 0,      "onboarding_pairs": [        {          "element_code": "G25",          "label": "Wall"        }      ],      "reason": "string",      "total_entities": 0,      "unread": false    }  ],  "elements": [    {      "attributes": {        "area_sqm": 45.2,        "width_mm": 4500      },      "bbox": {        "height": 200,        "width": 80,        "x": 120.5,        "y": 340.2      },      "document_id": "b792e8ae-2cb4-4209-85b9-32be4c2fcdd6",      "element_id": "98026c2a-b90c-4843-8adc-9992613765c9",      "element_type": "room",      "label_ar": "ممر هروب",      "label_en": "Egress corridor",      "page": 3    }  ],  "error_code": "DOCUMENT_PARSE_FAILED",  "error_message_ar": "فشل تحليل ملف PDF: تنسيق غير مدعوم",  "error_message_en": "Document parse failed: unsupported format",  "estimated_cost_credits": 12.5,  "eta_seconds": 90,  "job_id": "550e8400-e29b-41d4-a716-446655440000",  "result_pdf_url": "https://s3.eu-central-1.amazonaws.com/emtithal-...?X-Amz-Expires=900",  "status": "pending",  "verdict_summary": {    "conformity_form": {      "blocking": [        "COVERAGE_SHORT",        "NEEDS_REVIEW"      ],      "emittable": true,      "rules_examined": 2886,      "rules_in_force": 2886    },    "fail_count": 4,    "needs_review_count": 2,    "not_applicable_count": 0,    "not_evaluable_count": 0,    "overridden_count": 0,    "pass_count": 28,    "rules_examined": 34,    "rules_in_force": 7687,    "rules_withheld": 0,    "total": 34  },  "verdicts": [    {      "citations": [        {}      ],      "cited_drawing_element_id": "1aea6d40-086c-4933-ba23-c70ad6166893",      "cited_rule_quote": "لا يجب أن يقل عرض الممر عن ١١٢٠ ملم",      "code_edition_id": "4a5f38c5-02cb-48ac-ba22-00d46bdf8402",      "code_id": "801",      "confidence": 0.92,      "element_id": "98026c2a-b90c-4843-8adc-9992613765c9",      "justification_ar": "عرض الممر 1100 ملم أقل من الحد الأدنى المطلوب 1120 ملم",      "justification_en": "Corridor width 1100mm is below the required minimum of 1120mm",      "rule_id": "SBC301-1005.1",      "verdict": "FAIL",      "verdict_id": "a8098c1a-f86e-11da-bd1a-00112444be1e",      "verdict_raw": "FAIL",      "version": 1    }  ],  "verdicts_inline_count": 0,  "verdicts_truncated": false,  "verdicts_url": "/v1/jobs/550e8400-e29b-41d4-a716-446655440000/verdicts"}

Get compliance job state

GET
/v1/jobs/{job_id}

Returns the current state of a compliance job. When status='completed', the response includes all verdicts and drawing elements.

Authorization

BearerAuth
headerAuthorizationBearer <token>

JWT access token. Obtain via POST /v1/auth/login.

Path Parameters

job_id*Job Id
Formatuuid

Header Parameters

accept-language?string|null

Response Body

Job found.

application/json
  1. response

Response for GET /v1/jobs/{job_id} and POST /v1/jobs.

The response shape is the same for both endpoints. On POST, status='pending' and all verdict/element fields are null. On GET after completion, the full verdict + element sets are populated.

Examples: # POST /jobs response (pending) { "job_id": "550e8400-e29b-41d4-a716-446655440000", "status": "pending", "estimated_cost_credits": 12.5, "eta_seconds": 90 }

# GET /jobs/{id} response (completed){  "job_id": "550e8400-e29b-41d4-a716-446655440000",  "status": "completed",  "verdict_summary": {"total": 34, "pass_count": 28, ...},  "verdicts": [...],  "elements": [...]}
document_coverage?array<>

Per-document result for the files in this job: whether each was READ, and when it was not, why and what would fix it. A document missing from this list was not assessed, which is different from being read.

elements?array<>|

Visual elements extracted from the drawings. Null until job status='completed'.

error_code?|

Machine-readable error code. Only present when status='failed'.

error_message_ar?|

Arabic error message. Only present when status='failed'.

error_message_en?|

English error message. Only present when status='failed'.

estimated_cost_credits?|

Estimated credit cost for this job. Set at job creation based on document count and rule set size. Null until scope classification completes.

eta_seconds?|

Estimated seconds until completion. Null for completed/failed/cancelled jobs.

job_id*Job Id

UUID of the compliance job.

Formatuuid
result_pdf_url?|

S3 presigned URL to download the verdict PDF. Valid for 15 minutes after generation. Null until job status='completed'. Use GET /v1/jobs/{job_id}/result.pdf to get a fresh URL.

status*JobStatus

Current lifecycle status of the job.

Value in"pending""running""completed""failed""cancelled""expired"
verdict_summary?|

Aggregate verdict counts. Null until job status='completed'.

verdicts?array<>|

Per-rule compliance verdicts. Null until job status='completed'. Each entry is the latest-version verdict per rule_id.

verdicts_inline_count?|

Number of verdict rows in the inline verdicts slice. Present only when verdicts_truncated is True.

verdicts_truncated?Verdicts Truncated

True when the inline verdicts list is a bounded slice of the full result set. Use verdicts_url to retrieve all verdicts via pagination.

Defaultfalse
verdicts_url?|

URL to the paginated verdict list for this job (GET /v1/jobs/{job_id}/verdicts). Present when status='completed'. Use this endpoint rather than the inline verdicts field for large jobs.

curl -X GET 'https://api.aisc.sa/v1/jobs/497f6eca-6276-4993-bfeb-53cbbbba6f08'
{  "document_coverage": [    {      "document_id": "b792e8ae-2cb4-4209-85b9-32be4c2fcdd6",      "filename": "A-101-GROUND-FLOOR.dwg",      "layer_convention": "iso_13567",      "matched_count": 0,      "onboarding_pairs": [        {          "element_code": "G25",          "label": "Wall"        }      ],      "reason": "string",      "total_entities": 0,      "unread": false    }  ],  "elements": [    {      "attributes": {        "area_sqm": 45.2,        "width_mm": 4500      },      "bbox": {        "height": 200,        "width": 80,        "x": 120.5,        "y": 340.2      },      "document_id": "b792e8ae-2cb4-4209-85b9-32be4c2fcdd6",      "element_id": "98026c2a-b90c-4843-8adc-9992613765c9",      "element_type": "room",      "label_ar": "ممر هروب",      "label_en": "Egress corridor",      "page": 3    }  ],  "error_code": "DOCUMENT_PARSE_FAILED",  "error_message_ar": "فشل تحليل ملف PDF: تنسيق غير مدعوم",  "error_message_en": "Document parse failed: unsupported format",  "estimated_cost_credits": 12.5,  "eta_seconds": 90,  "job_id": "550e8400-e29b-41d4-a716-446655440000",  "result_pdf_url": "https://s3.eu-central-1.amazonaws.com/emtithal-...?X-Amz-Expires=900",  "status": "pending",  "verdict_summary": {    "conformity_form": {      "blocking": [        "COVERAGE_SHORT",        "NEEDS_REVIEW"      ],      "emittable": true,      "rules_examined": 2886,      "rules_in_force": 2886    },    "fail_count": 4,    "needs_review_count": 2,    "not_applicable_count": 0,    "not_evaluable_count": 0,    "overridden_count": 0,    "pass_count": 28,    "rules_examined": 34,    "rules_in_force": 7687,    "rules_withheld": 0,    "total": 34  },  "verdicts": [    {      "citations": [        {}      ],      "cited_drawing_element_id": "1aea6d40-086c-4933-ba23-c70ad6166893",      "cited_rule_quote": "لا يجب أن يقل عرض الممر عن ١١٢٠ ملم",      "code_edition_id": "4a5f38c5-02cb-48ac-ba22-00d46bdf8402",      "code_id": "801",      "confidence": 0.92,      "element_id": "98026c2a-b90c-4843-8adc-9992613765c9",      "justification_ar": "عرض الممر 1100 ملم أقل من الحد الأدنى المطلوب 1120 ملم",      "justification_en": "Corridor width 1100mm is below the required minimum of 1120mm",      "rule_id": "SBC301-1005.1",      "verdict": "FAIL",      "verdict_id": "a8098c1a-f86e-11da-bd1a-00112444be1e",      "verdict_raw": "FAIL",      "version": 1    }  ],  "verdicts_inline_count": 0,  "verdicts_truncated": false,  "verdicts_url": "/v1/jobs/550e8400-e29b-41d4-a716-446655440000/verdicts"}
POST
/v1/jobs/{job_id}/cancel

Sends a cancellation signal to the Temporal ComplianceJobWorkflow. Licensed_engineer-only.

Authorization

BearerAuth
headerAuthorizationBearer <token>

JWT access token. Obtain via POST /v1/auth/login.

Path Parameters

job_id*Job Id
Formatuuid

Header Parameters

accept-language?string|null

Response Body

Cancellation signal sent.

response?unknownapplication/json
curl -X POST 'https://api.aisc.sa/v1/jobs/497f6eca-6276-4993-bfeb-53cbbbba6f08/cancel'
null
POST
/v1/jobs/{job_id}/documents/{document_id}/codes

Answers the question GET /jobs/{job_id} already asks. When a drawing is unread, document_coverage[].onboarding_pairs lists the (element_code, label) pairs that would make it readable; this records one such answer for THIS TENANT ONLY.

Authorization

BearerAuth
headerAuthorizationBearer <token>

JWT access token. Obtain via POST /v1/auth/login.

Path Parameters

job_id*Job Id
Formatuuid
document_id*Document Id
Formatuuid

Request Body

application/json
  1. body

Body for POST /v1/jobs/{job_id}/documents/{document_id}/codes.

The REPLY to a question we already asked. DocumentCoverage reports an unread drawing and lists onboarding_pairs -- the (element_code, label) pairs that would make it readable -- and until now nothing could accept an answer, so the remedy we printed had no button (RFC-011).

The convention is NOT in this body. It is read from the document's own read-status row, which is what the reader recorded when it failed: a customer retyping 'iso_13567' could silently file the meaning under a convention no drawing uses, where it would never be found again.

element_code*Element Code

The layer code being explained, VERBATIM as we reported it: 'G25'. Must be one of the onboarding_pairs we listed for this document.

Lengthlength <= 40
element_type*ElementType

What that code means in this firm's drawings. Constrained to the vocabulary the rules are written against -- unlike the free-text corrected_element_type on an override, which reports a class we may be MISSING. Here a value outside the vocabulary cannot help: it would retype every matching layer in every future job to something no rule can read, and SAFE-1 could not detect it, because a wrong reading is exactly as corroborated as a right one.

Value in"room""door""window""exit""stair""corridor""ramp""shaft""elevator""mezzanine""fire_compartment""occupancy_zone""egress_path""wall""column""beam""slab""foundation""reinforcement""steel_connection""fire_alarm""smoke_detector""fire_extinguisher""sprinkler""standpipe""fire_pump""exit_sign""smoke_vent""emergency_light""travel_distance""common_path""fire_zone""circuit""earth_electrode""conductor""distribution_panel""socket_outlet""luminaire""conduit""transformer""air_handling_unit""chiller""fan""damper""diffuser""boiler""water_heater""trap""plumbing_fixture""backflow_preventer""floor_drain""cleanout""gas_appliance""gas_regulator""gas_meter""shutoff_valve""vent_terminal""footing""anchor""brace""pile""retaining_wall""expansion_joint""pool""diving_board""pool_drain""pool_barrier""insulation""glazing_unit""energy_meter""solar_panel""duct""pipe""equipment""building""floor""site""other""occupancy_area""unknown"
label?Label

The free-text tail we reported alongside the code ('Wall'). Part of the key, because one code covers several types: G25 is Wall AND Door AND Window depending on it.

Lengthlength <= 120
Default""

Response Body

Meaning recorded. Applies to the tenant's FUTURE jobs.

response?unknownapplication/json
curl -X POST 'https://api.aisc.sa/v1/jobs/497f6eca-6276-4993-bfeb-53cbbbba6f08/documents/497f6eca-6276-4993-bfeb-53cbbbba6f08/codes' \  -H 'Content-Type: application/json' \  -d '{  "element_code": "G25",  "element_type": "wall"}'
null
GET
/v1/jobs/{job_id}/documents/{document_id}/pages/{page}/image

ADR-0025 D5 / PRD-007 finding card: returns a short-TTL presigned URL for the page image the ingest pipeline produced — PNG raster for PDF/legacy documents, or the emt-e6s server-side SVG render for DXF/DWG documents (probed in that order; whichever exists is presigned). IFC uploads still have no page image (no render path built for them) and return 404 PREVIEW.PAGE_IMAGE_NOT_FOUND, same as a page out of range or a withheld/failed render.

Authorization

BearerAuth
headerAuthorizationBearer <token>

JWT access token. Obtain via POST /v1/auth/login.

Path Parameters

job_id*Job Id
Formatuuid
document_id*Document Id
Formatuuid
page*Page

1-based page number.

Range1 <= value <= 10000

Response Body

Presigned page-image URL.

application/json
  1. response

A short-TTL URL for one rasterized document page.

expires_in_seconds*Expires In Seconds

URL validity window.

page*Page

1-based page number.

url*Url

Presigned GET URL for the page PNG (short TTL).

curl -X GET 'https://api.aisc.sa/v1/jobs/497f6eca-6276-4993-bfeb-53cbbbba6f08/documents/497f6eca-6276-4993-bfeb-53cbbbba6f08/pages/1/image'
{  "expires_in_seconds": 300,  "page": 3,  "url": "string"}
GET
/v1/jobs/{job_id}/events

Server-Sent Events stream of compliance job progress. Reconnect with Last-Event-ID to resume from the last received sequence.

Authorization

BearerAuth
headerAuthorizationBearer <token>

JWT access token. Obtain via POST /v1/auth/login.

Path Parameters

job_id*Job Id
Formatuuid

Header Parameters

last-event-id?string|null
accept-language?string|null

Response Body

SSE stream of job progress events.

response?unknown
curl -X GET 'https://api.aisc.sa/v1/jobs/497f6eca-6276-4993-bfeb-53cbbbba6f08/events'
null
GET
/v1/jobs/{job_id}/issues.bcf

Generates a BCF 3.0 (BIM Collaboration Format) zip archive from the job's FAIL verdicts and every verdict that needs an engineer (NEEDS_REVIEW, CONFLICT, NOT_APPLICABLE_NO_ELEMENT_FOUND), read as presented verdicts. Each qualifying verdict becomes a BCF topic with title, status, rule snippet, and an element viewpoint bounding box where available. The archive is empty (but valid) when the job has none. Read-only — queries verdicts, writes nothing.

Authorization

BearerAuth
headerAuthorizationBearer <token>

JWT access token. Obtain via POST /v1/auth/login.

Path Parameters

job_id*Job Id
Formatuuid

Header Parameters

accept-language?string|null

Response Body

BCF 3.0 zip archive.

response?unknown
curl -X GET 'https://api.aisc.sa/v1/jobs/497f6eca-6276-4993-bfeb-53cbbbba6f08/issues.bcf'
null
POST
/v1/jobs/{job_id}/override

Licensed engineer overrides a compliance verdict. Persisted to projects.job_overrides AND signals the Temporal workflow.

Authorization

BearerAuth
headerAuthorizationBearer <token>

JWT access token. Obtain via POST /v1/auth/login.

Path Parameters

job_id*Job Id
Formatuuid

Header Parameters

accept-language?string|null

Request Body

application/json
  1. body

Body for POST /v1/jobs/{job_id}/override — reviewer verdict override.

Override is a first-class audit event. Justification is required; there are no rubber-stamp overrides. The licensed engineer must provide the reason in the regulatory language (Arabic preferred; English accepted).

Examples: { "rule_id": "SBC301-1005.1", "original_verdict": "FAIL", "new_verdict": "PASS", "justification": "المسافة المقيسة على الرسم صحيحة؛ الخطأ في التعرف البصري للحدود" }

corrected_element_type?|

OPTIONAL. What the reviewer says the element actually is, when disputed_element_id is supplied. Free text on purpose: a value outside the current vocabulary is evidence that the vocabulary is missing a class, and rejecting it would discard the report that reveals the gap.

disputed_element_id?|

OPTIONAL. The drawing element the reviewer says was read wrongly. Supply it only when the override disputes our READING of the drawing rather than the rule: the justification example above is exactly that case, and today it is recoverable only by reading the Arabic back with a model. Omit it for a rule disagreement; a value invented to fill the field is a fabricated correction.

justification*Justification

Required free-text justification for the override. Arabic is preferred (regulatory language). Minimum 20 characters. This text is preserved in projects.job_overrides for 7-year audit retention.

Length20 <= length <= 2000
new_verdict*VerdictValue

The reviewer's replacement verdict. OVERRIDDEN is set automatically by the system; supply PASS, FAIL, or NEEDS_REVIEW.

Value in"PASS""FAIL""NEEDS_REVIEW""OVERRIDDEN""NOT_APPLICABLE"
original_verdict*VerdictValue

The reviewer's replacement verdict. OVERRIDDEN is set automatically by the system; supply PASS, FAIL, or NEEDS_REVIEW.

Value in"PASS""FAIL""NEEDS_REVIEW""OVERRIDDEN""NOT_APPLICABLE"
rule_id*Rule Id

Rule identifier whose verdict is being overridden. Must match a rule_id in compliance.rules. Accepts live corpus IDs such as 'SBC-801-303.7-b77eaf76' (slash-separated taxonomy paths) and future edition-qualified IDs of the form '{code}:{edition}:{section}'.

Match^[A-Za-z0-9._/:\-]{1,120}$

Response Body

Override recorded and workflow signalled.

response?unknownapplication/json
curl -X POST 'https://api.aisc.sa/v1/jobs/497f6eca-6276-4993-bfeb-53cbbbba6f08/override' \  -H 'Content-Type: application/json' \  -d '{  "justification": "المسافة المقيسة على الرسم صحيحة؛ الخطأ في التعرف البصري للحدود",  "new_verdict": "PASS",  "original_verdict": "FAIL",  "rule_id": "SBC-801-303.7-b77eaf76"}'
null
GET
/v1/jobs/{job_id}/result.pdf

Returns an S3 presigned URL for the compliance verdict PDF. Valid for 15 minutes.

Authorization

BearerAuth
headerAuthorizationBearer <token>

JWT access token. Obtain via POST /v1/auth/login.

Path Parameters

job_id*Job Id
Formatuuid

Header Parameters

accept-language?string|null

Response Body

Presigned URL + expiry.

response?unknownapplication/json
curl -X GET 'https://api.aisc.sa/v1/jobs/497f6eca-6276-4993-bfeb-53cbbbba6f08/result.pdf'
null
GET
/v1/jobs/{job_id}/rules/{rule_id}/verdicts

AUD-048: returns ALL element-grained verdict rows for a single rule_id at the current (MAX) version — one row per cited_drawing_element_id. No rollup is applied here; both the FAIL row for door-A and the PASS row for door-B are returned so the workbench can deep-link each element to its position on the canvas. Paginated.

Authorization

BearerAuth
headerAuthorizationBearer <token>

JWT access token. Obtain via POST /v1/auth/login.

Path Parameters

job_id*Job Id
Formatuuid
rule_id*Rule Id

Query Parameters

verdict?|

Filter by presented verdict. Must be an exact PresentedVerdict (PASS / FAIL / NEEDS_REVIEW / NOT_APPLICABLE / NOT_APPLICABLE_NO_ELEMENT_FOUND / NOT_APPLICABLE_SCOPE_UNCONFIRMED / CONFLICT). Invalid values return 422.

limit?Limit

Page size (1-500).

Range1 <= value <= 500
Default200
offset?Offset

Zero-based offset.

Range0 <= value
Default0

Header Parameters

accept-language?string|null

Response Body

Per-element verdicts for the rule.

response?unknownapplication/json
curl -X GET 'https://api.aisc.sa/v1/jobs/497f6eca-6276-4993-bfeb-53cbbbba6f08/rules/string/verdicts'
null
GET
/v1/jobs/{job_id}/verdicts

Returns the compliance verdicts for a job as a paginated flat list. AUD-048: verdicts are worst-of-rolled-up per rule_id — a FAIL on any element surfaces as FAIL at rule grain, never masked by a PASS row. AUD-053: results are paginated (limit/offset); total_count is returned so the frontend can render a page indicator. Each verdict carries the multi-code platform fields (code_id, code_edition_id, element_id, citations) per docs/api/verdict-shape.md.

Authorization

BearerAuth
headerAuthorizationBearer <token>

JWT access token. Obtain via POST /v1/auth/login.

Path Parameters

job_id*Job Id
Formatuuid

Query Parameters

code_id?|

Filter by design-code identifier (e.g. '801', '201').

element_type?|

Filter by drawing element type. Must be an exact ElementType value (lowercase, e.g. 'door', 'corridor', 'room'). Invalid values return 422.

verdict_state?|

Filter by presented verdict. Must be an exact PresentedVerdict (PASS / FAIL / NEEDS_REVIEW / NOT_APPLICABLE / NOT_APPLICABLE_NO_ELEMENT_FOUND / NOT_APPLICABLE_SCOPE_UNCONFIRMED / CONFLICT). Invalid values return 422.

limit?Limit

Maximum number of verdicts to return per page (1-500). Default: 200.

Range1 <= value <= 500
Default200
offset?Offset

Zero-based offset for pagination.

Range0 <= value
Default0

Header Parameters

accept-language?string|null

Response Body

List of verdicts.

application/json
  1. response

AUD-061: typed response for GET /jobs/{job_id}/verdicts.

verdicts is a list of raw DB rows (each is a dict keyed per docs/api/verdict-shape.md). Using list[Any] avoids coupling the route layer to a DB-shape Pydantic model that would need to track every column rename — the CTO can generate types from the committed openapi.json schema.

filters*

AUD-061: filters applied to the verdict list query.

job_id*Job Id
Formatuuid
total_count*Total Count
verdicts*array<unknown>
curl -X GET 'https://api.aisc.sa/v1/jobs/497f6eca-6276-4993-bfeb-53cbbbba6f08/verdicts'
{  "filters": {    "code_id": "string",    "element_type": "string",    "limit": 0,    "offset": 0,    "verdict_state": "string"  },  "job_id": "453bd7d7-5355-4d6d-a38e-d9e7eb218c3f",  "total_count": 0,  "verdicts": [    null  ]}
GET
/v1/jobs/{job_id}/verdicts/{verdict_id}/citation

ADR-0025 D5 / PRD-007 Evidence Overlay: resolve one verdict to the drawing evidence the frontend needs for lineage click-through — the cited element's sheet_id, page and bbox plus the rule citation text. element is null for scope-level verdicts that cite no drawing element.

Authorization

BearerAuth
headerAuthorizationBearer <token>

JWT access token. Obtain via POST /v1/auth/login.

Path Parameters

job_id*Job Id
Formatuuid
verdict_id*Verdict Id
Formatuuid

Response Body

Resolved citation with element evidence (or element: null).

application/json
  1. response

Everything the workstation needs to render a verdict's evidence lineage.

cited_rule_quote?|

Exact rule text quoted by the evaluator.

element?|

The cited drawing element with sheet/page/bbox; null for scope-level verdicts that cite no specific element.

job_id*Job Id

Compliance job UUID.

Formatuuid
justification_ar?|

Arabic justification.

justification_en?|

English justification.

rule_id*Rule Id

Cited rule id.

verdict*Verdict

The presented verdict (spec B REQ-B-07): PASS / FAIL / NEEDS_REVIEW / NOT_APPLICABLE / NOT_APPLICABLE_NO_ELEMENT_FOUND / NOT_APPLICABLE_SCOPE_UNCONFIRMED / CONFLICT.

verdict_id*Verdict Id

Verdict row UUID.

Formatuuid
verdict_raw?|

The stored verdict, for audit. Never the value to act on: read verdict.

curl -X GET 'https://api.aisc.sa/v1/jobs/497f6eca-6276-4993-bfeb-53cbbbba6f08/verdicts/497f6eca-6276-4993-bfeb-53cbbbba6f08/citation'
{  "cited_rule_quote": "string",  "element": {    "bbox": {      "x_max": 12.4,      "x_min": 10,      "y_max": 6,      "y_min": 4.5    },    "document_id": "b792e8ae-2cb4-4209-85b9-32be4c2fcdd6",    "element_id": "98026c2a-b90c-4843-8adc-9992613765c9",    "element_type": "corridor",    "label_ar": "string",    "label_en": "string",    "page": 3,    "sheet_id": "90db66ec-6a25-4d34-a966-efba30db59e0"  },  "job_id": "453bd7d7-5355-4d6d-a38e-d9e7eb218c3f",  "justification_ar": "string",  "justification_en": "string",  "rule_id": "801-1020.2",  "verdict": "FAIL",  "verdict_id": "9e06fb72-25b2-4aaa-889d-bb1391af7030",  "verdict_raw": "FAIL"}
GET
/v1/projects/{project_id}/jobs

Returns recent compliance jobs for the given project, ordered by created_at descending. Paginated via limit and offset.

Authorization

BearerAuth
headerAuthorizationBearer <token>

JWT access token. Obtain via POST /v1/auth/login.

Path Parameters

project_id*Project Id
Formatuuid

Query Parameters

limit?Limit
Range1 <= value <= 100
Default20
offset?Offset
Range0 <= value
Default0

Header Parameters

accept-language?string|null

Response Body

List of jobs.

application/json
  1. response
[index: integer]?

Response for GET /v1/jobs/{job_id} and POST /v1/jobs.

The response shape is the same for both endpoints. On POST, status='pending' and all verdict/element fields are null. On GET after completion, the full verdict + element sets are populated.

Examples: # POST /jobs response (pending) { "job_id": "550e8400-e29b-41d4-a716-446655440000", "status": "pending", "estimated_cost_credits": 12.5, "eta_seconds": 90 }

# GET /jobs/{id} response (completed){  "job_id": "550e8400-e29b-41d4-a716-446655440000",  "status": "completed",  "verdict_summary": {"total": 34, "pass_count": 28, ...},  "verdicts": [...],  "elements": [...]}
curl -X GET 'https://api.aisc.sa/v1/projects/497f6eca-6276-4993-bfeb-53cbbbba6f08/jobs'
[  {    "document_coverage": [      {        "document_id": "b792e8ae-2cb4-4209-85b9-32be4c2fcdd6",        "filename": "A-101-GROUND-FLOOR.dwg",        "layer_convention": "iso_13567",        "matched_count": 0,        "onboarding_pairs": [          {            "element_code": "G25",            "label": "Wall"          }        ],        "reason": "string",        "total_entities": 0,        "unread": false      }    ],    "elements": [      {        "attributes": {          "area_sqm": 45.2,          "width_mm": 4500        },        "bbox": {          "height": 200,          "width": 80,          "x": 120.5,          "y": 340.2        },        "document_id": "b792e8ae-2cb4-4209-85b9-32be4c2fcdd6",        "element_id": "98026c2a-b90c-4843-8adc-9992613765c9",        "element_type": "room",        "label_ar": "ممر هروب",        "label_en": "Egress corridor",        "page": 3      }    ],    "error_code": "DOCUMENT_PARSE_FAILED",    "error_message_ar": "فشل تحليل ملف PDF: تنسيق غير مدعوم",    "error_message_en": "Document parse failed: unsupported format",    "estimated_cost_credits": 12.5,    "eta_seconds": 90,    "job_id": "550e8400-e29b-41d4-a716-446655440000",    "result_pdf_url": "https://s3.eu-central-1.amazonaws.com/emtithal-...?X-Amz-Expires=900",    "status": "pending",    "verdict_summary": {      "conformity_form": {        "blocking": [          "COVERAGE_SHORT",          "NEEDS_REVIEW"        ],        "emittable": true,        "rules_examined": 2886,        "rules_in_force": 2886      },      "fail_count": 4,      "needs_review_count": 2,      "not_applicable_count": 0,      "not_evaluable_count": 0,      "overridden_count": 0,      "pass_count": 28,      "rules_examined": 34,      "rules_in_force": 7687,      "rules_withheld": 0,      "total": 34    },    "verdicts": [      {        "citations": [          {}        ],        "cited_drawing_element_id": "1aea6d40-086c-4933-ba23-c70ad6166893",        "cited_rule_quote": "لا يجب أن يقل عرض الممر عن ١١٢٠ ملم",        "code_edition_id": "4a5f38c5-02cb-48ac-ba22-00d46bdf8402",        "code_id": "801",        "confidence": 0.92,        "element_id": "98026c2a-b90c-4843-8adc-9992613765c9",        "justification_ar": "عرض الممر 1100 ملم أقل من الحد الأدنى المطلوب 1120 ملم",        "justification_en": "Corridor width 1100mm is below the required minimum of 1120mm",        "rule_id": "SBC301-1005.1",        "verdict": "FAIL",        "verdict_id": "a8098c1a-f86e-11da-bd1a-00112444be1e",        "verdict_raw": "FAIL",        "version": 1      }    ],    "verdicts_inline_count": 0,    "verdicts_truncated": false,    "verdicts_url": "/v1/jobs/550e8400-e29b-41d4-a716-446655440000/verdicts"  }]