REST API

The web UI drives everything through these endpoints. Base URL: https://policypilot.sabiapps.com

Auth. Browser clients use the session cookie plus a CSRF token from GET /api/csrf in an x-csrf-token header. Machine clients send x-api-key and are exempt from CSRF. An API key carries its organisation, so every response is scoped to it. Create keys under Account.

POST /api/assessments

Run a gap analysis. The endpoint you would wire into CI.

curl -sX POST https://policypilot.sabiapps.com/api/assessments \
  -H 'x-api-key: pp_...' \
  -H 'content-type: application/json' \
  -d '{
    "policyId": 1,
    "frameworkIds": ["soc2", "iso27001"],
    "useAi": true,
    "save": true,
    "label": "CI run for PR #482"
  }'

Pass text instead of policyId to assess an unsaved document. Returns one entry per framework with the full per-control results, so a pipeline can fail the build on a regression:

{
  "document": { "wordCount": 4821, "sentenceCount": 312 },
  "assessments": [{
    "id": 17,
    "frameworkId": "soc2",
    "weightedCoverage": 0.78,
    "totals": { "covered": 24, "partial": 9, "missing": 7, "contradicted": 1, "excluded": 0 },
    "readiness": { "grade": "C", "verdict": "...", "criticalFailures": 1 },
    "priorityGaps": [ { "controlId": "CC6.7", "status": "partial", "severity": "critical", ... } ],
    "results": [ {
      "controlId": "CC6.1",
      "status": "covered",
      "score": 1,
      "confidence": 0.91,
      "requiredGroupsSatisfied": 2,
      "requiredGroupsTotal": 2,
      "evidence": [ { "text": "Access is granted on least privilege...", "start": 1204 } ],
      "rationale": "All 2 required elements of this control are addressed. ..."
    } ]
  }]
}

weightedCoverage weights each control by severity (critical ×4 down to low ×1). coverage is the unweighted count. Both are reported because they diverge exactly when it matters.

Reference data

EndpointReturns
GET /api/frameworksAll 305 controls' metadata, grouped by framework.
GET /api/frameworks/:idOne framework in full, including every control's signal table.
GET /api/frameworks/:id/controls/:controlIdOne control plus its computed crosswalk to other frameworks.
GET /api/topicsThe shared topic vocabulary and the multi-framework clusters.
GET /api/reference/riskLikelihood, impact, treatment and effectiveness scales.

Everything else

EndpointPurpose
GET /api/dashboardEvery headline number on one call.
GET /api/assessmentsList past assessments.
GET /api/assessments/:id/export?format=json|csv|mdDownload a report.
GET /api/assessments/compare/:frameworkIdDelta between the two most recent runs: which controls improved and which regressed.
POST /api/assessments/:id/remediationA remediation plan grouped into workstreams.
GET /api/policies, POST /api/policiesList and create policies.
POST /api/policies/:id/versionsSave a new version. Identical text is rejected with 409.
POST /api/policies/:id/statusdraft → review → approved → retired. Invalid jumps are rejected.
POST /api/policies/generateGenerate a draft from framework controls.
GET /api/controls/:frameworkId, PUT /api/controls/:frameworkId/:controlIdRead and update the control register.
GET /api/evidence, POST /api/evidenceEvidence tracker, linked to controls across frameworks.
GET /api/risks, POST /api/risks, PUT /api/risks/:idRisk register with computed inherent and residual scores.
POST /api/risks/from-assessment/:idSeed the register from an assessment's gaps.
POST /api/obligations/extractPull binding statements out of a contract or regulation.
GET /api/audit, GET /api/audit/verifyAudit log, and hash-chain verification. Returns 409 if the chain is broken.

Errors

{
  "error": {
    "code": "unprocessable",
    "message": "Some fields need attention.",
    "details": [ { "path": "frameworkIds", "message": "Pick at least one framework." } ],
    "requestId": "8f1c…"
  }
}
StatusCodeWhen
400bad_requestInvalid transition, or a control scoped out without a reason.
401unauthorizedNo session and no valid API key.
403forbiddenRole does not permit it, or a missing CSRF token.
404not_foundNo such resource in your organisation.
409conflictDuplicate version, or a broken audit chain on /audit/verify.
422unprocessableValidation failed; see details.
429rate_limitedCheck the RateLimit-* headers.

A model outage never produces a 5xx. The model review fails soft and you get the deterministic assessment with engine: "rules".