curl --request GET \
--url https://api.example.com/api/v1/workspaces/{workspace_id}/stats \
--header 'X-API-Key: <api-key>'import requests
url = "https://api.example.com/api/v1/workspaces/{workspace_id}/stats"
headers = {"X-API-Key": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {'X-API-Key': '<api-key>'}};
fetch('https://api.example.com/api/v1/workspaces/{workspace_id}/stats', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.example.com/api/v1/workspaces/{workspace_id}/stats",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"X-API-Key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.example.com/api/v1/workspaces/{workspace_id}/stats"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("X-API-Key", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api.example.com/api/v1/workspaces/{workspace_id}/stats")
.header("X-API-Key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/api/v1/workspaces/{workspace_id}/stats")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["X-API-Key"] = '<api-key>'
response = http.request(request)
puts response.read_body{
"artifact_type_breakdown": {},
"consumed_artifact_count": 123,
"declaration_outcomes": {
"external": 123,
"local_reference": 123,
"resolved_in_org": 123,
"self_reference": 123,
"unresolved_in_org": 123
},
"dependency_type_breakdown": {},
"last_scan": {
"completed_at": "2023-11-07T05:31:56Z",
"errors": 123,
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"repos_scanned": 123,
"status": "<string>"
},
"orphan_artifact_count": 123,
"total_artifact_rows": 123,
"total_declarations": 123,
"total_dependencies": 123,
"total_repositories": 123,
"upstream_alias_artifact_count": 0
}{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>",
"ctx": {},
"input": "<unknown>"
}
]
}Get Workspace Stats
Aggregate statistics across every connected org in the workspace: repository and dependency counts, artifact orphan breakdowns, last scan summary, and the declaration-outcome breakdown.
curl --request GET \
--url https://api.example.com/api/v1/workspaces/{workspace_id}/stats \
--header 'X-API-Key: <api-key>'import requests
url = "https://api.example.com/api/v1/workspaces/{workspace_id}/stats"
headers = {"X-API-Key": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {'X-API-Key': '<api-key>'}};
fetch('https://api.example.com/api/v1/workspaces/{workspace_id}/stats', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.example.com/api/v1/workspaces/{workspace_id}/stats",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"X-API-Key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.example.com/api/v1/workspaces/{workspace_id}/stats"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("X-API-Key", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api.example.com/api/v1/workspaces/{workspace_id}/stats")
.header("X-API-Key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/api/v1/workspaces/{workspace_id}/stats")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["X-API-Key"] = '<api-key>'
response = http.request(request)
puts response.read_body{
"artifact_type_breakdown": {},
"consumed_artifact_count": 123,
"declaration_outcomes": {
"external": 123,
"local_reference": 123,
"resolved_in_org": 123,
"self_reference": 123,
"unresolved_in_org": 123
},
"dependency_type_breakdown": {},
"last_scan": {
"completed_at": "2023-11-07T05:31:56Z",
"errors": 123,
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"repos_scanned": 123,
"status": "<string>"
},
"orphan_artifact_count": 123,
"total_artifact_rows": 123,
"total_declarations": 123,
"total_dependencies": 123,
"total_repositories": 123,
"upstream_alias_artifact_count": 0
}{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>",
"ctx": {},
"input": "<unknown>"
}
]
}Authorizations
Headers
Path Parameters
Cookies
Response
Successful Response
Per-connected-org stats payload (scoped to one connected org).
Artifact-count semantics. Every count is a raw artifact-row count — no
canonical-name dedup — restricted to first-party artifacts: rows a forked
repo produces only because it kept the upstream project's package identity are
excluded and counted separately (issue #258; is_first_party_artifact in
src/resolver/fork_alias.py is the single source of truth, cached on
artifacts.is_first_party). Consumption is computed by the single source of
truth src/graph/artifact_stats.py (the type-matched union), so an
artifact's orphan status here always matches its is_orphan on
GET /connected-orgs/{id}/artifacts:
total_artifact_rows— first-party artifact rows. Equals theX-Total-Countheader ofGET /connected-orgs/{id}/artifactswith its defaultinclude_upstream_aliases=false.upstream_alias_artifact_count— the rows that filter excludes. Add it tototal_artifact_rowsfor every row the org has, which is what?include_upstream_aliases=truereturns and what a disconnect deletes.orphan_artifact_count— first-party rows with zero consumers (consumer_count == 0). Equals the count ofis_orphan==truerows on the artifacts endpoint and the Artifacts page "Hide orphans (N)" toggle.consumed_artifact_count— first-party rows with at least one consumer (== total_artifact_rows - orphan_artifact_count).artifact_type_breakdown— first-party artifact rows grouped byartifact_type(includes every type present; never drops one). Invariant:sum(artifact_type_breakdown.values()) == total_artifact_rows.
total_declarations vs total_dependencies: the former counts every
manifest-context dependency declaration parsed out of the org's repos, the
latter counts the deduped cross-repo edges those declarations resolved to.
The gap between them is the org's coverage story — Patrimore parsed 4,390 and
got 9 edges — and naming both numbers is what lets a near-empty graph explain
itself instead of reading as a broken product (issue #225).
declaration_outcomes breaks that gap down: it partitions the same
total_declarations population into why each declaration did or did not
become an edge, so a large gap can be read as the ~99% correct rejections it
usually is rather than as a failure count (issue #264). It replaced
health, whose unresolved_declarations was that misreadable number and
whose other two fields were hardcoded zeros; the real on-latest / lagging
figures live on GET /artifacts/{id}/consumers.
Show child attributes
Show child attributes
What became of every dependency declaration parsed from the org (issue #264).
Five mutually exclusive buckets over the same population as
total_declarations — manifest-context declarations, scoped per repo to its
current_scan_id. They sum to total_declarations.
Replaces health.unresolved_declarations, which counted every unresolved
declaration regardless of why, named the result as if it were a failure count,
and — because it was computed over all reference contexts while
total_declarations is manifest-only — could exceed the total it was shown
against (cloudposse: 13,225 unresolved against 6,374 total).
Only unresolved_in_org means "Riftmap may have missed something". The other
three non-resolved buckets are correct rejections: a third-party package is
supposed to stay unresolved, a workspace: dependency lives in the same repo
by definition, and a repo importing its own subpackage is not a cross-repo edge.
Measured across all 321,445 production declarations on 2026-09-12: 86.13%
external, 4.73% resolved_in_org, 4.46% local_reference, 3.86%
self_reference, 0.81% unresolved_in_org. Those are platform
shares — a single org can sit far from them, so do not read the platform
figure as a per-org expectation. They predate v1.25.0, which counts every
repeated declaration of a reference (a second uses: of one action in a
workflow, a package listed as both a dev and a peer dependency), so every
bucket rises for an org once it is rescanned.
resolved_in_org is normally larger than total_dependencies, and that
is not a bug: it counts declarations, while total_dependencies counts deduped
graph edges. A declaration is not an edge — read-time DISTINCT ON collapses
declarations that share a (source, target, dependency_type) key, self-edges
are excluded from the graph, and an artifact target with no producing repo
renders no edge.
Classification is owned by src/graph/declaration_outcomes.py — the single
source of truth, which also documents what these buckets structurally cannot
see (a fork-alias rejection lands in external).
Show child attributes
Show child attributes
Show child attributes
Show child attributes
Summary of the most recent scan.
Show child attributes
Show child attributes
