REST API
Aveloxis includes a REST API server for programmatic access to collected data, repository statistics, time-series metrics, SBOM downloads, and vulnerability information. Start it with:
aveloxis api --addr :8383
The API runs as a separate process alongside aveloxis serve (collection) and aveloxis web (GUI). All three share the same PostgreSQL database.
Endpoints
Health Check
GET /api/v1/health
Returns the server status and version.
{"status": "ok", "version": "0.9.0"}
Public fleet stats
GET /api/v1/public/stats
Public (no token required, like /health) — the landing page’s
fleet-scale numbers. repos is the TOTAL repository count in the
catalog (v0.27.68: archived repos included — they carry full
collected history and analysis, and the number should agree with the
monitor’s queue view). v0.27.77 adds commits, issues, and prs
(sums of the collection queue’s cached cumulative per-repo gathered
totals — never a live scan of the data tables) and contributors
(the contributor catalog count). Served from a 60-second cache that
keeps the last good values if the database hiccups. Still subject to
the per-IP rate limiter.
{"repos": 94104, "commits": 512345678, "issues": 9123456, "prs": 8123456, "contributors": 1712345}
Repository Statistics
GET /api/v1/repos/{repoID}/stats
Returns gathered (collected totals) vs metadata (API-reported totals) for a single repo.
{
"repo_id": 42,
"gathered_prs": 1500,
"gathered_issues": 800,
"gathered_commits": 5000,
"metadata_prs": 1520,
"metadata_issues": 810,
"metadata_commits": 5100,
"vulnerabilities": 12,
"critical_vulns": 2,
"forked_from": "NixOS/nixpkgs"
}
Gathered counts are the collection queue’s cached cumulative totals, as of the repo’s last completed collection cycle (v0.27.85 — the same source and freshness as the monitor, group, and collection listings; previously three live
COUNT(*)aggregates that cost ~23,500 buffer pages per call on a large repo).Metadata counts come from the most recent
repo_infosnapshot (GitHub GraphQL / GitLab API totals).Vulnerabilities come from OSV.dev vulnerability scanning.
forked_from (v0.27.79, omitted when empty) is the upstream
owner/namewhen the repository is a fork — captured from the forge on every Phase 0 cycle since v0.27.78. The literal(unknown upstream)means the forge reports fork status but the upstream was deleted or is inaccessible. Drives the repo page’s “Forked from X” chip.
Batch Statistics
GET /api/v1/repos/stats?ids=1,2,3,42
Returns stats for multiple repos in one call. Response is a map keyed by repo ID.
Since v0.28.7 the batch rows carry the same gone_at and
metadata_as_of fields as the single-repo endpoint, and requested ids
with no collection-queue row (the prelim-dequeued “gone” cohort) fall
back to live gathered counts instead of serving zeros — the two
endpoints can no longer disagree about a vanished repository. The
fallback runs only for that rare queueless subset; every tracked repo
stays on the cached queue counts.
Since v0.28.10 the id list is capped at 500 per request (requests above the cap get a 400) — the queueless fallback makes per-id cost non-trivial for gone repos, so the batch stays bounded. Every real consumer (monitor at 200/page, group pages at 100/page) sits well under the cap.
Time Series
GET /api/v1/repos/{repoID}/timeseries
GET /api/v1/repos/{repoID}/timeseries?since=2024-01-01
Returns weekly aggregated counts for commits, PRs opened, PRs merged, and issues.
Parameter |
Type |
Default |
Description |
|---|---|---|---|
|
date (YYYY-MM-DD) |
2 years ago |
Start date for time series |
{
"repo_id": 42,
"repo_name": "augur",
"repo_owner": "aveloxis",
"commits": [
{"week_start": "2024-01-01T00:00:00Z", "count": 15},
{"week_start": "2024-01-08T00:00:00Z", "count": 22}
],
"prs_opened": [ /* ... */ ],
"prs_merged": [ /* ... */ ],
"issues": [ /* ... */ ]
}
Weeks are Monday-aligned via PostgreSQL date_trunc('week', timestamp). Queries use indexed timestamp columns for fast responses even on large databases.
Dependency Licenses
GET /api/v1/repos/{repoID}/licenses
Returns a summary of dependency licenses with counts and OSI compliance status.
[
{"license": "MIT", "count": 45, "is_osi": true},
{"license": "Apache-2.0", "count": 12, "is_osi": true},
{"license": "Unknown", "count": 3, "is_osi": false}
]
OSI compliance is checked against a built-in list of 30+ known OSI-approved SPDX identifiers.
Parameter |
Values |
Default |
Description |
|---|---|---|---|
|
|
|
v0.27.46: |
Repository Search
GET /api/v1/repos/search?q=augur
Case-insensitive search across repo name, owner, and URL. Returns up to 20 matches. Used by the comparison page’s autocomplete search.
[
{"id": 2, "owner": "aveloxis", "name": "augur"},
{"id": 31, "owner": "chaoss", "name": "augur-license"}
]
SBOM Download
GET /api/v1/repos/{repoID}/sbom?format=cyclonedx
GET /api/v1/repos/{repoID}/sbom?format=spdx
Generates and downloads a Software Bill of Materials in CycloneDX 1.5 or SPDX 2.3 JSON format. The SBOM is generated on-the-fly from collected dependency data.
Since v0.27.134, repositories with C2 lockfile data (collection.vuln_scan_transitive enabled) get the real dependency graph: lockfile transitives join the component/package list, CycloneDX’s dependencies array carries actual parent→child links from the stored edges (root → direct set; parents → their children; unreachable packages stay leaves), and SPDX gains the matching package→package DEPENDS_ON relationships. Repositories without C2 data keep the flat pre-v0.27.134 shape byte-identical.
Parameter |
Values |
Default |
Description |
|---|---|---|---|
|
|
|
SBOM format |
|
|
|
v0.27.46: |
|
|
absent |
Annotate with the repo’s CURRENT (unresolved) findings. CycloneDX: native 1.5 |
Returns JSON with Content-Disposition: attachment header for download.
Contributor identities in a window
GET /api/v1/repos/{repoID}/contributions/identities
GET /api/v1/repos/{repoID}/contributions/identities?since=2024-01-01
GET /api/v1/repos/{repoID}/contributions/identities?since=2024-01-01&until=2024-12-31
Returns every distinct contributor who made any kind of contribution to the repo in the requested window. The result is one row per person, suitable for rendering a roster or building an affiliation chart against a derived per-person grouping.
Parameter |
Type |
Default |
Description |
|---|---|---|---|
|
date (YYYY-MM-DD) |
2 years ago |
Window lower bound (inclusive) |
|
date (YYYY-MM-DD) |
unbounded (now) |
Window upper bound (inclusive — see note below) |
The until date is treated as inclusive of the entire calendar day — the server shifts it by +1 day before comparing against the half-open < upper SQL filter, so passing until=2024-12-31 captures everything through 2024-12-31T23:59:59.999Z.
Malformed dates fall back to the defaults rather than returning 400, matching the existing /timeseries endpoint behavior so charts and dashboards keep rendering. The one validation error that does surface as 400 is since >= until, which is almost certainly an operator typo.
Response shape:
[
{
"cntrb_id": "01000001-0000-4000-8000-000000000000",
"login": "alice",
"full_name": "Alice Anderson",
"email": "alice@example.com",
"profile_company": "Acme Corp",
"location": "Berlin",
"activity_class": "private-active",
"public_contribs_year": 0,
"restricted_contribs_year": 812,
"last_contribution_year": 2026
}
// ...more rows
]
All string fields are normalized server-side: "" represents “no value recorded” (no per-field null handling needed on the client). Ordering is by login NULLS LAST, full_name NULLS LAST so unidentifiable contributors sort to the bottom of the roster.
Activity classification fields (v0.27.57, GitHub only — GitLab has no
equivalent of restrictedContributionsCount, so gl-side contributors keep
an empty activity_class): sourced from GitHub’s GraphQL
contributionsCollection over the trailing year and refreshed by a
scheduler sweep on the breadth cooldown cadence.
|
Meaning |
|---|---|
|
Public contributions > 0 in the trailing year. |
|
Zero public, but the user disclosed private activity ( |
|
Nothing observable this year; |
|
Nothing, ever. Render this honestly — GitHub deliberately makes truly-inactive indistinguishable from active-only-in-private with disclosure off. |
|
Never checked yet, GitLab-side contributor, or the account was deleted/renamed at check time. |
public_contribs_year is derived as calendar-total minus restricted
(the calendar includes disclosed private contributions).
What counts as a contribution (all in one window via the unified messages table and the standard work-tracking tables):
Kind |
Source table |
Time column |
|---|---|---|
Commit authorship |
|
|
Issue opened |
|
|
Issue closed |
|
|
Issue event (label / assignment / reference) |
|
|
PR opened |
|
|
PR review submitted |
|
|
PR event |
|
|
Any message (issue comment, PR conversation comment, inline review comment body) |
|
|
Per the “Unified message architecture” contract, all three text-contribution kinds live in messages with cntrb_id as the author — one filter covers them all.
What’s intentionally not counted:
Assignees and reviewers who never actually did anything — being asked to review isn’t a contribution. They show up in
*_assignees/*_reviewerstables but aren’t surfaced here.Commits whose
cmt_ght_author_idis NULL — these are commits aveloxis hasn’t been able to resolve to a contributor row (private email, or the search-resolve background ticker hasn’t reached them yet). They don’t have acntrb_idto return. The number of such commits in a given window is queryable via the metric endpoints (/code-changes) and the gap is closed over time by the v0.19.2 search-resolve work.contributor_reporows from the breadth worker — those represent “this person was active anywhere on the repo at some point” but the time semantics are different (collection-cycle timestamp, not when the contribution happened), so they don’t belong in a contribution-window query.Soft-deleted contributors (
cntrb_deleted != 0) — the v0.20.2 logical-merge path marks loser rows when a rename was detected. Filtering them out is the contract; merged identities surface only under the winningcntrb_id.
Affiliation breakdown for the same window
GET /api/v1/repos/{repoID}/contributions/affiliations
GET /api/v1/repos/{repoID}/contributions/affiliations?since=2024-01-01&until=2024-12-31
Returns the count of distinct contributors per affiliation, using the same window and the same contribution-kind definition as /contributions/identities. The two endpoints share a single SQL CTE on the server so the two responses can never disagree on which people are in scope — a sum across this endpoint’s contributor_count values equals the row count of /contributions/identities.
Same since / until parameters as the identities endpoint; same behavior on malformed input and since >= until.
Response shape:
[
{"affiliation": "Acme Corp", "contributor_count": 47},
{"affiliation": "RedHat", "contributor_count": 12},
{"affiliation": "(unknown)", "contributor_count": 31}
]
Ordered by contributor_count DESC then affiliation ASC. The (unknown) bucket is included rather than hidden so callers can decide whether to surface unaffiliated contributors (often the right call on community projects) or omit them.
Affiliation derivation priority (applied per-contributor):
contributor_affiliations[domain_of(cntrb_canonical)]— the curated email-domain → org map maintained by aveloxis’sPopulateAffiliationsbackground task. This is the most reliable signal because it covers people whose GitHub/GitLab profile is blank but whose verified email domain is well-known (e.g.@redhat.com→ “RedHat”).cntrb_company— what the user typed into their GitHub or GitLab profile. Freeform text; often blank, sometimes “@org” (the GitHub@-mention reference style — aveloxis strips the leading@before using it as an affiliation label).(unknown)— fallback bucket for contributors with neither a domain-mapped canonical email nor a profile company string.
The derivation priority is deliberate: the curated domain map is updated by a background task that watches observed contributor data, while the profile field is freeform and easily stale (“Self-employed”, “Earth”, typos of well-known company names, etc.). When both are present the domain-mapped value wins because it’s more likely to be canonical.
Tweaks you can make on the client side
Narrow to creative work only: this endpoint includes everything. To exclude event-only activity (labels, references) post-process the identities list against another endpoint that’s restricted to commits/PRs/issues only, or filter on the client.
Group of repos: the two endpoints are per-repo. For an org-wide rollup, call them for each repo in the group and merge the responses (the
cntrb_idcolumn is stable across repos so dedup is trivial).Hide the
(unknown)bucket: filter on the client. The server returns it so the math reconciles with/contributions/identities.Different windows:
?since=YYYY-MM-DDand?until=YYYY-MM-DDare both accepted independently. Omituntilfor “everything sincesince”.
Top contributors, ranked (v0.27.61)
GET /api/v1/repos/{repoID}/contributors/top
GET /api/v1/repos/{repoID}/contributors/top?since=2024-01-01&until=2024-12-31&limit=20
The ranked “who does the work here” table behind the repo page’s Top
contributors card. Same since/until window semantics as the
/contributions/* endpoints (default: trailing 2 years; until is
inclusive); limit defaults to 20 and is capped at 100.
?bots=hide (v0.27.69; widened v0.28.1) filters automation
identities by four markers: the non-human account types
gh_type IN ('Bot', 'ProgrammaticAccessBot', 'Organization')
(GitHub App accounts like dependabot[bot]; fine-grained-PAT
actors; org accounts acting as contributors — the codecov shape,
whose enriched row is typed Organization), the [bot] login
suffix, the hyphenated -bot/-robot machine-account convention
(the k8s-ci-robot class, which GitHub types as a regular User —
verified live), and the curated system-account list (actions-user,
web-flow, ghost, codecov, codecov-io, codecov-commenter — machine
accounts GitHub types as plain User). Mannequin rows are
deliberately NOT hidden: mannequins are import placeholders
standing in for unmatched humans, not automation. The separator
requirement keeps human surnames (talbot) safe. Deliberately
broader than the contributor_retention metric’s bot exclusion,
which is pinned to 8Knot parity; this one is a display filter. The same parameter works
on /contributors/elsewhere so the two surfaces stay consistent. Requires the
same repo scope as every other per-repo endpoint; responses are served
from a 60-second cache (the underlying data only changes per
collection cycle).
Response:
{
"since": "2024-07-31T00:00:00Z",
"until": "0001-01-01T00:00:00Z",
"limit": 20,
"contributors": [
{
"cntrb_id": "01001234-...",
"login": "octocat",
"full_name": "Octo Cat",
"activity_class": "active",
"commits": 412, "issues": 38, "prs": 91, "reviews": 130, "comments": 505,
"total": 1176
}
]
}
What counts, per column: commits = DISTINCT commit hashes authored
(windowed on the commit’s author timestamp); issues = issues opened;
prs = pull requests opened; reviews = PR reviews submitted;
comments = unified messages (issue comments + PR conversation +
inline review comments). total is the sum and the ranking key.
Issue/PR events (labels, assignments, closes) deliberately do NOT
count here even though they DO count for cohort membership in
/contributions/identities — this endpoint ranks creative/review
work; the identities endpoint enumerates everyone who touched the
repo at all. activity_class is the v0.27.57 classification (empty =
never checked, GitLab-side, or — since v0.27.81 — an account whose
activity query exceeds GitHub’s per-query resource budget even when
fetched alone; those accounts are stamped checked without a class so
they stop re-claiming). Commits whose author was never resolved
to a contributor identity are excluded — there is no identity to rank.
Where else are these contributors active? (v0.27.64)
GET /api/v1/repos/{repoID}/contributors/elsewhere
GET /api/v1/repos/{repoID}/contributors/elsewhere?since=2026-02-01&limit=10
For the repo’s top contributors (same ranking as /contributors/top;
limit default 10, cap 25), their top-10 OTHER repositories from the
v0.27.58 contributor daily-history tables (GitHub
contributionsCollection data), aggregated over the window (since
defaults to the trailing 180 days — the standard history window).
Response rows per contributor:
{
"since": "2026-02-01", "limit": 10,
"contributors": [
{
"cntrb_id": "0100...", "login": "octocat",
"backfilled_at": "2026-07-30T02:11:00Z",
"elsewhere": [
{"repo_full_name": "kubernetes/kubernetes", "repo_id": 4021,
"active_days": 14, "commits": 33, "issues": 2, "prs": 5, "reviews": 9},
{"repo_full_name": "torvalds/linux",
"active_days": 3, "commits": 4, "issues": 0, "prs": 0, "reviews": 0}
]
}
]
}
Reading backfilled_at (the honesty rule): a null stamp means
the v0.27.58 history backfill has not reached this contributor yet —
their empty elsewhere array is “history pending”, NOT “active
nowhere else”. Frontends must render the two differently. Exception
(v0.27.81; widened v0.28.1): accounts typed Bot /
ProgrammaticAccessBot / Organization and the curated system-account
list (actions-user, web-flow, ghost, codecov, codecov-io,
codecov-commenter) are excluded from the backfill claim entirely —
their stamps stay null permanently by design (their histories are
the most expensive to fetch and the contributor surfaces exclude
bots from display anyway). repo_id
is present only when the repo is tracked on this instance
(GitHub-side match; the history source is GitHub-only). The current
repo is excluded case-insensitively (GitHub’s canonical casing can
differ from ours).
One contributor’s cross-repo activity (v0.27.64)
GET /api/v1/contributors/{cntrbID}/activity
GET /api/v1/contributors/{cntrbID}/activity?bucket=month&months=24
The person-level view: monthly per-repo activity buckets (top-10
repos by total activity in the window) plus the contribution
calendar’s disclosed day-total series (day_totals — includes
private contributions when the user enabled disclosure, which is why
it can exceed the per-repo sum). months defaults 24, caps 60;
bucket accepts only month (400 otherwise — reserved for future
granularities). cntrbID must be a UUID (400 otherwise); unknown
contributors 404. Operational failures (database errors) surface as
500, never 404 — a 404 always means “this id parsed but nobody has
it” (v0.27.76).
Requires a Bearer identity but NOT repo scope: person-level history
spans repositories this instance doesn’t track, so repo scope cannot
express an authorization boundary for it — and it is not anonymous
data, so a signed-in identity is the floor. Carries backfilled_at
with the same semantics as /contributors/elsewhere.
Knowing whether your coverage is complete
GET /api/v1/repos/{repoID}/contributions/coverage
GET /api/v1/repos/{repoID}/contributions/coverage?since=2024-01-01&until=2024-12-31
Returns the enrichment-state snapshot for the same cohort as /contributions/identities and /contributions/affiliations. Operators call this before drawing conclusions from the affiliation breakdown to tell whether an (unknown) bucket represents truly unaffiliated contributors or just people the v0.18.29 enrichment ticker hasn’t reached yet.
Same since / until parameters as the other two endpoints; same behavior on malformed input and since >= until.
Response shape:
{
"window_since": "2024-05-21T00:00:00Z",
"window_until": "2026-05-21T00:00:00Z",
"total_contributors": 412,
"enriched": 389,
"canonical_email": 356,
"gh_user_id_resolved": 401,
"search_resolve_attempted": 47,
"breadth_attempted": 378,
"affiliation_resolved": 318,
"affiliation_unknown": 94,
"enrichment_oldest_pending": "2026-05-12T18:31:04Z",
"enrichment_stalest": "2024-08-15T03:22:11Z"
}
The two timestamp fields are omitted entirely when the cohort has no rows in the relevant state (no pointer → field absent in JSON rather than emitting zero-time, which is operator-confusing).
Reading the response. A response with total=412, enriched=389, affiliation_resolved=318, affiliation_unknown=94 reads as:
412 people contributed in the window. 389 of them have been successfully enriched via
/users/{login}and 23 haven’t yet — the enrichment ticker is still working through them. 318 have a resolvable affiliation (either via the curated email-domain map or via their profile company field). 94 are bucketed as(unknown)— but 23 of those might be the unenriched cohort that will pick up an affiliation once the ticker reaches them. So the true unaffiliated count for this window is somewhere between 71 (if all 23 unenriched contributors turn out to be unaffiliated) and 94 (if none of them do).
Operators surface this floor-and-ceiling on dashboards rather than reporting (unknown) alone — the latter conflates “no affiliation” with “we haven’t asked yet.”
Field-by-field reference:
Field |
Source signal |
What it tells you |
|---|---|---|
|
The cohort |
Denominator for everything else |
|
|
|
|
|
Verified email known — drives domain → affiliation lookup |
|
|
Person matched to numeric GitHub user (stable identity across renames) |
|
|
v0.19.2 search-resolve ticker has tried to look this person up by email (60-min cooldown, 30-day re-attempt) |
|
|
v0.20.17 breadth worker has tried |
|
Domain-mapped via |
Will show up under a non- |
|
|
The |
|
|
How long the most-delayed unenriched contributor has been waiting — compare against your configured |
|
|
Oldest “last refreshed” timestamp — surfaces the long tail of “enriched 18 months ago and never refreshed” |
Spotting a stuck enrichment ticker. If enrichment_oldest_pending is more than ~2× your configured enrich_interval_minutes behind NOW(), the ticker may be stuck. Investigation:
# What does the enrich interval look like?
grep enrich_interval_minutes ~/.aveloxis/aveloxis.json
# Has the enrichment ticker been ticking?
grep -E "EnrichThinContributors|enrichment" ~/.aveloxis/aveloxis.log | tail -20
# Are we burning API budget?
grep -E "all API keys rate-limited|rate limit" ~/.aveloxis/aveloxis.log | tail -10
If the ticker is running but enrichment is still falling behind, it’s almost always API-key budget exhaustion (the v0.18.29 EnrichBatchSize = 14000 per tick is sized for a 73-key fleet; smaller key pools can’t keep up).
What this endpoint doesn’t tell you:
Per-affiliation coverage drill-down: the response is global to the cohort. If you need “what % of Acme Corp contributors have canonical emails” specifically, that’s a derived query — call
/identitiesand group client-side, or open an issue for a per-affiliation coverage endpoint.Whether
PopulateAffiliationsis current: the domain-mapped affiliations come from thecontributor_affiliationstable, which is rebuilt hourly by the v0.19.7 ticker. The table state at any given moment reflects the most recent successful rebuild, not a continuous live view. If you’ve just added new contributors with novel company strings, give it an hour forPopulateAffiliationsto surface them in the map.Fleet-wide coverage: this endpoint is per-repo. For a fleet-wide rollup, call it per repo and aggregate (or, if there’s operator demand, request a
/api/v1/contributions/coverageglobal endpoint as a follow-up).
When to use which endpoint
Need |
Use |
|---|---|
“Who contributed to this repo in the last two years?” |
|
“How many people from each company contributed?” |
|
“Is the affiliation data trustworthy yet, or is enrichment still catching up?” |
|
“How many new contributors per month did this repo gain?” (Augur metric) |
|
“Total contributor count, no window” |
|
“How many commits per week?” |
|
The Augur-compatible endpoints (/contributors, /contributors-new, etc.) follow Augur’s swagger spec with begin_date / end_date / period query params and return aggregated counts. The /contributions/* endpoints follow the aveloxis convention (since / until) and return per-contributor identity rows, an aggregated affiliation roll-up, and a coverage snapshot respectively. The two groups serve different questions and don’t overlap.
Mailing-list collection coverage
GET /api/v1/mailing-list/stats
Fleet-wide rollup of the mailing-list ingestion subsystem (architecture) — the same data as aveloxis mailing-list-stats. No parameters. Returns 500 if the query fails.
Response (note: keys are PascalCase — the rollup struct carries no JSON tags):
{
"Lists": 16,
"ScanComplete": 14,
"EmailMessages": 68514,
"Mirrors": 41841,
"SignaledCaptured": 40044,
"SignaledResolved": 25591,
"SenderTotal": 26673,
"SenderResolved": 17012,
"ByClass": {
"github_mirror": 40044,
"issue_event": 5251,
"patch_submission": 4568,
"discuss": 1161,
"review": 953
}
}
Field |
Meaning |
|---|---|
|
registered lists, and how many have finished their current scan |
|
total |
|
rows classified as mirror mail ( |
|
messages that named a repo (Axis B) / those resolved to a repo we hold. The ratio is catalog-coverage, not quality — unresolved signals point at sibling repos not yet tracked. |
|
mailing-list message bodies / those whose sender resolved to a contributor (improves over time via the hourly backfill) |
|
per- |
Scancode results
Per-file license and copyright data gathered by the decoupled scancode worker (v0.21.0+).
GET /api/v1/repos/{repoID}/scancode-licenses— aggregated license breakdown from per-file scan results, pluslast_runandscancode_versionfreshness metadata.GET /api/v1/repos/{repoID}/scancode-files— per-file rows (path, detected license expression, copyright holders) backing the repo detail page’s sortable table.
Augur-compatible metric endpoints
For 8Knot and other Augur API consumers, the server also exposes the
Augur swagger-spec metric routes. They use Augur’s parameter conventions
(begin_date, end_date, period) rather than the native since/until.
Catalog routes: /api/v1/repo-groups, /api/v1/repos,
/api/v1/repos/{repoID}, /api/v1/repo-groups/{groupID}/repos,
/api/v1/owner/{owner}/repo/{repo}, /api/v1/rg-name/{rgName},
/api/v1/rg-name/{rgName}/repo-name/{repoName}.
Per-repo metrics (all under /api/v1/repos/{repoID}/):
Category |
Endpoints |
|---|---|
Issues |
|
Pull requests |
|
Commits |
|
Contributors |
|
Popularity |
|
Code / deps |
|
Other |
|
License drill-down on /deps (v0.28.1)
GET /api/v1/repos/{repoID}/deps accepts two optional parameters
that turn it into the drill-down behind the dependency-licenses
table:
license=<canonical>— only rows whose license normalizes to the given canonical bucket (the sameNormalizeLicenseToSPDXmerge the licenses aggregate applies, so “MIT License”-spelled rows match theMITbucket). Pass thelicensevalue exactly as the/repos/{id}/licensesresponse returned it.scope=runtime|all— the same scope filter as/licenses(runtimeexcludes dev/test/build/optional/peer rows). Invalid values are a 400.
With EITHER parameter present, the row universe mirrors the licenses aggregate exactly (GitHub-Actions rows excluded — they carry no license data), so a license bucket counting N always drills down to exactly N rows. With NEITHER parameter, the legacy behavior is unchanged: the full raw dependency list, unfiltered.
CORS
CORS is handled by a single middleware (v0.27.1). With api.cors_origins
unset the API returns Access-Control-Allow-Origin: * (compatible with the
web GUI’s cross-port fetches); once configured it becomes a strict
allowlist — set it in production deployments, listing the aveloxis-gui
origin (and the web GUI origin if its pages fetch the API cross-port).
Deployment
The API server is stateless — it reads directly from PostgreSQL. You can run multiple instances behind a load balancer for high availability.
# Typical 3-process deployment
(nohup aveloxis serve --workers 40 --monitor :5555 >> aveloxis.log &)
(nohup aveloxis web >> web.log &)
(nohup aveloxis api --addr :8383 >> api.log &)
The web GUI’s Chart.js visualizations fetch data from the API server. The API URL is configured as http://localhost:8383 by default. If running on a different host or port, update the API base URL in the web templates.
Comparison analytics (v0.27.2)
See docs/guide/metrics.md for the metric definitions (“Improvements
on CHAOSS metrics”). Entities: repo:<id> or org:<host>/<login>
(≤7 per request; an org is the union of its tracked repos, capped at
500). All entities are validated against the caller’s §2b scope.
Out-of-scope selections auto-add (v0.27.14). When an
authenticated non-admin selects a COLLECTED entity entirely outside
their groups, the request no longer dead-ends in a 403: the entity’s
already-collected repos are added to the user’s implicit
“Comparisons” group (created on first use, normal v0.19.0 status
rules — the same pattern as the v0.27.4 “Starred” group), the request
proceeds, and the response carries a one-time
added_to_group: [{entity, group}] notice for the GUI toast
(responses carrying it are never cached). Org entities add only the
org’s already-collected repo set (≤500) — org TRACKING is never
registered, so the flow can never enqueue new collection; approval
continues to gate collection, not visibility. The structured 403
(entity_out_of_scope) remains for entities that resolve to nothing
collected.
Comparisons is a persistent record (v0.27.86). Every
authenticated compare — admins and in-scope selections included —
silently links its REPO entities into the caller’s “Comparisons”
group, so the group is the standing record of what the user has
compared (before this, only the out-of-scope auto-add above wrote to
it, and admin comparisons left no trace). Repo entities only: org
entities are never recorded (an org expands to up to 500 repos).
Idempotent, best-effort (a record failure never fails the compare),
and notice-free — added_to_group stays reserved for the
scope-granting auto-add.
GET /api/v1/metrics— the metric catalog (docs-as-data; drives the GUI’s popovers and reference page).GET /api/v1/compare?entities=repo:1,org:github.com/chaoss&metric=contributors&since=2023-07-01&until=2026-07-01&bucket=week— temporal metrics; window defaults to the trailing 3 years; bucket week (default) or month; buckets are densified (aligned x-axes). Responses are cached 60s per (user, query).Per-entity window clamp (v0.27.24): each entity’s series is densified from
max(since, its first activity)— the least of first issue, first PR, first commit, and the forge’s repo creation date — so young repositories chart from when their data starts instead of padding zeros back to the window start. Each series entry carriesdata_start(YYYY-MM-DD, omitted when the entity has no dateable activity). Series in one response may therefore have different lengths: consumers must align them by bucket value, not array position. The clamp is per-entity, not per-metric — a repo whose issues begin a year after its commits shows that year as real flat zeros.metric=contributor_retention(v0.27.16) additionally acceptsretention_threshold=N(N ≥ 1, default 4 — 8Knot’s “Contributions Required” default): contributors with ≥ N total contributions across all collected history are “repeat”, the rest “drive-by”, bucketed by the month/week of their FIRST contribution. Each series in the response carriespoints(per-bucket total) plusparts.drive_byandparts.repeatcomponent series — the only multi-series temporal metric.
GET /api/v1/compare/snapshot?entities=...&metric=labor_investment— snapshot metrics (labor_investment, upstream_dependencies, license_coverage) with per-entity value + as_of + detail.GET /api/v1/entities/search?q=augur— picker results in three classes:in_scope(chartable now),collected(one click to add),uncollected(submit a collection request).
Authentication: getting an API token
When api.require_auth is true, every data endpoint (everything
except GET /api/v1/health and GET /api/v1/public/stats) requires a session token sent as
Authorization: Bearer <token>. Exempt-CIDR clients (same box / LAN
by default) bypass this for the read endpoints; the portal endpoints
above always require a token regardless.
Using the web GUI: nothing to do — the login page exchanges your OAuth session for a token automatically and stores it in the browser.
For scripts, curl, or notebooks, mint a token through the same exchange:
Sign in to the web GUI (GitHub or GitLab OAuth) in your browser.
In the same browser, visit
https://<your-site>/auth/token. You get JSON back:{"token": "64-hex-chars...", "expires_at": "2026-08-13T10:00:00Z", "user_id": 3, "login": "yourlogin"}
Copy the
tokenvalue and send it on every request:curl -H "Authorization: Bearer $TOKEN" \ "https://<your-site>/api/v1/compare?entities=repo:42&metric=contributors"
Token semantics:
Tokens are DB-backed and live 30 days — they survive server restarts. An expired or unknown token gets a structured 401; sign in again and mint a new one.
Each visit to
/auth/tokenmints a new token; existing tokens keep working until they expire, so long-running scripts aren’t cut off when you log in elsewhere.Your token carries your repository scope — every repo in any of your groups (pending included; approval gates new collection, not visibility of collected data). Administrators are unscoped.
Shared links (v0.27.82): requesting a repo outside your groups auto-adds it to your implicit “Shared with Me” group and the request proceeds — repo links are shareable between signed-in users (the Starred/Comparisons auto-add pattern, triggered by viewing). The response that performed the add carries a one-time
X-Aveloxis-Added-To-Group: Shared with Meheader (CORS-exposed) so GUIs can toast it; subsequent requests are ordinary in-scope reads. The auto-add LINKS the existing repo only — it never enqueues collection, never registers org tracking (the v0.27.20 approval principle: approval gates collection, never visibility). Repo IDs with no repos row still return the structured 403 (repo_out_of_scope), as do requests when the auto-add cannot be performed (fail closed).Treat the token like a password. There is no self-service revoke endpoint yet; operators can delete rows from
aveloxis_ops.user_session_tokensto revoke immediately.
Vulnerabilities and home tab (v0.27.4)
GET /api/v1/repos/{repoID}/vulnerabilities— every finding ever recorded for the repo, most critical first, CURRENT findings before resolved-historical ones. Rows are never deleted: when a complete scan stops reporting a finding it is stampedresolved_at(dependency upgraded past it, or dropped) and kept as historical record; a finding that reappears is un-resolved automatically. Each row carriesadvisory_url(osv.dev — resolves every id) andcve_url(app.opencve.io, only when a CVE id exists), plusfirst_detected_at/last_seen_at/resolved_at(absent = currently affected). The envelope’scountsobject hascurrent,resolved, andcritical(current-only), plus — v0.27.21 Phase C1 —direct,transitive, anddev(all current-only; pre-C1 rows count as direct), and — v0.27.46 —runtime(current findings on runtime-scope dependencies:current - dev, the headline GUIs should lead with).Transitive findings (v0.27.21 Phase C1). With
collection.vuln_scan_transitiveenabled, findings from the full lockfile closure carrydependency_kind: "transitive"(direct declarations carry"direct";""= pre-C1 row that heals on the repo’s next scan) and adependency_scopewhen the lockfile flags the entry’s scope. Since v0.27.46 DIRECT findings carrydependency_scopetoo, stamped from the manifest’s own scope (dev/test/build/optional/peer;"runtime"explicit since v0.27.51 — legacy""rows read as runtime and heal to the word on each repo’s next scan). GUIs should lead with direct findings — a repo with 3 direct and 400 transitive findings must never headline “403 vulnerabilities”.introduced_by(v0.27.133, CURRENT transitive findings only): up to 3 attributions{"root": "<direct dep>", "chain": ["<root>", ..., "<vulnerable package>"]}walked from the stored lockfile edges. Each walk stays inside ONE lockfile’s graph (v0.27.135) and roots on that lockfile’s own direct dependencies plus the repo’s manifest-declared set as a repo-wide fallback (v0.27.137) — in a monorepo, a package that is direct in one workspace never terminates another workspace’s chain. Resolved-historical findings never carry the field (today’s graph cannot explain an older snapshot’s finding). ABSENT when no edge data exists (knob off, or an edge-less lockfile format) — treat absence as “attribution unavailable”, never as “no parents”.introduced_by_total_roots(v0.27.148): the TRUE count of distinct direct roots pulling the package in.introduced_bycaps at 3 emitted chains; this field is how a consumer distinguishes “exactly 3 roots” from “30 roots, showing 3”. Whenintroduced_by_total_roots == len(introduced_by)the attribution set is complete; when it is larger, bumping the named roots alone may not remove the package. Omitted (0) when no chain resolved.Version-resolution accuracy (v0.27.11). Each finding also carries
declared_requirement— the raw manifest requirement string (apache-airflow>=3.0.0) — andversion_resolution, how the scanned version was chosen:version_resolutionMeaning
lockedA committed lockfile resolved this package — the purl is the LOCKED version, not the range floor. Go dependencies are
lockedby construction (go.mod versions are exact under MVS).exact==Xor a bare version: the manifest names exactly one version.bounded-rangeThe requirement has an upper bound (
~=,^,~, or a compound containing</<=). The purl is the range FLOOR.range-floorLower bound only (
>=,>). The purl is the FLOOR — the worst case the declaration permits. UIs should render e.g. “≥2.20 declared — floor shown”.unpinnedNo version declared (produces no findings today).
Both fields are absent (
"") on findings last touched by a pre-v0.27.11 scan and heal on the repo’s next scan.The envelope carries
scanned_at(v0.28.1) — the timestamp of the most recent COMPLETED vulnerability scan.nullmeans the repo has never been scanned; a date on a zero-finding repo means “scanned, clean”. The stamp is written only where OSV was actually consulted or the dependency universe was genuinely empty (v0.28.5): scan errors never stamp, and neither do the two degenerate passes that query nothing — a repo whose dependencies all lack purl mappings, or whose legacy targets were all dropped as malformed. Those staynull(honestly “not yet scanned”) until a real scan runs, so a date + zero findings genuinely means “checked and clean”. Frontends must rendernulldistinctly (e.g. “not yet scanned”), never as a clean result.The envelope also gains
lockfile_certainty, derived at read time:"lockfile_certainty": { "overall": "partial", "ecosystems": [ {"ecosystem": "npm", "lockfile_kind": "package-lock.json", "locked_packages": 14}, {"ecosystem": "go", "lockfile_kind": "go.mod", "locked_packages": 9} ] }
overallisfullwhen every ecosystem that has dependencies also has a committed lockfile (Go counts as covered by construction),partialwhen some do,noneotherwise (including repos with no dependencies at all).requirements.txtis NEVER treated as a lockfile — even fully==-pinned it carries the same ambiguities as any other manifest (its pins classify per-finding asexact, but it does not contribute lockfile certainty).scanned_version(v0.27.14) is the version the scan actually ran against, derived from the purl (everything after the last@; empty when the purl carries no version) — pair it with the version-resolution class when rendering, since a range-declared dependency is scanned at its floor, not necessarily the installed version.GET /api/v1/repos/{repoID}/sbom?vulns=1— the CURRENT SBOM annotated with the repo’s unresolved findings. CycloneDX: native 1.5vulnerabilitiesarray (affects.ref= component purl). SPDX (since v0.27.46): package-level SECURITY/advisoryexternalRefs— the 2.3-conformant vehicle (the old 400 is gone).GET /api/v1/repos/{repoID}/stats— response gains (v0.27.50)archived(the forge’s read-only status, read from the latest repo_info snapshot — the ACCURATE signal, distinct from the internal repos.repo_archived flag) andlast_activity_at(most recent observed commit/issue/PR timestamp; omitted when the repo has no activity). The GUI derives the Archived/Dormant chip and the historical chart window from these. v0.27.84 addslast_collected(omitted when the repo has never completed a collection pass) — the GUI’s signal to render the “queued for first collection” banner instead of misleading zeros. v0.28.1 addsgone_at(omitted unless prelim’s probe got a definitive 404/410 — the repo no longer resolves on its forge; the GUI must suppress the queued banner and render the no-longer-available notice, with gone taking precedence over the archived chip) andmetadata_as_of(the repo_info snapshot date behind the metadata_* counts, so gone repos can date their frozen metadata honestly). Also v0.28.1: for QUEUELESS repos (no collection_queue row — the prelim-dequeued gone cohort) the gathered counts fall back to live row counts instead of fabricated zeros; tracked repos keep the cached-count read.GET /api/v1/repos/{repoID}/licenses— response is now an envelope{"scanned": bool, "licenses": [...]}.scanned=falsemeans the dependency-analysis phase has not recorded anything for this repo yet;scanned=truewith an empty list means the repository declares no dependencies.PUT|DELETE /api/v1/repos/{repoID}/star— star/unstar for the signed-in user (Bearer required unconditionally). Idempotent. Starring a repo outside the caller’s groups auto-adds it to their implicit “Starred” group (created on first use) and the response carriesadded_to_group: "Starred"— approval only ever gates NEW collection, and stars can only target already-collected repos, so no approval is involved. Unstarring never removes the repo from the group (scope stays until the user prunes the group).GET /api/v1/repos/{repoID}/star—{"starred": bool}, the signed-in caller’s current star state for one repo (v0.27.85 — backs the repo page’s star toggle; every other starred signal is list-shaped). Bearer required unconditionally, same posture as PUT/DELETE; starred state is caller-personal, so repo scope does not apply.GET /api/v1/home/repos?limit=50— the home-tab list: the user’s starred repos first (always included), then the most active repos from their own groups over the trailing 90 days (issues + change requests opened). Default limit is 50 (v0.27.14; was 20). There is no cap on the number of repos a user may star — the limit only bounds how many rows the home list returns per request.GET /api/v1/home/new-repos?days=30— the “New Repositories” home feed (v0.27.62):{"days", "fleet": [...], "mine": [...]}where each row is{repo_id, owner, name, org, added_at}, newest-first, up to 200 rows per arm.fleet= repos added inside the window whose owner matches an org registered by an ADMIN user (the curated what’s-new-on-this-instance signal);mine= same, for orgs the caller registered. Orgs whose owning group is rejected surface in neither arm.daysdefaults to 30, capped at 90. Requires a Bearer identity (discovery surface — not repo-scoped, same posture as/repos/search).added_atis the v0.27.60 fleet-entry stamp; rows created before that release carry a last-touch approximation, so the feed is honest-noisy for one window after that deploy. v0.27.84: rows surface only AFTER the repo’s first completed collection pass — a freshly-added, never-collected repo would land users on an all-zeros page, so it stays out of the feed until it has data to show. Known v1 edge: GitLab nested-group paths (group/subgroup) don’t equalrepo_ownerand fall out of the feed.GET /api/v1/repos/{repoID}/scorecard— the current OpenSSF Scorecard results for the repo:{"repo_id", "scanned", "as_of", "overall", "checks": [{"name", "score"}]}.overallis scorecard’s aggregate headline score (one decimal); it is absent for repos whose last scan predates v0.27.4 and fills in on the next scheduled scorecard run. Check scores are 0–10 as reported by scorecard;-1means the check did not apply or was inconclusive (render as N/A, not as a failure).scanned=falsemeans scorecard has never run for this repository.
Portal and admin endpoints (v0.27.3)
These back the aveloxis-gui portal pages (group / monitor /
pending-groups / users). Unlike the read endpoints above — which are
gated by api.require_auth during rollout — every endpoint here
requires a valid Authorization: Bearer token unconditionally,
even from exempt-LAN addresses and while require_auth is off. They
carry user context (whose groups? who approves?) that cannot exist
without an identity. The /api/v1/admin/* routes additionally
require the caller’s user to be an administrator (403 otherwise).
Per-user:
GET /api/v1/me—{user_id, login, name, avatar_url, is_admin, scope_repo_count}(loginadded v0.27.77;name+avatar_urladded v0.27.84 — the home greeting and nav avatar render the REAL signed-in identity, stored from OAuth and refreshed on every login).scope_repo_countis-1for admins (unscoped). The GUI usesis_adminto decide whether to render the admin navigation.GET /api/v1/groups— the caller’s groups:{groups: [{group_id, name, status, repo_count, favorited, pending_adds}]}.statusisapproved,pending, orrejected(empty legacy values normalize toapproved).pending_adds(v0.27.84) counts the group’s pending addition requests — the GUI renders “N additions awaiting approval” from it, since under the v0.27.20 model the GROUP is approved while its ADDITIONS may pend.POST /api/v1/groupswith{"name": "..."}— create a group. Groups are createdapproved(v0.27.20 — the approval unit is the ADDITION of not-yet-collected repos/orgs, never the group container). Returns{group_id}.GET /api/v1/groups/{groupID}/repos?page=1&page_size=50&sort=name&dir=asc— one page of the group’s repos in a pagination envelope:{repos: [...], total, page, page_size, sort, dir}(page_sizedefaults to 50, capped at 100). v0.27.75: the row shape and sort semantics are IDENTICAL to the collection detail listing. Each repo row is{repo_id, owner, name, git_url, issues, prs, commits, last_collected?, last_activity?, starred}— the counts are the collection queue’s cached cumulative gathered totals (never a per-request aggregation),last_activityis the forge’s ownlast_updatedfrom the latest repo_info snapshot, andstarredreflects the calling user’s star state.sortacceptsname(default),issues,prs,commits,collected,activity;diracceptsasc(default) ordesc; unknown values fall back and the response echoes the effective sort. Non-admins may only read their own groups (403 otherwise).POST /api/v1/groups/{groupID}/reposwith{"url": "https://github.com/owner/repo", "kind": "repo"}(or"kind": "org"with an org URL) — add a repo or track an org. This is the “request access / request collection” affordance the compare picker’s three-class results point at: already-collected repos link instantly; new repos in a pending group wait for admin approval before collection starts. Bulk paste (2026-07-21): the body also accepts{"urls": ["...", "..."], "kind": "repo"}— every URL lands in ONE batched add (one approval unit per v0.27.20).urlswins when both fields are present; the single-urlbody remains accepted. The response carriessubmittedplus the outcome counts{linked, enqueued, pending_approval?, request_id?, registered?}. Org outcomes (v0.27.84):registered: 1means the org is tracked NOW — admin adds always register, and a non-admin’s add of an org that is ALREADY registered in any group auto-approves (it adds zero new collection: the org’s repos are already tracked and its future repos already auto-enqueue via the existing registration; an auto-approved audit row is still recorded). A non-admin’s add of a NEW org returnspending_approval— registering a new org is an open-ended future-repo commitment, so it awaits admin review. Orgs stay one per request — a multi-URLkind: "org"body is a 400.GET /api/v1/groups/{groupID}/orgs— the organizations tracked in the group (2026-07-21; read-only — registration goes through the POST above withkind: "org"). Envelope:{orgs: [{org_request_id, url, name, platform, last_scanned?}]}.last_scannedis omitted until the scheduler’s org scan first visits the org. Ownership-checked for non-admins (403 otherwise).
Admin-only:
GET /api/v1/admin/users—{users: [{user_id, login, email, provider, is_admin, created_at, last_seen}]}. Since v0.27.89created_atis the REAL join date (INSERT-onlyusers.created_at) andlast_seenisdata_collection_date, which every login re-stamps. Rows that predate v0.27.89 carry the migration’s honest last-touch approximation as their join date (the table had no earlier timestamp to backfill from), so for those two fields start equal and diverge from the next login onward.POST /api/v1/admin/users/{userID}/adminwith{"admin": true|false}— promote/demote. Self-demotion is refused (last-admin guard).GET /api/v1/admin/groups/pending— pending groups awaiting approval, with requester login/email and repo/org counts.POST /api/v1/admin/groups/{groupID}/{decision}where decision isapproveorreject— decide a pending group. Approval bulk-enqueues the group’s repos for collection (same machinery as the server-rendered admin UI). Legacy: new groups always create approved since v0.27.20; this route remains for pre-conversion pending groups.GET /api/v1/groups/{groupID}/pending-adds— the group’s own awaiting-approval content (v0.27.20): repo URLs from pending add-requests plus pending org registrations, each withrequest_id,kind,url,created_at. Ownership-checked for non-admins. Envelope:{pending: [...]}.GET /api/v1/admin/add-requests— the v0.27.20 per-add approval queue: pending additions of not-yet-collected repos/orgs by non-admins. Each entry carries the requester (user_login,user_email), group,kind(repos|org),item_count, up to 10sample_urls(ororg_urlfor org requests), andcreated_at. Envelope:{pending: [...]}.POST /api/v1/admin/add-requests/{requestID}/{decision}where decision isapproveorreject— decide one add-request. Approving areposrequest creates + enqueues + links each item in the background (resumable — re-approving picks up unprocessed items); approving anorgrequest registers the org for tracking (the scheduler’s next org-scan tick collects its repos). The requester is notified by email when a mailer is configured. Response:{ok: true, changed: bool}—changed=falsemeans the request was already decided (idempotent double-click).GET /api/v1/admin/monitor/stats—{queue: {status: count}}.GET /api/v1/admin/monitor/queue?page=1&q=augur— the collection queue, 100 rows per page, optional search. Each job carries the repo label (owner/name), status, priority, due_at, last_collected, last_error, gathered issue/PR/commit counts, AND the forge-reported meta counts (meta_issues,meta_prs,meta_commitsfrom the latest repo_info snapshot) for gathered-vs-metadata comparison.POST /api/v1/admin/monitor/queue/{repoID}/prioritize— the SPA monitor’s “Boost” button (v0.27.14). Pushes the repo to priority 0, makes it immediately due, and resets its status toqueued— the exact samePrioritizeRepostore call the legacy :5555 monitor’s/api/prioritize/{repoID}makes. 404 when the repo has no queue row; 400 for a non-numeric id. Returns{ok: true, repo_id}.
Collections (v0.27.63)
Admin-curated groups-of-groups for the GUI home page (“Apache
Graduated”, “CNCF Sandbox”, …). A collection joins to LIVE
user_groups — never a frozen repo list — so admin org-groups that
auto-grow via org scans keep their collections fresh for free. Only
admin-owned groups may be members (link-time check). Mutations are
POST-everywhere because the CORS Allow-Methods header only
advertises GET/POST.
Reads (any authenticated user):
GET /api/v1/collections—{collections: [{collection_id, name, description, position, groups, repos, starred, created_at}]}, repo counts deduped across member groups. Ordering (v0.27.70): the CALLER’s starred collections first, then position, then name.starredreflects the calling user only — stars are a per-user sort preference, never a visibility control (every collection is visible to every authenticated user).PUT /api/v1/collections/{collectionID}/star/DELETE .../star— star / unstar the collection for the signed-in user (mirrors the repo-star routes). Both idempotent; PUT on a nonexistent collection returns 404. Response{ok, starred}.GET /api/v1/collections/{collectionID}?page&page_size&sort&dir— member groups (each with live repo count) + one page of the deduped repo set:{collection_id, groups, repos, total, page, page_size, sort, dir}. Page size defaults 50, caps 100. v0.27.74: rows carryissues,prs,commits(the queue’s CACHED cumulative counts — the monitor’s numbers, never per-request aggregation),last_collected,last_activity(the FORGE’s own last_updated from the latest repo_info snapshot — deliberately not a live per-table MAX, keeping page renders cheap at 3K-repo collection scale), and the calling user’sstarredflag.sort∈ name | issues | prs | commits | collected | activity (allowlisted server-side; unknown falls back to name),dir∈ asc | desc; repo_id is always the stable tiebreaker. The envelope echoes the EFFECTIVE sort/dir.POST /api/v1/collections/{collectionID}/copywith{group_id}(must be the caller’s) or{group_name}(find-or-create for the caller) — links every repo of the collection into that group and returns{added, group_id}(repos already in the group no-op). Never enqueues collection and never creates add-requests: collection repos are already tracked, and approval only ever gates NEW collection (v0.27.20). The caller’s auth scope and home cache are invalidated so the repos are visible on the next request.
Admin mutations (requireAdmin):
POST /api/v1/admin/collections{name, description, position}→{collection_id}. Name is globally UNIQUE.POST /api/v1/admin/collections/{collectionID}{name, description, position}— update in place.POST /api/v1/admin/collections/{collectionID}/delete— removes the collection; member links cascade, member groups are untouched.POST /api/v1/admin/collections/{collectionID}/groups{group_id}— link an ADMIN-OWNED group (400 for a group owned by a non-admin; linking an already-linked group is an idempotent ok).POST /api/v1/admin/collections/{collectionID}/groups/{groupID}/remove— unlink; the group and its repos are untouched.
Window-boundary semantics (v0.27.39)
Two deliberate boundary behaviors that differ between endpoint families — documented so cross-referencing them is not mistaken for a data bug:
untilinclusivity differs:/repos/{id}/timeseriestreatsuntilas INCLUSIVE (the named day’s data appears — the handler shifts by +1 day internally);/comparetreats the parsed date’s midnight as EXCLUSIVE. Changing either would break existing consumers, so the divergence is documented rather than converged./compareends at the last COMPLETE bucket (v0.27.39): the in-progress week/month is never served as a data point — pre-fix it made every active repo’s final point droop and painted phantom anomaly dots on trend charts. Request an explicituntilinside a bucket and the series still ends at that bucket’s start.Commit bucketing differs by family (deliberate, v0.27.29): the Augur-compat metric routes bucket commits by the AUTHOR-LOCAL date (
cmt_author_date, Augur’s historical semantic);/compareand/timeseriesbucket by UTC (cmt_author_timestamp AT TIME ZONE 'UTC'). The same commit can land in adjacent days/weeks across the two families at bucket edges; both answers are internally correct.