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.
{
"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
| Status | Meaning |
|---|---|
not_analyzed | No 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 · running | A run is in flight. The response carries a message with poll guidance and no results yet. |
complete | results is populated. A partial failure still lands here and names the phases that failed in failed_phases. |
error | The 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.
{
"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.
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)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.