Analysis

Read what was already found.

Analysis is expensive and per-version, so nothing produces one as a side effect. GET /projects/{id}/analysis reads the run the platform already did; POST /projects/{id}/analyses is how you ask for a fresh one. Both need analysis_tab.

Read the current version's analysis

GET /projects/{id}/analysis returns the analysis for the project's current version, or for the version ?version_id= names.

json
{
  "project_id": "prj_…",
  "version_id": "ver_…",
  "is_current_version": true,
  "status": "complete",
  "analyzed_at": "2026-09-01T14:22:08.114Z",
  "results": { "…": "the stored findings blob" }
}

The five statuses

StatusMeaning
not_analyzedNo run has ever happened for this version, and none will start on its own. A normal state, not an error — and the expected answer right after committing a version, which never analyzes. Do not poll it.
queued · runningA run is in flight. The response carries a message with poll guidance and no results yet.
completeresults is populated. A partial failure still lands here and names the phases that failed in failed_phases.
errorThe run failed outright; failed_phases names the phases.

Findings describe a version, and say which one

Every payload carries is_current_version. When it is false, the findings describe code the project no longer contains — they may already have been fixed by the commit that superseded that version, or that commit may have introduced defects the run never saw.

Reachable and labelled, never substituted. No read ever serves an older version's findings under a newer version's name. On a current-version read with not_analyzed, last_analyzed_version_id (plus last_analyzed_at) points at the newest version that does have a completed analysis. Fetch it deliberately with ?version_id= and it comes back labelled is_current_version: false.

Ask for a fresh run

POST /projects/{id}/analyses returns 202 with the run id immediately. It is metered, needs an Idempotency-Key, and a concurrent start never schedules a second billable run — started: false means a healthy run already owned the version and the id is that run's.

json
{
  "analysis_id": "an_…",
  "project_id": "prj_…",
  "version_id": "ver_…",
  "status": "queued",
  "started": true
}

Read that run back with GET /analyses/{analysisId}. It stays pinned to the version the run analysed, so a version committed mid-run does not lose it — where the project-keyed read always follows the current version. Comparing the two tells you whether the analysis you started is still the live one.

python
a = client.get_project_analysis("prj_…")

if a.status == "not_analyzed":
    # Nothing is coming on its own. Either read the older run…
    if a.last_analyzed_version_id:
        old = client.get_project_analysis(
            "prj_…", version_id=a.last_analyzed_version_id
        )
        print(old.is_current_version)      # False — labelled, never substituted
    # …or spend on a fresh one.
    started = client.start_analysis("prj_…")
    a = client.wait_for_analysis(started.analysis_id)

if a.status == "complete":
    print(a.results, a.failed_phases)
csharp
var a = await client.GetProjectAnalysisAsync("prj_…");

if (a.Status == "not_analyzed")
{
    var started = await client.StartAnalysisAsync("prj_…");
    a = await client.WaitForAnalysisAsync(started.AnalysisId);
}

if (a.Status == "complete")
    Console.WriteLine(a.Results);

All three reads are on the job-poll tier — 12 requests / minute. A run takes seconds to minutes, so the SDKs' wait_for_* helpers pace themselves for it; they also return on not_analyzed rather than waiting for a run that is never coming.