---
title: Enterprise Data API
description: Read your CodeDD audit and portfolio data from your own BI tools, warehouses, and reporting pipelines
category: Portfolio Analytics
order: 2
---

# Enterprise Data API

The Enterprise Data API gives you programmatic, read-only access to everything CodeDD has analysed for your firm — repository audit results, findings, dependencies, architecture, portfolio-level KPIs and, with Portfolio Monitoring, the monitoring, Insights and CVE data — so you can build your own dashboards, feed a warehouse, or attach CodeDD numbers to an investment committee pack.

It is a **REST API over HTTPS returning JSON**, authenticated with an **organization-scoped API key**. Every data endpoint is a `GET`; nothing in this API can change your data. The numbers are the ones the CodeDD app shows: the app, this API, the in-app AI advisor and the [MCP Server](/documentation/mcp-server) read them through the same code.

**Availability:** the CodeDD **Enterprise** plan. Portfolio Monitoring-only contracts do not include it. The [Portfolio Monitoring endpoints](#portfolio-monitoring-endpoints) also need the Portfolio Monitoring module on the contract.

## Quick start

```bash
# 1. Store your key outside your shell history
export CODEDD_API_KEY="codedd_ent_..."

# 2. Ask the API what your key can do
curl -H "Authorization: Bearer $CODEDD_API_KEY" \
  https://api.codedd.ai/api/v1/enterprise/

# 3. List the portfolio companies the key covers
curl -H "Authorization: Bearer $CODEDD_API_KEY" \
  https://api.codedd.ai/api/v1/enterprise/portcos/
```

## Creating a key

Keys are created by an **administrator of your legal organization** in **Account → Enterprise plan → Data API keys**.

1. Click **Create API key**.
2. Give it the name of the tool that will use it — `Power BI production`, not `test` — so you know what breaks if you revoke it.
3. Choose the **scopes** it needs (see below).
4. Choose a lifetime. Every key expires; the maximum is 365 days.
5. **Copy the key immediately.** It is displayed once and cannot be retrieved again.

A key belongs to the organization, not to the person who created it, so an integration keeps working when that person changes role or leaves. Up to **10 active keys** per organization.

### Scopes

Grant only what the consuming tool needs.

| Scope | Grants |
|-------|--------|
| (any valid key) | The API root, the list of portfolio companies and one portfolio company |
| `read:audits` | Discovery (group audits of a company, group audit metadata, the repository list) and every single-repository endpoint |
| `read:portfolio` | Every group-audit roll-up: KPIs, technical debt, benchmark, estate map, executive summary, category breakdown, development, security findings, DORA, AI-Native, financials, repo activity, supply chain, integration assessment, the AI delivery funnel, innovation commits, contributors, the package catalog, licence coverage and requirement evidence; and per company the KPI history, configuration sections, DORA sync state and settings (see [Settings scope change](#settings-scope-change)) |
| `read:monitoring` | Portfolio Monitoring: the monitoring summary, metric history, Portfolio Insights, CVE coverage, CVE search ("are we affected?"), company alerts and monitoring schedules. Needs the **Portfolio Monitoring** module on your contract. |

`read:audits` does **not** imply `read:portfolio`. A pipeline that only needs per-repository findings should not be able to read the firm-level roll-ups you would present to an investment committee.

`read:monitoring` is a new scope, added on 11 November 2026. No existing key has it: to use these endpoints, create a key with it. It can be selected only when your contract includes Portfolio Monitoring (otherwise **Account → Organization** shows it disabled, with the reason), and creating a key with it is refused otherwise. Every request also checks the module, so a key keeps the scope but is refused (`403 portfolio_monitoring_required`) if the module lapses.

An organization key never sees per-person state: no CVE inbox (seen or dismissed alerts), no personal watch rules, no saved scorecard selections or saved Issue Compass selections, and no alert e-mail preferences. Goals are your organization's defaults (`source: "default"`). The monitoring schedule is read as a fund admin reads it, so it includes what the first audit costs.

### Settings scope change

**Announcement (11 November 2026):** `GET /api/v1/enterprise/portcos/{portco_uuid}/settings/` now requires the `read:portfolio` scope. Until **9 February 2027** (90 days) a key without it is still served, and the response carries:

- `Deprecation: @1794355200` (RFC 9745: the announcement date)
- `Sunset: Tue, 09 Feb 2027 23:59:59 GMT` (RFC 8594: the last day the route is served without the scope)
- `Warning: 299 - "This API key lacks the read:portfolio scope, which /portcos/{portco_uuid}/settings/ requires from 2027-02-10 (403 insufficient_scope). Mint a key with read:portfolio."`
- `Link: </documentation/enterprise-data-api>; rel="deprecation"`

From **10 February 2027** such a key gets `403 insufficient_scope`. Every use without the scope during the grace period is logged, so your CodeDD contact can tell you which keys still need it. To prepare, create a key with `read:portfolio` (or add it on your next rotation) and switch your pipeline to it. The API root lists the route with `"scope": "read:portfolio"` and `"scope_enforced_from": "2027-02-10"`.

This page previously listed PortCo settings under `read:audits`; the route in fact checked no scope at all. It also listed the KPI history under "no scope", while the route has always needed `read:portfolio`.

### If you lose a key

There is no reveal endpoint, by design: a value that can be fetched again can be fetched by the wrong person. **Rotate** the key instead. Rotation issues a replacement immediately and keeps the old key working for **24 hours**, so you can roll the secret forward on your next deploy rather than at the same instant. The replacement keeps the old key's scopes.

### Key management endpoints

The app's key dialog uses these endpoints. They take a signed-in CodeDD session of an organization administrator, never an API key: a credential must not be able to mint its own successor.

| Endpoint | Method | Does |
|----------|--------|------|
| `/api/v1/enterprise/keys/` | `GET` | Lists the organization's keys (prefix, name, scopes, expiry, status) and `available_scopes`. Each scope entry has `requires_module` (`portfolio_monitoring` or `null`) and `available` (whether a key holding it would be served today). |
| `/api/v1/enterprise/keys/create/` | `POST` | Creates a key and returns its value once |
| `/api/v1/enterprise/keys/{key_id}/rotate/` | `POST` | Issues a replacement; the old key keeps working for 24 hours |
| `/api/v1/enterprise/keys/{key_id}/revoke/` | `POST` | Revokes a key at once |

## Authentication

Send the key as a bearer token on every request:

```
Authorization: Bearer codedd_ent_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

The key must not appear in a URL, a query string, or a client-side bundle. It reads your firm's entire audit estate; treat it like a database password and store it in your secret manager.

## Navigating your data

You never have to construct an identifier. Start from the key and walk down:

```
GET /api/v1/enterprise/                                        → what this key can do
GET /api/v1/enterprise/portcos/                                → your portfolio companies
GET /api/v1/enterprise/portcos/{portco_uuid}/group-audits/      → audits run for one company
GET /api/v1/enterprise/group-audits/{group_audit_uuid}/audits/  → repositories in one audit
GET /api/v1/enterprise/audits/{audit_uuid}/scores/               → results for one repository
```

In the tables below, **Tier** is the [rate-limit bucket](#rate-limits) (standard or heavy), **Paged** says whether the endpoint returns `meta.next_cursor` ([Pagination](#pagination)), and **ETag** whether a finished result carries a validator ([Caching with ETags](#caching-with-etags)).

### Discovery endpoints

| Endpoint | Scope | Returns | Tier | Paged | ETag |
|----------|-------|---------|------|-------|------|
| `/api/v1/enterprise/` | any key | The key's name, prefix, scopes and expiry, the rate limits, and the index of every endpoint with its scope | standard | no | no |
| `/api/v1/enterprise/portcos/` | any key | The portfolio companies the key covers | standard | yes | no |
| `/api/v1/enterprise/portcos/{portco_uuid}/` | any key | One portfolio company | standard | no | no |
| `/api/v1/enterprise/portcos/{portco_uuid}/group-audits/` | `read:audits` | The group audits run for one company | standard | yes | no |
| `/api/v1/enterprise/group-audits/{group_audit_uuid}/` | `read:audits` | Group audit metadata and status | standard | no | no |
| `/api/v1/enterprise/group-audits/{group_audit_uuid}/audits/` | `read:audits` | The repository audits of one group audit, with the fields of `/audits/{audit_uuid}/` | standard | yes | no |

### PortCo endpoints

| Endpoint | Scope | Returns | Tier | Paged | ETag |
|----------|-------|---------|------|-------|------|
| `/api/v1/enterprise/portcos/{portco_uuid}/settings/` | `read:portfolio` (enforced from 10 February 2027) | Financial and quality assumptions: `hourly_rate_usd`, `quality_threshold`, `target_coverage`, `min_domain_loc_threshold`, `min_bus_factor`, `workload_threshold_per_dev`, `hourly_rate_source`. Additive: `maturity_stage`, `mask_developer_identities` (`true` when the setting cannot be read: fail closed), `sources` (`company` or `default` per setting, as the app's settings dialog shows), `currency` (the currency the company's money figures are in: `USD` unless the company chose another currency or region; the key `hourly_rate_usd` keeps its name) and `financial_inputs` (the saved Financials inputs; `null` = not set) | standard | no | no |
| `/api/v1/enterprise/portcos/{portco_uuid}/kpi-history/` | `read:portfolio` | Executive KPI values across group audits (`?group_audit_uuids=uuid1,uuid2`, at most 25; default: the 25 most recent). An entry that could not be computed has every KPI `null` with `available: false` and a `reason` (`audit_in_progress`, `audit_failed`, `not_in_org`, `no_repositories` (also a group audit that ended with nothing to analyse), `error`); computed entries have `available: true`, `reason: null`. `group_audits` maps each requested uuid to `{status, is_completed, state}` with `state` `completed`, `running`, `failed`, `empty` (ended with nothing to analyse, see [Group audit endings](#group-audit-endings)) or `removed`; a uuid that is not one of the company's group audits is `reason: not_in_org` with `status` and `state` `null`. A running group audit is never computed from the repositories finished so far. The entries carry values, not a trend: compare two of them only when both group audits covered the same set of repositories (`/group-audits/{group_audit_uuid}/audits/` lists them; `/metric-history/` and `/monitoring-summary/` mark a change of set as `comparable: false` with `scope_change`) | heavy | no | no |
| `/api/v1/enterprise/portcos/{portco_uuid}/config/{section}/` | `read:portfolio` | One configuration section of the company: `settings` (values with their sources `company` / `default`, defaults, currency, next audit date (the monitoring schedule's when there is one, `next_audit_date_source` says which; `org_next_audit_date` is the date set on the company), organization name and status), `financial_inputs`, `comparison_metrics`, `dora_environment`, `developer_identity` (whether names are masked), `package_provenance` (the rules, active and revoked), `remediation` or `telemetry`. Editor- and initiator-only fields (rule authors, identity baskets, the telemetry connection) are never served to a key. An unknown section is `400 invalid_parameter` | standard | no | no |
| `/api/v1/enterprise/portcos/{portco_uuid}/dora-sync/` | `read:portfolio` | The company's delivery-telemetry sync state per repository: provider, `sync_status`, `last_sync_at`, `credential_ok` (the credential seen at the last sync, `false` when no usable credential was seen; `credential_ok_at_last_sync` and `credential_checked_at` repeat it with its check time and are null when the repository never synced; `credential_current` and `git_connection` give the connection state now), history coverage. Read from stored state (no provider is called; never a token or a provider id). `404 data_not_available` when the company has no group audit the key can read | standard | no | no |

### Repository audit endpoints

All require `read:audits`. Every result endpoint answers `409 audit_in_progress` until the repository audit's status is `Audit completed`, and `409 audit_failed` once it ended without results (failed or cancelled); only `/audits/{audit_uuid}/`, `/architecture/` and `/history/` (which lists the repository's other, finished audits) are served while it runs. A result that cannot be read right now on a completed audit answers `503 temporarily_unavailable` with `Retry-After: 15` (retry shortly); it never answers `audit_in_progress` for a finished audit.

| Endpoint | Returns | Tier | Paged | ETag |
|----------|---------|------|-------|------|
| `/api/v1/enterprise/audits/{audit_uuid}/` | Name, status, mode, file count, synthesis date; `lines_of_code` and `audited_lines` (the audited lines, see [Definitions](#definitions)), `whole_repo_code_lines`, `audited_coverage_percent`; `is_completed` (the status is `Audit completed`) and `is_failed` (the audit ended without results: failed, errored, cancelled, deleted or expired) | standard | no | no |
| `/api/v1/enterprise/audits/{audit_uuid}/scores/` | Quality scores across all assessed categories. A score the pipeline does not compute is `null` (`dependencies_score`) | standard | no | yes |
| `/api/v1/enterprise/audits/{audit_uuid}/summaries/` | Full narrative summaries — executive, security, code quality, performance, recommendations. `404 data_not_available` when they were never generated | standard | no | yes |
| `/api/v1/enterprise/audits/{audit_uuid}/flags/` | Every flag raised, with severity, location (repository-relative `file_path`), `category_id`, `owasp`, `kit_type` with its `type_basis`, and the open and original counts and hours (`openFlagCounts`, `openHours`, `originalHours`, `totalFlagCounts`) | heavy | no | yes |
| `/api/v1/enterprise/audits/{audit_uuid}/dependencies/` | Packages, licences, and vulnerabilities. [Versioned](#schema-versions): v1 gives the dependency totals; v2 adds the package catalog (paged with `cursor` / `limit`) and the licence counts as the app's Dependencies tab shows them | heavy | v2 | yes |
| `/api/v1/enterprise/audits/{audit_uuid}/architecture/` | Detected technologies, components, and relationships ([versioned](#schema-versions)) | standard | no | yes |
| `/api/v1/enterprise/audits/{audit_uuid}/development/` | Git-derived development and contribution metrics. [Versioned](#schema-versions): v1 is the development narrative (markdown string); v2 a structured object with git statistics, contributors, test coverage and extension mix (`?time_range=month\|quarter\|year\|all`, v2 only) | standard | no | yes |
| `/api/v1/enterprise/audits/{audit_uuid}/files/` | File statistics, extension mix, and test coverage | heavy | no | yes |
| `/api/v1/enterprise/audits/{audit_uuid}/executive/` | Executive dashboard payload (`?time_range=month\|quarter\|year\|all`) | standard | no | yes |
| `/api/v1/enterprise/audits/{audit_uuid}/complexity/` | Cyclomatic complexity and Halstead grade distribution, with `statistics_basis` (`functions`, `stored_summary`, `file_averages` or `null` when nothing was analysed) | standard | no | yes |
| `/api/v1/enterprise/audits/{audit_uuid}/tier-benchmark/` | Tier-matched peer cohort comparison. `cohort_status` (`ok`, `too_small`, `no_tier`, `not_classified`), `subject_scores` (also without a tier) and `score_key_map` (each cohort key with the repository score it compares and its label). Additive fields: the repository's tier as CodeDD shows it (`archetype_tier_effective`), its certainty (`archetype_tier_certainty`: `exact` or `at_least`, a lower bound) and method (`archetype_tier_basis`: `requirement_ladder` or `legacy_centroid`), and `cohort_tier_basis` (`legacy_centroid`: peers are matched on the previous method's tier, `archetype_primary_tier`) | standard | no | yes |
| `/api/v1/enterprise/audits/{audit_uuid}/ai-authorship/` | Human vs AI authorship attribution | standard | no | yes |
| `/api/v1/enterprise/audits/{audit_uuid}/ml-roles/` | Own machine-learning code vs AI-integration code (rubric `mlr1`): lines per role and the top files. [Capability-served](#capability-served-endpoints) | standard | no | no |
| `/api/v1/enterprise/audits/{audit_uuid}/history/` | The other finished audits of the same repository in your organization, with their scores and synthesis dates. Unfinished audits are never listed (`unfinished_excluded` counts them). [Capability-served](#capability-served-endpoints) | heavy | no | no |

`ml-roles` and `history` are wrapped as `data` = `{audit: {...header}, sections: {<section>: {available, data | reason}}}`.

Unlike the in-app AI advisor, these return **complete payloads** — no truncated narratives, no "top 5 findings only". The versioned resources are the exception to "payload as the dashboard sees it": they have a stable, versioned shape of their own (see [Schema versions](#schema-versions)).

### Portfolio endpoints

All require `read:portfolio` and are addressed by group audit; CodeDD derives the portfolio company for you. None sends an `ETag`.

While a group audit runs, these endpoints answer as the app's dashboard shows it. Before it starts analysing (`Opening`, `Group audit ready`) every group resource except the executive summary answers `409 audit_in_progress`. While it runs, `financials`, `integration-assessment`, `estate-map`, the KPIs and `innovation-commits` answer `409 audit_in_progress` (the app hides them until completion); `benchmark`, `technical-debt`, `category-breakdown`, `development`, `dora`, `ai-native`, `ai-funnel`, `repo-activity`, `supply-chain`, `contributors` and `security-findings` are served from the repositories analysed so far, with `meta.partial: true`, `meta.group_audit_state` (`in_progress`, or `failed` for a group audit that ended without completing) and `meta.note`. A completed group audit has `meta.partial: false`.

| Endpoint | Returns | Tier | Paged |
|----------|---------|------|-------|
| `/api/v1/enterprise/group-audits/{group_audit_uuid}/kpis/{kpi_name}/` | One KPI: `technical-debt`, `key-person`, `innovation`, `ip-security`, `scalability`. `409 audit_in_progress` until the group audit's status is `Group audit completed` (no partial KPIs from the repositories finished so far); `404 data_not_available` when it has no analysable repositories | heavy | no |
| `/api/v1/enterprise/group-audits/{group_audit_uuid}/technical-debt/` | Full technical debt dashboard with financial impact (`summary.overallDebtScore`: see [Definitions](#definitions)) | heavy | no |
| `/api/v1/enterprise/group-audits/{group_audit_uuid}/benchmark/` | Benchmark comparison. `pulse_overlay` lists, for the latest monthly pulse over this group audit, each metric's audit value next to its pulse value (`critical_high_cves`, `innovation_rate_pct`, `ai_authored_pct`, `dora_tier`; `null` without a pulse or without Portfolio Monitoring). The category key `scalability` is labelled "Performance & scalability": it is the repositories' Performance chapter score, not the Scalability KPI | heavy | no |
| `/api/v1/enterprise/group-audits/{group_audit_uuid}/estate-map/` | Estate map of repositories and technologies ([versioned](#schema-versions)) | heavy | no |
| `/api/v1/enterprise/group-audits/{group_audit_uuid}/executive-summary/` | Portfolio-level narrative ([versioned](#schema-versions); v2 adds its provenance) | heavy | no |
| `/api/v1/enterprise/group-audits/{group_audit_uuid}/category-breakdown/` | Subcategory score breakdown (`?category=quality`) | heavy | no |
| `/api/v1/enterprise/group-audits/{group_audit_uuid}/development/` | Portfolio development overview | heavy | no |
| `/api/v1/enterprise/group-audits/{group_audit_uuid}/security-findings/` | Issue Compass findings, as the app's explorer lists them (see [Security findings](#security-findings)) | heavy | yes |
| `/api/v1/enterprise/group-audits/{group_audit_uuid}/security-findings/{finding_id}/` | One Issue Compass finding in full (`finding_id` is a row `id` from the list; optional `?audit_uuid=` and `?source=llm\|sonar` narrow the match) | heavy | no |
| `/api/v1/enterprise/group-audits/{group_audit_uuid}/dora/` | DORA four-key metrics and trends; `classification_summary_90` next to the 30-day classification | heavy | no |
| `/api/v1/enterprise/group-audits/{group_audit_uuid}/dora/weeks/{week_start}/` | The records behind one weekly DORA point: the deploys, changes or incidents of the ISO week containing `week_start` (YYYY-MM-DD). `?metric=` is required: `deployment_frequency`, `lead_time`, `change_failure_rate`, `deployment_rework_rate` or `time_to_restore`. [Capability-served](#capability-served-endpoints) | standard | no |
| `/api/v1/enterprise/group-audits/{group_audit_uuid}/ai-native/` | AI-Native portfolio assessment | heavy | no |
| `/api/v1/enterprise/group-audits/{group_audit_uuid}/ai-funnel/` | The AI delivery funnel of the AI-Native tab: AI-authored commits → reviewed → merged via PR → shipped to production, the shipping strip, the agents writing code and the unknown share. Optional `window_days` = `30`, `90` (default), `180` or `365`; `repositories` (comma-separated names, substring) narrows the per-repository rows. Only verifiable AI authorship counts, so the figures are a floor. [Capability-served](#capability-served-endpoints) | heavy | no |
| `/api/v1/enterprise/group-audits/{group_audit_uuid}/innovation-commits/` | The commits behind the innovation-rate KPI, as JSON (the CSV export stays an in-app download). Filters: `classification` (`new_feature`, `bug_fix`, `refactoring`, `maintenance`, `unplanned`), `date_from` / `date_to` (YYYY-MM-DD), `repositories`, `window` (`recent` = the KPI's last two years, default, or `lifetime`); `limit` 1-100 (default 50). `409 audit_in_progress` while the group audit runs. [Capability-served](#capability-served-endpoints) | heavy | yes |
| `/api/v1/enterprise/group-audits/{group_audit_uuid}/contributors/` | Contributor search: commits, domains, activity, repositories, innovation share. `q` = words of the display name as the app shows it (with masking on, the alias such as `dev 3`); `limit` 1-25 (default 10). `report.developer_identities_masked` says whether names are aliases. Never an e-mail address. [Capability-served](#capability-served-endpoints) | heavy | no |
| `/api/v1/enterprise/group-audits/{group_audit_uuid}/financials/` | Dollar impact modeling (`?key_person_scope=audited\|material\|root`, default `audited`) | heavy | no |
| `/api/v1/enterprise/group-audits/{group_audit_uuid}/repo-activity/` | Repository activity compass | heavy | no |
| `/api/v1/enterprise/group-audits/{group_audit_uuid}/supply-chain/` | Package-level licence and vulnerability drilldown (the frozen pre-provenance licence view; for package rows with display names, advisory facts and current licence categories use `/packages/`) | heavy | no |
| `/api/v1/enterprise/group-audits/{group_audit_uuid}/packages/` | The PortCo-wide package catalog: one row per (ecosystem, name) with the `repositories[]` that use it, licence and licence category, `vulnerabilities[]` {`id`, `severity`, `severity_basis` (`advisory`, `feed`, `audit` or `null`), `rated`, `withdrawn`, `fixed_in`, `kev`, `epss`} and `severity_unattributed`, `is_direct` (declared scope), `imported_in_code` and `scope_basis` (`declared`, `imported` or `unknown`), riskiest first. `overview` carries the catalog totals (with `packages_by_import_basis` and `packages_by_license_scope`), `repositories_read` / `repositories_not_finished` / `repositories_failed`, `cve_headline` (direct is the scored value, transitive next to it; `count_unit` and `distinct_advisories`, see [Definitions](#definitions)) and `license_coverage`. Filters `vulnerable_only=true`, `license_risk` (`copyleft`, `strong_copyleft`, `weak_copyleft`, `unknown`; only the third-party rows the licence totals count), `q` (part of a name); `limit` 1-50 (default 25). Finished repositories only; `409 audit_in_progress` while the group audit runs. [Capability-served](#capability-served-endpoints) | heavy | yes |
| `/api/v1/enterprise/group-audits/{group_audit_uuid}/license-coverage/` | Licence coverage of the finished repositories: third-party packages by licence category, identified vs unknown with the unknown reasons, company-owned packages excluded from third-party licence risk, coverage %. Every figure is `null` (never `0`) when no repository has a coverage report. `409 audit_in_progress` while the group audit runs; `404 data_not_available` when the group audit has no dependency data. [Capability-served](#capability-served-endpoints) | heavy | no |
| `/api/v1/enterprise/group-audits/{group_audit_uuid}/integration-assessment/` | M&A/PMI language × domain assessment | heavy | no |
| `/api/v1/enterprise/group-audits/{group_audit_uuid}/requirement-evidence/` | The scale-readiness requirement checklist: every requirement with its status (`met`, `partial`, `missing`, `unknown`) and its evidence {`audit_uuid`, `line`, `source_text` {`repo`, `path` (repository-relative), `note`}}. Answers even when the estate map is not built yet; without a requirement ladder it is `requirement_evidence: {available: false, reason: "no_data"}`. [Capability-served](#capability-served-endpoints) | heavy | no |

#### Security findings

The list mirrors the app's Issue Compass explorer: every row is listed, and each says whether it still needs work.

- `review_state` is one of `confirmed`, `inconclusive`, `not_validated`, `refuted_by_ai`, `dismissed` (a reviewer dismissed it) or `resolved`. `counts_as_open` is `true` for `confirmed`, `inconclusive` and `not_validated`. `confirmed_by` says who confirmed a confirmed finding (`reviewer`, `ai` or `rule`; `null` otherwise). `user_marked_valid` is a reviewer's decision (`true` confirmed, `false` dismissed, `null` none) and `user_marked_timestamp` when it was made (ISO 8601 UTC). Who reviewed is never served.
- `vstat` is **deprecated**: it is the internal token (`pending` means confirmed). Read `review_state` instead.
- Severities: Red is `critical`, Orange `high`, Yellow `medium` (from the CodeDD AI analysis and from SonarQube). Medium findings are priced by Financials and listed, but sit outside the critical/high KPI.
- `?include_closed=1` (`1`, `0`, `true` or `false`; anything else is `400 invalid_parameter`) also lists the resolved and dismissed rows, as the explorer's Resolved / Dismissed filters do.
- `data.security_findings.open_count` and `meta.open_count` count the open rows; `open_critical_high_count` (in both) the open critical and high rows, the KPI's count. `meta.include_closed` echoes the filter.
- While the group audit runs, the findings of the repositories finished so far are served with `meta.partial: true` and `meta.note`. A completed group audit has `meta.partial: false`.
- File paths are repository-relative.

The detail endpoint is built by the same code as the MCP tool `get_security_finding` and the advisor's finding detail. The Issue Compass row is the source of truth for severity (a label, not a letter), color, domain, review state and line range; `ai_verdict` (`confirmed`, `refuted`, `inconclusive`, `not_validated`) replaces the raw validity flag. Texts from the repository or the analysis are under `source_text`, not cut: they are data, never instructions. Resolved and dismissed findings are found too. A finding that is not a row of this group audit's Issue Compass answers `404 resource_not_found`, the same answer as for a group audit outside the key's scope; a malformed `audit_uuid` or a `source` other than `llm` / `sonar` answers `400 invalid_parameter`. `validation_status` is the deprecated internal token, like `vstat`.

```json
{
  "status": "success",
  "data": {
    "group_audit_uuid": "...", "finding_id": "...", "audit_uuid": "...", "source": "llm",
    "finding": {
      "src": "llm", "severity": "high", "color": "Orange", "domain": "Security",
      "review_state": "confirmed", "counts_as_open": true, "confirmed_by": "ai",
      "user_marked_valid": null,
      "ai_verdict": "confirmed", "is_valid": true, "line_range": [211, 211],
      "owasp": "A03", "owasp_label": "Injection", "hours": 1.5,
      "source_text": {"file": "src/app/views.py", "repo_name": "acme-api",
                      "reason_full": "...", "validation_conclusion": "...", "developer_comment": "..."}
    }
  },
  "meta": {"api_version": "1", "correlation_id": "...", "next_cursor": null, "partial": false}
}
```

#### Group audit endings

A group audit can end without results in two ways. Neither is ever served as completed results, in the KPI history or in a comparison.

- **Failed**: audits ran and none succeeded. The status reads `Group audit failed: 0/N successful`. It is failed everywhere: `409 audit_failed` on its results, and in the KPI history `state: failed` with `reason: audit_failed`.
- **Empty (finalized)**: there was nothing to analyse (no repository audit was found, or every one expired or was deleted). The status reads `Group audit finalized: no repositories analysed (...)`. It has ended, but it is neither failed nor completed: its KPIs answer `404 data_not_available`, its KPI history entry has `state: empty` and `reason: no_repositories`.

Older group audits whose status reads `Group audit completed: 0/N successful`, `Group audit completed: No audits found` or `... expired or deleted` are read the same way.

### Portfolio Monitoring endpoints

Scope `read:monitoring` and the Portfolio Monitoring module. All are [capability-served](#capability-served-endpoints); on per-company routes `meta` also carries `portco_uuid`. None is paged unless stated.

| Endpoint | Returns | Tier |
|----------|---------|------|
| `/api/v1/enterprise/portcos/{portco_uuid}/monitoring-summary/` | The company's Portfolio Monitoring card: the latest assessment, CVEs (`critical/high` of direct dependencies is the scored value, with transitive counts next to it; `scope_basis: imported_in_code`, and `caveat: no_imported_packages` when packages are declared but none is imported; the band then reads `unknown`, not `safe`), the bus factor (`null` with status `unknown` when no domain is large enough to assess), alerts and signals, the company alerts of Portfolio Admin (`company_alerts`: setup and Git problems, as `/company-alerts/`; `needs_admin`, `setup_open`, and the counts in `health`; `null` when they could not be read), scorecard goals (`tracked_metrics`, `goal_status`), audit freshness, the next audit and pulse dates, and the latest pulse. A tracked metric compares the latest audit with the previous one only when both covered the same set of repositories: otherwise it is `comparable: false` and never `worsening` (its goal status still reads the current value), and the company row's `scope_change` says what changed (`added`, `removed`, the names under `source_text`; `null` for the same set). `open_cve_alerts` {critical, high} counts the company's open CVE feed alerts next to the scored CVEs; `assessment.fix_days_basis` names the basis of `est_fix_days`; the schedule carries `latest_run`, `latest_run_stuck` and `overdue_run` (with `overdue_reason`). A company the key can read that is not in Portfolio Monitoring answers 404 `resource_not_found`, as before (the MCP tool and the AI Advisor answer `monitored: false` with `reason: not_monitored` instead) | standard |
| `/api/v1/enterprise/portcos/{portco_uuid}/metric-history/?metric=code_health_score` | Twelve months of one scorecard metric: audit samples and monthly pulses with `as_of`, `value` and `comparable` (`false` marks a basis break, so never compute a trend across it). An audit point is compared only with an audit of the same set of repositories (the repository audits that produced results; a failed one does not count): where a repository was added, removed or swapped, the point is `comparable: false` and carries `scope_change` (`added`, `removed`, and the names under `source_text`). `metric` is required (alias `metric_key`). `critical_high_cves` points carry `scope: "direct"` and `transitive_critical_high` beside the value. Light and Full audits never share a series: `series_audit_mode` names the series' mode and `omitted_other_mode_audits` counts the completed audits of the other mode in the window that are left out | standard |
| `/api/v1/enterprise/portcos/{portco_uuid}/insights/?section=overview` | One Portfolio Insights section for the company, read over your organization (the fund). `section` is required. `pendingCount > 0` means figures are still being computed, so retry in about 15 seconds | heavy |
| `/api/v1/enterprise/portcos/{portco_uuid}/cve-coverage/` | How complete and fresh CVE screening is: screened repositories, last screening, the CVE headline (`scope: "direct"`, `transitive` next to it, `packages_with_vulns` {direct, transitive}), `feed` (the advisory feed's freshness), `count_unit` and `distinct_advisories` (see [Definitions](#definitions)), `portcos[].flags` and `portcos[].caveats`, `packages.distinct_across_repositories`, per repository `screened_at` and `earlier_screening_*`, and `facets.registries[].vulnerable_occurrences`. Use `?include=gaps` (paged with `limit` 1-500 and `cursor`) for unscreened repositories, and `?include=repositories` for every repository. Read the flags before concluding "not affected" | heavy |
| `/api/v1/enterprise/cve-search/?q=CVE-2021-44228` | "Are we affected?" across your companies: one verdict per company (affected, not affected, or unknown because coverage is incomplete; `verdicts[].label` is the wording) and the affected package uses (`groups[].severity_basis` says where the severity comes from: `feed` marks a severity from the OSV feed records). Display-only groups carry `aliases_source` (`advisory` or `feed`) and `feed_alias_group` (the canonical ID, CVE first, that their OSV feed records share; two groups with one `feed_alias_group` are one vulnerability). `q` is an advisory id (`CVE-`, `GHSA-`, `PYSEC-`, `GO-`, `RUSTSEC-`, …) or a package, optionally `name@version`. You can give `cve_id`, or `package` with optional `version` and `ecosystem`, explicitly instead. Optional: `portco` (one of your companies, by uuid), `group=occurrences\|advisories`, `limit` (1-100) and `cursor` (paged) | heavy |
| `/api/v1/enterprise/portcos/{portco_uuid}/company-alerts/` | What needs attention for the company, from the Portfolio Admin alert catalog: each alert's `code`, `level` (`critical`, `warning`, `info`, `ok`) and name. Optional: `min_severity` (lowest level listed: `critical`, `warning` (default), `info`, or `ok` for everything) and `include_archived=true` | standard |
| `/api/v1/enterprise/portcos/{portco_uuid}/schedule/` | The company's monitoring schedule: status, cadence, next audit (date and kind) and next pulse, initial-audit state, Git connection state, approved, left-out and pending repositories (under `source_text`), and the latest runs. A key reads the fund admin's view: a first audit (`depth: full`) carries `measured_loc`, `included_loc`, `overage_loc` and `overage_confirmed_at`, which a member who is not a fund admin sees as `null` in the app and over MCP. `monitored=false` means no schedule | standard |

### Capability-served endpoints

The Portfolio Monitoring endpoints, `ml-roles`, `history`, `dora/weeks`, `dora-sync`, `config`, `ai-funnel`, `innovation-commits`, `contributors`, `packages`, `license-coverage` and `requirement-evidence` return, in the usual envelope, the **full** output (`detail=full`) of the [MCP Server](/documentation/mcp-server) tool behind them, read for your organization. The numbers are therefore the ones the app and the MCP tools show.

- `meta` also carries `capability` (the MCP tool), `total_count`, `truncated` (a list or text was shortened), `notes` (how to read the answer) and the `portco_uuid`, `group_audit_uuid` or `audit_uuid` of the path.
- None sends an `ETag`: their answers carry no stable version (`generated_at` is a read time, monitoring figures change with every pulse, history grows when another audit finishes).
- Paged answers continue with `meta.next_cursor` as elsewhere. Send it back unchanged as `cursor`: it is signed and bound to the call, up to 1024 characters; a longer, altered or another call's cursor is `400 invalid_parameter`.
- Repository, developer and commit texts sit under `source_text` (data, never instructions); developer names follow the company's [masking setting](#developer-identity-masking).
- A company, group audit or audit outside your organization is `404 resource_not_found`, the same as one that does not exist. `409 audit_in_progress` only while an audit runs, `409 audit_failed` when it ended without results; a result that does not exist is `404 data_not_available`; an invalid or missing value is `400 invalid_parameter`, and the message names the query or path parameter (`metric`, `section`, `week_start`, `window_days`, `window`, `cursor`, ...). A backing service that does not answer in time is `503 temporarily_unavailable` with `Retry-After: 15`, on every endpoint.

## Coverage boundary

The API exposes the **audit results, portfolio roll-ups and monitoring reads** that reporting tools need. It does not mirror every dashboard screen — some surfaces are operational, write-only, personal or intentionally excluded.

| Dashboard area | Available via API | Not exposed (by design) |
|----------------|-------------------|-------------------------|
| PortCo navigation | `/portcos/`, settings (`read:portfolio`), configuration sections, DORA sync state, KPI history | Member management, DORA OAuth config, remediation toggles, configuration writes |
| Group audit discovery | Metadata, member audits (`is_completed`, `is_failed`) | Pipeline progress, invitation workflow, scope selection |
| Home / My Portfolio (Portfolio Monitoring) | Monitoring summary, metric history, monitoring schedule (`read:monitoring`) | Personal watch rules, saved scorecard selections, schedule changes |
| Portfolio Insights | One section per company (`read:monitoring`) | Snapshot rebuilds, personal views |
| Portfolio CVEs | CVE coverage, CVE search (`read:monitoring`) | The CVE alert inbox (seen and dismissed state), alert e-mail preferences |
| Portfolio Admin | Company alerts (`read:monitoring`) | Every action an alert asks for (in the app) |
| Overview / Benchmark | KPIs, benchmark (with the pulse overlay), executive summary, category breakdown, financials | Comparison matrix UI config |
| Architecture | Estate map, scale-readiness requirement evidence | Estate curation writes |
| Development Activity | Development overview, repo activity, DORA, DORA week detail, DORA sync state, contributor search, innovation commits (JSON, paged), integration assessment | The innovation commits CSV download (the same rows are served as JSON) |
| Debt & Security | Technical debt, security findings (open and, on request, closed rows), finding detail, supply chain, PortCo-wide package catalog, licence coverage | Saved Issue Compass selections (personal), confirm and dismiss actions |
| AI-Native | AI-Native assessment, AI delivery funnel, per-repo AI authorship, per-repo ML roles | — |
| Single audit — Summary | Scores, summaries, executive, files (aggregates), audit history of the repository | Per-file tree, file search |
| Single audit — Files | Statistics, extension mix, test coverage, complexity | File treetable, file detail drawer, one file's record |
| Remediation | — | Remediation dashboard (module-gated, per user) |
| Admin / ops | — | Audit logs, comparable audits workflow |

The AI delivery funnel and the innovation commits were listed here as not exposed by design; both are now served. The remediation dashboard stays outside the API: it is per user and module-gated.

If you need a dataset that is not listed above, contact your account manager — new endpoints are added when a reporting use case is clear and the underlying data is stable.

## Response format

Every successful response uses the same envelope:

```json
{
  "status": "success",
  "data": { "portcos": [ { "portco_uuid": "...", "name": "Acme GmbH" } ] },
  "meta": {
    "api_version": "1",
    "correlation_id": "3f2b...",
    "next_cursor": null,
    "total_count": 12
  }
}
```

Quote the `correlation_id` when contacting support — it identifies the exact request in our logs.

The five [versioned resources](#schema-versions) also state the schema version of `data` in `meta.schema_version`.

### Null means not measured

A value CodeDD did not compute is `null`, never `0` or `100`: a score that is not scored, a coverage figure without a domain large enough to assess, a quarter without commits, a ratio without a denominator. A `0` means CodeDD counted and found nothing. An unfinished audit never reads as finished: its results answer `409 audit_in_progress`, never values made up from what has been read so far.

### Timestamps

Every timestamp is an ISO-8601 string in UTC ending in `Z` (`2026-11-03T09:15:00Z`, with microseconds when the source has them). A date without a time stays `YYYY-MM-DD`. This applies to the v2 resources and to every [capability-served endpoint](#capability-served-endpoints); the rule is the same one the MCP server applies (fields named `*_at`, `*_date`, `*_timestamp`, `*_as_of`, `timestamp`, `ai_synthesis`, `as_of`, `since` or `…At`). v1-legacy resources and the other unversioned endpoints keep their stored form; where they serve an ISO time (for example `user_marked_timestamp` on the security findings) it is UTC with a `+00:00` offset. Both forms are UTC ISO-8601.

### Developer identity masking

When a portfolio company masks developer identities (its settings: `mask_developer_identities`), every endpoint serves the stable alias `dev N` instead of a developer's name or e-mail address. Masking fails closed: an e-mail address with no alias, and any e-mail address inside free text (a finding, evidence, a commit message, a `Co-authored-by` trailer), is replaced by `dev N` when it belongs to a known developer and by `unknown developer` otherwise. With masking off nothing is replaced.

### Pagination

List endpoints are paginated. Pass `limit` (default 50, maximum 100, unless the endpoint states other bounds) and follow `meta.next_cursor` until it is `null`:

```python
import requests

session = requests.Session()
session.headers['Authorization'] = f'Bearer {API_KEY}'
BASE = 'https://api.codedd.ai/api/v1/enterprise'

def fetch_all(path, key):
    """Every page of a list endpoint."""
    items, cursor = [], None
    while True:
        params = {'limit': 100}
        if cursor:
            params['cursor'] = cursor
        payload = session.get(f'{BASE}{path}', params=params, timeout=30).json()
        items.extend(payload['data'][key])
        cursor = payload['meta']['next_cursor']
        if not cursor:
            return items

portcos = fetch_all('/portcos/', 'portcos')
```

Treat the cursor as opaque. It is not an offset, and building your own would break the moment ordering changes.

### Caching with ETags

Audit results are written once and then do not change, so repeat reads should be conditional. The repository result endpoints marked **ETag** above return an `ETag`; send it back as `If-None-Match` and a `304 Not Modified` costs you nothing:

```bash
curl -H "Authorization: Bearer $CODEDD_API_KEY" \
     -H 'If-None-Match: "a1b2c3d4..."' \
     https://api.codedd.ai/api/v1/enterprise/audits/$AUDIT/scores/
```

A repository audit gets an `ETag` only once its status is `Audit completed`. An audit still being analysed, or one that failed, has none, because its data is still moving or will never exist. Each page of a paged versioned resource (dependencies v2) and each `time_range` of development v2 has its own `ETag`.

The architecture resource can change after an audit completes: when an admin curates the repository's architecture style, CodeDD stores the repository's architecture model later, or its requirement ladder is recomputed. Its `ETag` changes with it, so a conditional request then returns the new representation instead of a `304`. The tier-benchmark resource's `ETag` also follows the requirement ladder and the method the tier is shown by. Portfolio, PortCo, discovery and capability-served endpoints send no `ETag`.

## Definitions

These are the terms whose meaning a pipeline most often gets wrong. The shared [Glossary](/documentation/glossary) defines every term CodeDD uses, the same for the app, this API, the AI advisor and the MCP server.

| Term | Where | Meaning |
|------|-------|---------|
| Code health | Overview card; KPI `technical-debt` | 100 minus the technical-debt KPI's `ratio_percent`. Bands: Good ≥ 67, Fair ≥ 33, Poor < 33. |
| `overallDebtScore` | `/technical-debt/` `summary.overallDebtScore` | The average of the repositories' technical-debt subcategory scores (0-100; unscored repositories left out, `null` when none is scored). It is **not** code health; the key keeps its name. |
| Fix days, `fix_days_basis` | KPI `technical-debt`, `/financials/` | Three labelled figures. `open_red_orange_findings`: the KPI's `total_estimated_fix_days`, the open critical and high findings plus the KPI's other components. `issue_compass_scope`: Financials `technical_debt.total_estimated_fix_days` and `derivations.capex_remediation_fix_days`, over the Issue Compass flag scope (medium included). `all_levers_to_targets` (`effort.total.basis`): the Financials headline effort, every lever to its target. `flag_scope` says which findings were counted. |
| Key-person scope | KPI `key-person` `financial_impact`, `/financials/` `?key_person_scope=` | The one-off scenario of losing the top knowledge holder, sized on `scope` lines: `audited` (default and headline, the same on the KPI and in Financials), `material`, or `root` (the whole repository, served as `financial_impact_root`). |
| Bus factor, `domains_at_risk` | KPI `key-person`, monitoring summary | The bus factor counts people. The KPI's count of material domains at risk is `domains_at_risk`; `bus_factor` on the KPI is a deprecated alias of it with the same value. |
| AI share | Monitoring summary, metric history, Insights | The share of AI-authored lines in the measurable repositories over the 3 complete months before the audit (`ai_authored_pct`). The all-history figure is served beside it as `ai_authored_pct_all_history`. |
| Audited lines, `loc_basis` | `/audits/{audit_uuid}/`, estate map v2 | `audited_lines` (and `lines_of_code`) are the code and documentation lines of the files selected for the audit, the denominator of every per-kLOC figure. `whole_repo_code_lines` counts the code lines of the whole repository; `audited_coverage_percent` is audited / whole, one decimal. `loc_basis` says what a `loc` figure counts: `audited_loc`, `first_party_loc`, `total_loc` (the whole-repository fallback) or `null` (no count). |
| Direct vs transitive CVEs | KPI `ip-security`, `/packages/`, CVE coverage, monitoring | Critical and high CVEs of direct (shipped) dependencies are the scored value (`status_scope` / `scope: "direct"`). Transitive counts are served next to them and never change a status or a goal. A package row's `is_direct` is the declared scope (the manifest lists it); `imported_in_code` (the audited code imports it) is the reading the CVE headline calls direct (`scope_basis`). |
| CVE counts, `count_unit` | `/packages/` `cve_headline`, CVE coverage | The headline's counts are package-advisory pairs (`count_unit: "package_advisory_pairs"`): one advisory two packages carry counts twice. `distinct_advisories` {`direct`, `transitive`} counts each advisory once next to them (`null` until known). |
| Open finding, `counts_as_open` | Security findings, flags | Confirmed, inconclusive or not validated (`review_state`). Counts and hours of open findings come from the same rows (`openFlagCounts`, `openHours`); `totalFlagCounts` and `originalHours` count every flag originally raised. |

## Schema versions

CodeDD's analysis keeps improving. So that an improvement never changes the data your pipeline parses without warning, five resources have a **schema version**:

| Resource | Endpoint | Default without a version |
|----------|----------|---------------------------|
| Repository architecture | `/api/v1/enterprise/audits/{audit_uuid}/architecture/` | v1-legacy until 30 April 2027, then v2 |
| Estate map | `/api/v1/enterprise/group-audits/{group_audit_uuid}/estate-map/` | v1-legacy until 30 April 2027, then v2 |
| Portfolio executive summary | `/api/v1/enterprise/group-audits/{group_audit_uuid}/executive-summary/` | v1-legacy until 30 April 2027, then v2 |
| Repository dependencies | `/api/v1/enterprise/audits/{audit_uuid}/dependencies/` | v1-legacy (no sunset announced; responses carry no `Sunset` header) |
| Repository development | `/api/v1/enterprise/audits/{audit_uuid}/development/` | v1-legacy (no sunset announced; responses carry no `Sunset` header) |

| Version | What it is |
|---------|------------|
| `1` (**v1-legacy**) | The shape these resources had before versions existed, frozen: no field is renamed or removed, and fields CodeDD adds appear only in v2. |
| `2` | A typed shape with explicit fields and no file-system paths. For the architecture resources: what every relationship means and what evidence it rests on, coverage and data quality, one architecture style with its source, the tier with its status. For dependencies: the package catalog; for development: the structured git object. v2 only grows: new fields may be added, existing ones keep their name and meaning. |

All other endpoints are unversioned and ignore the selector.

### Selecting a version

```bash
# Query parameter (or its alias: ?schema_version=v2)
curl -H "Authorization: Bearer $CODEDD_API_KEY" \
  "https://api.codedd.ai/api/v1/enterprise/audits/$AUDIT/architecture/?version=2"

# Or a header
curl -H "Authorization: Bearer $CODEDD_API_KEY" -H "X-CodeDD-Schema-Version: 2" \
  https://api.codedd.ai/api/v1/enterprise/audits/$AUDIT/architecture/
```

- Accepted values are `1` and `2` (`v1`, `v1-legacy` and `v2` work too). The `version` query parameter also accepts the alias `schema_version`, on every versioned resource. Any other value is a `400 invalid_parameter`, as are selectors (the two query parameters and the header) that name different versions.
- The response states the version it carries: in `meta.schema_version` and in the `X-CodeDD-Schema-Version` response header (responses vary on that header). A v1 response of an architecture resource served because the request named no version also carries a `Sunset` header with the date below: it marks the end of v1 as the default. The dependencies and development resources have no sunset and never send one. A request that names `version=1` gets no `Sunset` header, because explicit v1 keeps working after that date.
- Each version has its own `ETag`, so a conditional request never answers with a `304` for the other shape. The `ETag` also changes when the version's output changes for the same audit (v2 gains a field, a value rule is corrected), so a cached copy is never revalidated across such a change. The `ETag`s of the architecture resource changed once when versions were introduced; expect one full re-read.

### Default version and sunset plan

| Period | Request without a version | `version=1` | `version=2` |
|--------|---------------------------|-------------|-------------|
| Until **30 April 2027** | v1-legacy | v1-legacy | v2 |
| From **1 May 2027** | v2 | v1-legacy, until its removal is announced in this document | v2 |

This plan covers the three architecture resources. The dependencies and development resources serve v1-legacy to a request that names no version, with no sunset announced: send `version=2` to get v2.

Send `version=2` now: requests that name a version are unaffected by the default changing.

### What can change in each version

| Version | Fields | Values | Retirement |
|---------|--------|--------|------------|
| v1-legacy | Frozen. No field is added, renamed or removed. | A value the analysis now computes correctly is served corrected, under the same field. Every correction is listed in the [v1-legacy](#v1-legacy) tables below and in [Corrections to unversioned endpoints](#corrections-to-unversioned-endpoints). | The architecture resources' v1 stops being the default on 1 May 2027, as shown above; dependencies and development keep v1 as their default. Explicit `version=1` keeps working until a removal date is announced in this document. Removal is never unannounced. |
| v2 | Grows only. New fields are added and documented in the [v2](#v2) section. Existing fields keep their name, type and meaning. | Corrected like v1. A field that is `null` today may carry a value once the analysis records it. | No sunset is planned. A change that would rename, remove or redefine a field gets a new schema version and is never made in v2. |

Both versions change their `ETag` whenever their output for the same audit changes, whether a field is added or a value rule is corrected. A conditional request therefore never keeps a stale copy. Parse v2 leniently: ignore fields you do not know.

#### Revisions

Each version's output has a revision, part of its `ETag` (`schema-<version>.<revision>`). A new revision means cached representations revalidate once (expect one full re-read). The current revisions are **v1 `5`** and **v2 `10`**.

| Revision | What changed for the same stored data |
|----------|---------------------------------------|
| v1 2, v2 2 | Domain recommendations are drawn from evidence (v1 values change); v2 adds each recommendation's `trigger` |
| v1 3, v2 3 | A domain's aggregate scores follow the code-health rule (v1 values change; its key set is frozen); v2 adds relationships, structured evidence, candidate links, the estate stages and membership, the org consolidation, org domains, deployment platforms, the run outcome, the coverage data source, structural criticality and module dependencies |
| v2 4 | The repository architecture adds `model` (the provenance of its facts) |
| v2 5 | `requirement_ladder_summary` on the estate, each estate repository and the repository architecture |
| v1 4, v2 6 | The dependencies and development resources are versioned: their v1-legacy shapes are documented (v1 output unchanged) and v2 serves the package catalog and the structured development object. The dependencies and development responses now carry `meta.schema_version`, the `X-CodeDD-Schema-Version` header and `Vary: X-CodeDD-Schema-Version` |
| v2 7 | The development git documents are fixed shapes (no copied names or free text outside `source_text`); dependencies `licenses` is `null` when the overview could not be computed |
| v2 8 | Every timestamp field is ISO-8601 UTC ending in `Z` (some were served as stored: a naive time, nanosecond fractions, `+00:00`, a local offset or epoch numbers). Dependencies rows add `package_key`, `vulnerabilities[]` and `vulnerabilities_truncated`. Development answers `500 internal_error` when the test-coverage or extension-mix read fails (it served `null`, which read as "not measured"). The estate map's `org_tier.excluded_repos[]` add `loc_basis`. The security finding detail adds `user_marked_valid` |
| v2 9 | Money states its real currency: the configuration section documents that its money is in the company's Financials `currency` (USD when the company chose none); the Financials resource adds a top-level `currency`, the KPI resource adds `currency` when the company's costs are not in US dollars, and Insights money rows state each company's `currency` (`mixed` on a payload whose companies differ) |
| v1 5 | With masking on, e-mail-only fields (`email`, `developer_email`, `author_email`) are `null` instead of the `dev N` label (review 2026-11-11) |
| v2 10 | Review 2026-11-11 corrections: with masking on, e-mail-only fields are `null`; a dependency vulnerability's `cvss` is `null` when no score is known (was `0`), a severity the advisory store does not know comes from the OSV feed when it rates the advisory, and development releases are newest first |

### v1-legacy

v1-legacy keeps the **fields and nesting** these resources had before versions existed. Fields the analysis has gained since appear only in v2. These include coverage and data quality, edge semantics and relationships, structured evidence and candidate links, detection coverage, the shared stack, dimension evidence states, repository kinds, deployment platforms, the estate stages and membership, the org consolidation, structural criticality, module dependencies, provenance and the requirement ladder. The frozen field set reaches into the documents CodeDD's classifiers store: dimension scores, confidence components, tier gap and ceiling analyses, the repository tier breakdown and the group topology summary keep the fields they had.

Only documents without a fixed schema are served as stored, as before, and may carry whatever the analysis wrote: a component's or relationship's `properties` (the LLM's component document), the `insights_*` blocks, and the detector blocks `coupling_analysis`, `communication_patterns`, `flow_patterns`, `coupling_metrics` and `communication_flows`. Treat their keys as data, not as part of the contract.

The **values** are not frozen: where the analysis now computes a value correctly, v1 carries the corrected value under the same field.

**Repository architecture**

| Field | Correction |
|-------|-----------|
| `archetype_classification.archetype_name`, `tier_reference.*.name`, `tier_gap_analysis.next_tier_name` | Scale tier names (Foundational, Established, Scalable, High-Scale, Hyperscale). A tier describes operational scale, not an architecture style. |
| `archetype_classification.architecture_style` (and `_label`), `executive_overview.service_architecture` | The repository's architecture style decided from evidence, `unknown` when undecided, instead of a guess from the number of components. |
| `archetype_classification.primary_tier`, `archetype_name`, `confidence`, `tier_gap_analysis` | `primary_tier` is `null` when the scale-tier ladder does not apply to the repository (a library, infrastructure code, a command-line tool, ...) or too few of its dimensions have evidence; `archetype_name` then reads `Not applicable` or `Insufficient evidence` and `tier_gap_analysis` names no next tier. A dimension without evidence no longer counts as the lowest tier, and `confidence` is how decisively the evidence places the tier, scaled by the share of dimensions with evidence and the completeness of the analysed inputs. |
| `archetype_classification.dimension_scores.deployment_model`, `tier_gap_analysis.actions.*.recommendation` | The deployment model scores deployment independence and automation (build artefacts, deployment declared as code, automated deploys, environment promotion, zero-downtime and progressive rollouts), not the number of deployable units; platform-managed hosting (PaaS, serverless, ECS) is not scored below Kubernetes. Tier recommendations are fitted to the repository's architecture style, platform and stack and never recommend a capability it already has: no decomposition advice for a single deployable, no Kubernetes advice for a repository on ECS or a PaaS. |
| `executive_overview.key_metrics.coupling_level` | `null` when the component graph cannot support a coupling figure (edge extraction missing or failed, or fewer than half of the source packages represented). |
| `executive_overview.key_metrics.primary_communication` | Derived from the graph's synchronous and asynchronous edges. |
| `executive_overview.critical_dependencies`, `insights_critical_dependencies`, `key_metrics.critical_dependencies_count` | Single points of failure of the component graph (a component that at least 3, and at least 15% of, the graph's components depend on at runtime) instead of keyword matches. The count is `null` when the graph cannot be searched. |
| `confidence_basis` on edges | `corroborated` means the source component's files literally reference the target; edges only consistent with other LLM output are `mentioned`. |
| `dependency_flow.critical_paths.critical_components`, `critical_paths.critical_relationships`, `visualization_data.enhanced_edges[].is_critical` | Structural criticality of the component graph, replacing the LLM's `high` criticality and strength labels. A component is critical when it is an articulation point (removing it cuts at least two components off the rest), has a fan-in at the graph's 90th percentile and at least 3, or is a single point of failure. An edge is critical when it is the only link between part of the graph and the rest (a bridge). Both lists are empty when the graph cannot be assessed (no graph, no relationships). |
| `visualization_data.enhanced_edges[].visual_properties.stroke_style` | Follows the edge's evidence: `solid` for static evidence (an import, a literal reference, a matched pattern), `dashed` for an LLM claim consistent with other analysis, `dotted` for an LLM inference or an edge whose evidence was not recorded. |
| `metadata.analysis_timestamp` | ISO 8601 (UTC) instead of always `null`. |
| `metadata.file_analyses_summary.total_files` | Counts the stored file analyses (it read 0). |
| Every text value | Paths of CodeDD's analysis machines are removed; file references are repository-relative. |

**Estate map**

| Field | Correction |
|-------|-----------|
| `estate_summary.total_cross_repo_edges`, `estate_summary.coupling_density` | Count directed dependency edges between estate repositories only. Shared technology and delivery links (CI, deployment, base images) are not coupling. Edges are matched to repositories by audit, not by name, so the edges of two repositories that share a name are counted. |
| `edges` | New group audits no longer record shared technology (two repositories using PostgreSQL) as an edge; v2 lists it as `shared_stack`. Directed edges read "source depends on target" (`event_dependency` edges are no longer served publisher first), undirected ones list their repositories in name order, an edge served in reverse carries no `is_critical_path`, and evidence that differs only in case or a technology alias is listed once. `strength` follows `confidence` (high from 0.85, medium from 0.6); an edge several independent detectors found combines their confidences (detectors that restate the same repository analysis, such as a declared integration and the producer/consumer pairing built from it, count once). A relation curated under an older type name (`shared_datastore`, `shared_library`, `event_bus`) is served under its current type (`data_dependency`, `library_dependency`, `event_coupling`). New group audits no longer draw an API edge between two repositories only because they share a business domain. |
| `estate_summary.choke_points`, `estate_summary.blast_radius`, `org_archetype.topology_summary` | Only repositories that others depend on through directed edges, with at least medium evidence confidence. A dependency several detectors found counts once in fan-in, choke points and roles; a shared data store adds no fan-in to either repository. A blast radius lists every repository a failure reaches, directly or through other repositories; a delivery link (base image, CI templates, deployment assembly) reaches only the repositories that build or ship with it, not their callers. A choke point is matched to its repository by audit, so a repository that shares its name with another is not dropped. |
| `archetype_name`, `estate_summary.org_archetype_name`, `org_archetype.org_archetype_name`, `tier_ceiling_analysis.next_tier_name` | Scale tier names. |
| `archetype_primary_tier`, `estate_summary.tier_composition`, `estate_summary.org_primary_tier`, `org_archetype.org_primary_tier`, `org_archetype.tier_gap_analysis`, `org_archetype.tier_ceiling_analysis` | A repository without a tier (see the repository architecture) has a `null` tier and is counted under `not_applicable` or `insufficient_evidence` in `tier_composition`. When no repository of the estate has a tier, the estate has none either: a `null` org tier and no tier gap or ceiling. |
| `architecture_style`, `architectural_style`, `estate_summary.style_composition` | The architecture style decided from evidence, as on the repository architecture. |
| `domains[].recommendations` | Drawn from evidence: a critical-path repository of the domain, a score below the estate average (no longer described as a tier-peer comparison), and the largest next-tier gaps of the domain's repositories with recommendations fitted to each repository; no tier templates. A repository whose tier is shown by the requirement ladder gets no next-tier gap advice (what lifts its tier is the ladder's list of items to build or confirm). v2 adds the `trigger` each one cites. |
| `edges` (integration edges) | New group audits record an `api_dependency` or `event_coupling` edge when a repository's runtime configuration or code references something another estate repository publishes, such as a configured endpoint URL or host, a service name, or a topic or queue. The edge's `evidence` names the reference, the file it was read from and the identity it matched. A match too weak to draw is not an edge. v2 lists it under `relationship_candidates` with its reason. |
| `edges`, `estate_summary`, `org_archetype.topology_summary` (relationships) | One coupling is counted once. When several detectors or edge types state the same dependency between two repositories over one channel, that dependency counts once in fan-in, choke points, criticality and roles. `edges` still lists one entry per edge type. v2 groups them as `relationships`. |
| `domains`, `estate_summary.total_systems` | List and count only the business domains that at least one repository of this estate map belongs to, plus Unclassified. An organization domain without a repository in this group audit is not a system of this estate. v2 lists every domain of the organization as `org_domains`. |
| `is_user_curated_domain`, `is_user_curated_role`, `is_user_curated_style`, `is_user_curated_topology` and the curated values | A curation is kept for the repository itself, identified by its organization and git remote, so a re-audited repository keeps its curated domain, role, style, topology role and relations. |
| `estate_summary.org_primary_tier`, `org_archetype.org_primary_tier`, `org_confidence`, `org_dimension_scores` | One consolidation, shared by the group audit, curation and the KPIs. Each repository is weighted by the square root of its lines of code, and in an estate of three or more repositories none carries more than 45% of the weight. Topology roles change the weights only when at least half of the repositories have dependency evidence. A repository a user excluded from the estate tier ("Exclude from benchmark") or one without a tier takes no part. v2 lists those under `org_tier.excluded_repos`. |
| `domains[].aggregate_scores` | The domain's code health. A repository without audit scores no longer counts as seven zero scores, and a category scored 0 is no longer dropped from `composite`. `composite` is the mean of the categories at least one scored repository reports, and each category is the lines-of-code-weighted mean over the scored repositories. The object keeps its fields. v2 adds `scored_repo_count`. |

**Portfolio executive summary**: summaries generated with evidence guardrails (v2 `provenance.guardrails_applied`) make no coupling, role or tier claim the evidence does not support, and state when cross-repository coupling cannot be determined.

**Repository dependencies**: the unversioned payload as it was served before, frozen: `total_dependencies`, `outdated_count` (always 0), `vulnerabilities {critical, high, medium, low}` and `licenses`. It keeps the historic licence semantics: `licenses` is unfiltered (company-owned packages included) and there is no `license_counts`.

**Repository development**: the development narrative as a bare markdown string, unchanged.

The value corrections of the unversioned endpoints (developer identity masking, repository-relative paths, `409` for unfinished audits) apply to every version; see [Corrections to unversioned endpoints](#corrections-to-unversioned-endpoints).

### v2

Conventions for every v2 resource:

- Every documented field is always present; `null` means unknown, never zero: a count is `0` only when CodeDD counted nothing, and a ratio without a denominator (`dependency_edges_per_repo` of an estate without repositories) is `null`. Flags such as `is_external` and the `curated` flags are `false` unless set.
- Every `confidence` is an integer from 0 to 100.
- File references are repository-relative paths; no field contains an absolute path.
- Every timestamp is ISO-8601 UTC ending in `Z` (see [Timestamps](#timestamps)).
- Stored LLM documents (component `properties`, insight blocks, detector blobs) are not served; their facts are, as typed fields.
- A list taken from an unbounded source is capped and its total is served beside it (for example `external_systems.total`).

**Vocabularies**

| Field | Values |
|-------|--------|
| `relation_kind` | `directed` (source depends on, or acts on, target), `undirected` (no direction; for example two repositories sharing one data store) |
| `relation_class` | `dependency`, `delivery` (build, CI, deployment, base images), `commonality` (shared technology, never coupling), `structural` (one component contains another), `unclassified` |
| `evidence_tier` | `deterministic` (static evidence in files: imports, manifests, configuration, literal references), `curated` (asserted by a user of your organization), `heuristic` (rule-based pairing without a cited file), `llm` (an LLM's claim), `unknown` (not recorded) |
| `tier.tier_status` | `applicable`, `not_applicable` (the scale-tier ladder does not apply to this kind of repository, e.g. a library or infrastructure code), `insufficient_evidence`; `null` when no classification exists |
| `requirement_ladder_summary.basis` | `requirement_ladder` (the tier is shown by the requirement ladder), `legacy_centroid` (the tier is shown by the previous method; `available` is `false` when no ladder was computed) |
| `requirement_ladder_summary.certainty` | `exact`, `at_least` (a lower bound: the next tier could not be verified; never compare it as an exact tier) |
| `requirement_ladder_summary.areas[].next_state` | `blocked` (the next tier was checked and is not met), `not_verified` (could not be assessed), `beyond_checks` (no check exists yet for the next tier), `at_top` |
| `repo_kind.source` | `classifier` (classified and stored by the analysis), `derived` (classified when served, for audits stored before kinds were), `none` (not determined), `display_labels` (older audits: a label-only kind) |
| `coverage.data_source` | `server` (the analysis read a server-side clone), `cli_v1` (a CLI submitted its own computed document), `cli_v2` (a CLI run driven by the server); `null` when not recorded |
| `deployment_platform` | `container_orchestrated`, `container_simple` (containers on hosts), `paas`, `serverless`, `vm`, `unknown` (no deployment target found); `null` when no deployment profile was recorded |

**Estate edge types.** Every relationship between repositories has one of the types below. A type's meaning does not change. A type can gain an alias, and an edge stored under an alias is served under the type.

| `type` | Meaning | `relation_class` | `relation_kind` | `channel` |
|--------|---------|------------------|-----------------|-----------|
| `vcs_dependency` | Pulls source code from the target (submodule, git dependency) | `dependency` | `directed` | `library` |
| `library_dependency` (alias `shared_library`) | Depends on a library the target publishes | `dependency` | `directed` | `library` |
| `build_inheritance` | Builds against build configuration the target publishes (parent POM, BOM, composite build) | `dependency` | `directed` | `build` |
| `catalog_relation` | Declares a dependency on the target in its service catalog | `dependency` | `directed` | `catalog` |
| `contract_dependency` | Consumes API contracts or schemas the target owns | `dependency` | `directed` | `contract` |
| `interface_dependency` | Calls an interface of the target named in the architecture analysis | `dependency` | `directed` | `http` |
| `api_dependency` | Calls the target's API (configured endpoint, service name, host) | `dependency` | `directed` | `http` |
| `api_consumer_producer` | API call paired from consumer and producer roles | `dependency` | `directed` | `http` |
| `event_coupling` (alias `event_bus`) | Consumes a topic or queue the target publishes | `dependency` | `directed` | `event` |
| `event_dependency` | Consumes from the messaging infrastructure the target publishes to; no shared topic was matched | `dependency` | `directed` | `event` |
| `data_dependency` (alias `shared_datastore`) | Both reference the same data store | `dependency` | `undirected` | `data` |
| `uses_base_image` | Builds on the target's base image | `delivery` | `directed` | `build` |
| `ci_orchestration` | Runs CI/CD workflows or pipeline templates maintained in the target | `delivery` | `directed` | `build` |
| `deploy_assembly` | Assembled and deployed by the target (a platform or GitOps repository) | `delivery` | `directed` | `deploy` |
| `shared_technology`, `shared_messaging`, `shared_database` | Both use the same technology, messaging infrastructure or data store name; no shared instance is implied | `commonality` | `undirected` | `commonality` |
| `unknown` | A link whose kind was not recorded | `unclassified` | `undirected` | `unclassified` |

**Repository architecture (v2)**

| Field | Contents |
|-------|----------|
| `schema_version` | `"2"` |
| `analysis_timestamp`, `summary` | When the analysis ran; its narrative summary |
| `repo_kind` | `kind`, `label`, `modifiers` (e.g. `monorepo`), `confidence`, `source`, `reasons` |
| `style` | `value`, `label`, `source` (`user`, `deterministic`, `llm`, `none`), `basis`, `confidence`, `conflict`, `conflicting_values`, `unverified_value` (a suggestion while the style is unknown), `candidates` with their evidence |
| `tier` | `available`, `tier_status`, `tier_status_reason`, `primary_tier`, `name`, `confidence`, `mixed_tier_note`, `dimensions` |
| `metrics` | Component, relationship and technology counts, `coupling_edges_per_component` with `coupling_measurable`, `primary_communication`, `single_points_of_failure_count` with `single_points_of_failure_complete` |
| `technologies`, `patterns` | Detected technologies with confidence and evidence; architectural patterns |
| `components` | Graph nodes (first-party and `is_external` systems): `domain`, `criticality` (the LLM's label), `confidence`, `evidence_tier`, `evidence_basis`, `key_files` (at most 25) and `key_file_count`, and `structural_criticality` (below) |
| `edges` | `source`, `target`, `type`, `relation_kind`, `relation_class`, `communication_type`, `confidence`, `evidence_tier`, `evidence_basis`, `evidence` (at most 10) and `evidence_count`, and `is_critical` with its `criticality_reason` (a critical bridge) |
| `single_points_of_failure` | Components many others depend on at runtime |
| `coverage` | `reported`, `run_status`, file and package counts, `inventory_capped`, `external_systems_detected`, `edge_evidence_tiers` (edges per evidence tier), `data_source` |
| `data_quality` | `level` (`good`, `partial`, `low`, `unknown`) and the `reasons` behind it |
| `validation` | The structural validation of the analysis: `status` and its `checks` |
| `architecture_run` | How the analysis run ended: `status` (`completed`, `partial`, `failed`, `unknown`), its `reasons`, and the `basis` it is known from (`run_record`, `coverage` for audits analysed before runs were recorded, `none`). An unrecorded run is `unknown`, never a success. |
| `structural_criticality` | What holds the component graph together: `assessed` (with `not_assessed_reason` when the graph cannot be searched), `complete` (`false` when the graph is partial and the result a lower bound), `graph_limitations`, node and link counts, `deterministic_link_count`, `critical_node_ids` and `critical_link_count`. `available` is `false` for audits analysed before it existed. |
| `module_dependencies` | The deterministic module-level import graph: `status` (`available`, `single_module`, `unsupported` when no resolver exists for the main language, `outdated_resolver`, `not_analysed`, `unavailable`), `primary_ecosystem`, `resolution_rate`, `ecosystems`, `structure` (cycles, instability, layer violations), `modules_total` and `edges_total`, and the 25 most connected `modules` and 40 heaviest `edges` |
| `model` | Where the facts above come from: the typed architecture model of the audit. `available`, `basis` (`stored` by the analysis, or `legacy_adapter`: rebuilt from the stored results of an audit analysed before the model existed), `source`, `schema_version`, `algorithm_version` with `current_algorithm_version` and `algorithm_current`, `model_hash` (changes only when a fact changes), `stored_at` and `run_status`. `available` is `false`, with every other field `null`, when no model could be built. |
| `requirement_ladder_summary` | The repository's tier by the requirement ladder (see "How the tier is determined" below): `available`, `basis`, `tier_status`, `tier`, `certainty`, `next_tier`, `catalogue_version` (the requirement list, e.g. `ladder-v0.2`), `rule_id` (`ladder_staged_v2`), `areas` (`area`, `applies`, `level`, `next_state` per area) and `lift` (`build_count`, `confirm_count`, `not_checked_count` for the next tier). No requirement results, evidence or file paths. `available` is `false`, with every other field `null`, for an audit analysed before the ladder. |

A component's `structural_criticality` is `{is_critical, reason, reasons, fan_in, fan_out}`. Each entry of `reasons` is a `code` with its figures: `articulation_point` (`separated_nodes`, `parts`), `high_fan_in` (`fan_in`, `percentile`) or `single_point_of_failure` (`dependent_count`). It is `null`, and so is an edge's `is_critical`, when the graph could not be assessed or the audit predates structural criticality. Before that, `is_critical` was the LLM's strength label, and v2 never serves it as structural criticality.

A component of the LLM synthesis has the `evidence_tier` `llm`: its boundary, name and responsibilities are the model's, even when the files it cites are real. Its `evidence_basis` says what backs it: `repository_files_verified` (the cited files were checked against the analysed repository), `repository_files` (cited, not checked) or `analysis_only` (no file cited). A component of the deterministic fallback graph (`deterministic_fallback`) is `deterministic`, and an external system (`relationship_target`) takes the strongest tier of the edges that reference it.

**Estate map (v2)**

| Field | Contents |
|-------|----------|
| `summary` | Repository, line and system counts; `dependency_edge_count` (directed dependencies: the only coupling), `dependency_undirected_count`, `commonality_count`, `delivery_edge_count`, `dependency_edges_per_repo`, critical dependency counts, role, tier and style composition, `choke_points`, `governance_coverage` |
| `org_tier` | The estate's tier: `available`, `tier_status`, `primary_tier`, `name`, `confidence`, `dimensions`, and the consolidation behind it: `excluded_repos` (each with its `reason`: `user_excluded`, `not_applicable`, `insufficient_evidence` or `classification_unavailable`, and its `loc` with `loc_basis`: `audited_loc`, `first_party_loc`, `total_loc` or `null` when it was not recorded), `loc_concentration` (the repository holding most of the code, with the capped weight it got) and `consolidation` (`model`, `input_hash`). The three are `null` for estates consolidated before they were recorded. |
| `repos` | One entry per repository: `repo_kind`, `style`, `tier`, estate and topology role, `curated` flags, `governance`, `scores`, `evidence_status`, `dependency_assessed`, `external_systems` (the 25 most confident with `total` and `truncated`), `deployment_platform` with its label, `estate_membership` (`member`, `in_topology`, `reason`) and `requirement_ladder_summary` (the repository's ladder tier in the estate, as on the repository resource) |
| `domains` | Business domains with a repository in this estate map: `repo_audit_uuids`, `aggregate_scores` (code health; every value `null` when no repository has audit scores), `scored_repo_count`, and recommendations (each with the `trigger` it was drawn from) |
| `org_domains` | Every business domain of your organization, including those with no repository in this estate map (`repo_count` 0) |
| `edges` | Typed edges between repositories, one per edge type and orientation: `relation_kind`, `direction_meaning`, `relation_class`, `channel`, `evidence_tier`, `evidence_basis`, `confidence`, `evidence_confidence`, `coupling_weight`, `detector`, `is_critical_path`, the `subject` they name (technology, library, topic, service, ...), their `evidence` and structured `evidence_items`, and for integration edges the `identity_tier`, `matched_identity` and `integration_signals`. `relationship_id` names the relationship the edge belongs to, and `facet_id` the edge itself. |
| `relationships` | One per source, target and channel: the unit that fan-in, choke points and criticality count. It has a stable `edge_id`, its typed edges as `facets` (one per type and detector, each with its own confidence and evidence), `evidence_confidence`, `coupling_weight`, the strongest `evidence_tier`, and the merged `evidence_items` |
| `relationship_candidates` | Pairings too weak to draw, proposed for curation, each with its `reason`: `below_confidence_threshold`, `loose_identity_match`, `broker_only` (both use one broker, no topic compared), `domain_http_pair` or `domain_single_producer` (paired only by shared business domain). They are never relationships and never count as coupling. |
| `shared_stack` | Technology two or more repositories use, with how each repository was found to use it. Commonality, never coupling. |
| `detection_coverage` | What the cross-repository detection could see, per repository and for the estate, including `estate.coupling_verdict`: no coupling verdict is drawn from missing evidence |
| `estate_analysis` | The estate stages (`business_domains`, `estate_topology`, `ic_summary`): the overall `state` (`done`, `running`, `pending`, `failed`, `not_recorded`), each stage's `status`, `error`, `attempts` and timing, and `stale` when membership changed after a stage ran |
| `estate_membership` | Which sub-audits take part: `included_count` (members whose audit completed), `topology_count`, `shown_count`, and the `excluded` sub-audits with their reason. `available` is `false` when the membership could not be read. |
| `requirement_ladder_summary` | The company tier by the requirement ladder (`rule_id` `weighted_quantile_v1`): the fields of the repository summary, plus `input_snapshot_hash` (the same hash means the same inputs) and `computed_at`. `org_tier` keeps the previous method's values. |

**How the tier is determined (requirement ladder).** A tier is the highest level whose required capabilities are all evidenced, cumulatively, in every area that applies to the repository. Each requirement is Met, Not met, Not assessed (it could not be checked, for example because it is configured outside the repository, or CodeDD does not check it yet) or Not applicable, and Not assessed never counts against a tier. When the next tier could not be verified, the tier is a lower bound (`certainty` `at_least`). The company tier is reached in an area when repositories holding at least 80% of the weight (the square root of their lines of code) reach it there, plus every repository rated critical; the company tier is the lowest area. Libraries, infrastructure code and other repositories without a running scale are not rated. `basis` says which method the tier is shown by; the `tier` and `org_tier` blocks keep the previous method's values until every consumer switches, and v1-legacy does not change.

**Two confidences.** `evidence_confidence` (0 to 100) answers whether the relation is real. Detectors that read the same input count once, and independent ones are combined (noisy-OR), so a relation found by the runtime configuration and by the architecture analysis scores above either finding. `coupling_weight` (0 to 1) answers how significant the relation is architecturally. It is the fixed weight of the edge's type in the edge-type registry, and a relationship takes the largest weight of its types. The weights are: `deploy_assembly` 1, `catalog_relation` 0.95, `build_inheritance` 0.9, `interface_dependency` 0.88, `vcs_dependency` and `contract_dependency` 0.85, `ci_orchestration` 0.8, `api_dependency` 0.75, `library_dependency`, `event_coupling` and `event_dependency` 0.7, `data_dependency` and `uses_base_image` 0.65, `api_consumer_producer` (paired from consumer and producer roles) 0.6, `unknown` 0.5, and `shared_technology`, `shared_messaging` and `shared_database` 0. One is never folded into the other. `confidence` on an edge equals its `evidence_confidence`.

**Structured evidence.** Each item of `evidence_items` is `{repo, file, signal, detector, identity_tier}`: the repository whose file holds the evidence, the repository-relative `file`, the `signal` that matched (for example `BILLING_URL=http://billing-api:8080`), the detector that read it, and how precisely the target was identified (`coordinate`, `git`, `image`, `chart`, `runtime_name`, `host`, `alias`, `repo_name`, ...). A list holds at most 20 items, and `evidence_item_count` gives the total.

**Portfolio executive summary (v2)**

```json
{
  "schema_version": "2",
  "group_audit_uuid": "8c1e...",
  "executive_summary": "## 1. ...",
  "provenance": {
    "generated_at": "2026-10-04T12:30:00+00:00",
    "input_hash": "3f7a...",
    "guardrails_applied": true,
    "coverage_caveat_applied": true
  }
}
```

`input_hash` is the SHA-256 of the input the summary was generated from: a changed hash means a summary generated from different data. `coverage_caveat_applied` says whether the summary states that cross-repository coupling cannot be determined from the evidence. Both are `null` (and `guardrails_applied` is `false`) for summaries generated before provenance was recorded. When the provenance cannot be read, every provenance field is `null`, `guardrails_applied` included: unknown, not false.

**Example: one estate edge in v2** (abridged: the audit and domain identifiers of both ends and the empty `subject` fields are left out)

```json
{
  "source_repo": "checkout",
  "target_repo": "payments",
  "type": "api_dependency",
  "relation_kind": "directed",
  "direction_meaning": "depends_on",
  "relation_class": "dependency",
  "evidence_tier": "deterministic",
  "evidence_basis": "static_evidence",
  "confidence": 85,
  "is_critical_path": true,
  "criticality_kind": "structural",
  "subject": { "service_name": "payments-api" },
  "evidence": ["config/services.yml: PAYMENTS_URL"],
  "evidence_count": 1
}
```

**Example: the relationship that edge belongs to** (abridged). A second detector found the same call in the architecture analysis. Its finding is a separate facet, and because it is independent evidence it raises `evidence_confidence`.

```json
{
  "edge_id": "rel_3c9d0e5a71b24f6a8d10",
  "source_repo": "checkout",
  "target_repo": "payments",
  "channel": "http",
  "relation_kind": "directed",
  "relation_class": "dependency",
  "is_coupling": true,
  "types": ["api_dependency", "interface_dependency"],
  "evidence_confidence": 93,
  "coupling_weight": 0.88,
  "evidence_tier": "deterministic",
  "is_critical_path": true,
  "facets": [
    { "type": "api_dependency", "detector": "integration_matcher", "confidence": 85, "evidence_tier": "deterministic" },
    { "type": "interface_dependency", "detector": "architecture_bridge", "confidence": 55, "evidence_tier": "heuristic" }
  ],
  "facet_count": 2,
  "evidence_items": [
    { "repo": "checkout", "file": "config/services.yml", "signal": "PAYMENTS_URL=http://payments-api:8080", "detector": "integration_matcher", "identity_tier": "runtime_name" }
  ],
  "evidence_item_count": 1
}
```

**Repository dependencies (v2)**

| Field | Meaning |
|-------|---------|
| `schema_version` | `"2"` |
| `audit_uuid` | The repository audit |
| `total_dependencies`, `vulnerabilities` | Totals as in v1 |
| `licenses` | Distinct licences of third-party packages (company-owned packages excluded). `null` when the overview could not be computed (unknown, not "none"); `[]` only when there really are none. `total_dependencies` and `vulnerabilities.*` are `null` in that case too |
| `license_counts` | `permissive`, `weak_copyleft`, `strong_copyleft`, `unknown`, `total` (declared third-party packages) and `company_owned_excluded`, as the app's Dependencies tab shows them. `null` when they could not be computed, never zeros |
| `package_count` | Packages in the catalog (all pages; also `meta.total_count`) |
| `packages[]` | One page, by name then ecosystem: `name`, `package_key` (`<registry>/<name>`, `null` without a package identity), `ecosystem`, `version` (declared, for display), `declared_version`, `resolved_version`, `is_direct`, `is_declared`, `times_used`, `vulnerability_count`, `critical`, `high`, `medium`, `low`, `vulnerability_ids` (CVE/GHSA), `vulnerability_ids_truncated`, `vulnerabilities[]` {`id`, `severity` (the advisory store's counted severity, or the OSV feed records' when only the feed knows the ID; `null` when not rated or unknown, never a guess), `rated`, `withdrawn`, `fixed_in`, `kev`, `epss`} with `vulnerabilities_truncated`, `vulnerability_kev_ids`, `vulnerability_max_cvss_score` (`null` when no advisory has a score), `scorecard_aggregate_score`, `licence` (`null` when none was detected), `license_category` (`permissive`, `weak_copyleft`, `strong_copyleft`, `unknown`; `null` for a company-owned package), `license_scope` (`third_party`, `company_owned`, `not_declared`, `runtime`), `in_license_counts`, `dependency_provenance`, `source_text.vulnerability_summary` |

Summing `license_category` over the packages with `in_license_counts: true` gives `license_counts`, because both use the same classifier and exclusions. Package names never carry CodeDD's internal registry prefix. The rows equal the rows of the MCP tool `list_audit_dependencies` at `detail=full`; two packages of the same name in different ecosystems are two rows. Pages use `?cursor=` (from `meta.next_cursor`) and `?limit=` (1-100, default 50). Each page has its own `ETag`. A finished audit with no packages serves `packages: []`. A read failure answers `500 internal_error`, never an empty catalog.

**Repository development (v2)**

| Field | Meaning |
|-------|---------|
| `schema_version` | `"2"` |
| `audit_uuid` | The repository audit |
| `git` | The git statistics the app's Development tab reads, or `null` when the repository has none. Fields: `time_range`; `repository {name, url}` (the URL never carries credentials); `contributor_count`; `contributors[]` ranked by commits: `author`, `name`, `email`, `commit_count`, `first_commit`, `last_commit`, `lines_added`, `lines_removed`, `files_modified`, `unique_file_types` (`null` when not recorded, never 0), `expertise {total_files, total_files_w_domains, domain_contributions[{domain, files}]}` and `commit_series` (`{YYYY-MM-DD: commits}`, the activity graph; `null` when not recorded). The git documents are fixed shapes: `time_based_stats {commit_frequency {total_commits, days_active, avg_commits_per_day, avg_commits_per_week, most_active_day {period, commits}, most_active_week}, activity_periods {first_commit_date, last_commit_date, repository_age_days, total_daily_commits}}`; `code_churn_stats {overall_churn {...}, file_stability {total_files_modified, avg_changes_per_file, files_by_churn_level}, hotspots[] {times_modified, total_changes, total_additions, total_deletions, hotspot_score, domain, source_text.file_path}}` (50 at most); `collaboration_stats {overview, author_involvement}` (counts only); `release_stats {total_releases, release_frequency, releases[] {date, changes_count, source_text {tag, message}}}` (50 at most); `branch_stats {total_branches, active_branches, stale_branches}`; `merge_stats {total_merges}`; `meta_info {total_files, first_commit_date, latest_commit_date, source_text.default_branch}`. Each is `null` when not stored, and a date that is not a date is `null`. Per-branch, per-merge and collaboration-network rows are not served: they carry names the identity mask cannot map |
| `development_analysis` | `{source_text: <markdown>}`: the v1 narrative. `null` when it was never generated |
| `test_coverage`, `extension_distribution` | As in `/files/`. `null` when absent; a failed read answers `500 internal_error` instead of `null` |
| `developer_identities_masked` | `true` when the company masks developer identities. Contributors (author, name, email) then carry the stable `dev N` labels, and e-mail addresses in the narrative are masked too |

`?time_range=` (`month`, `quarter`, `year`, `all`; default `year`) selects the activity window. Each value has its own `ETag`. A finished audit with neither a narrative nor git statistics answers `404 data_not_available`; a read failure answers `500 internal_error`.

### Moving from v1 to v2

| v1-legacy | v2 |
|-----------|----|
| `archetype_classification.primary_tier`, `archetype_name`, `confidence` | `tier.primary_tier`, `tier.name`, `tier.confidence` (check `tier.tier_status` first) |
| `archetype_classification.architecture_style`, `executive_overview.service_architecture` | `style.value` (with `style.source` and `style.conflict`) |
| `visualization_data.enhanced_nodes` | `components` |
| `visualization_data.enhanced_edges`, `component_view.relationships` | `edges` |
| `executive_overview.key_metrics` | `metrics` |
| `executive_overview.critical_dependencies` | `single_points_of_failure` |
| `metadata.coverage`, `metadata.data_completeness` | `coverage`, `data_quality` |
| `dependency_flow.critical_paths`, `visualization_data.enhanced_edges[].is_critical` | `structural_criticality`, `components[].structural_criticality`, `edges[].is_critical` |
| `estate_summary.total_cross_repo_edges`, `coupling_density` | `summary.dependency_edge_count`, `summary.dependency_edges_per_repo` (read `detection_coverage.estate.coupling_verdict` before drawing a coupling conclusion) |
| `edges` | `edges` (typed) and `relationships` (one per source, target and channel, the unit criticality counts) |
| `domains[].repos[]` | `repos` (and `domains[].repo_audit_uuids`) |
| `domains[].aggregate_scores` | `domains[].aggregate_scores` with `scored_repo_count` |
| `org_archetype` | `org_tier` (with `excluded_repos`, `loc_concentration` and `consolidation`) |
| dependencies `licenses` (unfiltered) | `licenses` (third-party only), `license_counts` and the `packages` catalog |
| development (markdown string) | `development_analysis.source_text`, next to `git`, `test_coverage` and `extension_distribution` |

The OpenAPI document of the API describes both versions of each resource (`oneOf`).

## Corrections to unversioned endpoints

The unversioned endpoints keep their fields: nothing is renamed or removed, and new fields are only added. Their **values** are corrected where CodeDD computed or served them wrongly, as in [v1-legacy](#v1-legacy). The corrections of November 2026, for every schema version where an endpoint is versioned:

**Status and availability**

| Endpoint | Field or answer | Before | Now |
|----------|-----------------|--------|-----|
| `/audits/{audit_uuid}/`, `/group-audits/{group_audit_uuid}/audits/` | `is_completed` | `true` for every audit with a synthesis date, including running ones (e.g. status `File information imported`) | `true` only when the status is `Audit completed` |
| same | `is_failed` (new) | — | `true` when the audit ended without results (failed, errored, cancelled, deleted or expired); such an audit has `is_completed: false`, so stop waiting for it |
| Every repository result endpoint (`scores`, `summaries`, `flags`, `dependencies`, `development`, `files`, `executive`, `complexity`, `tier-benchmark`, `ai-authorship`) | Answer while the audit runs | `200` with the rows read so far (zero scores, complexity grade `A` with 0 functions) | `409 audit_in_progress` |
| Repository result endpoints | `ETag` | An in-progress audit had a validator that stayed the same when it finished | A validator only once the audit is completed |
| `/group-audits/{group_audit_uuid}/kpis/{kpi_name}/` | Answer while the group audit runs | `200` with defaults (health 60, debt 40 % critical, innovation 70 % "excellent" from 0 commits) and `audit_is_completed: false` | `409 audit_in_progress` for all five KPIs |
| Repository and group result endpoints | An audit that failed or was cancelled | `409 audit_in_progress`, as if it were still running | `409 audit_failed` (see [Errors](#errors)); a kpi-history entry of such a group audit has `reason: "audit_failed"` |
| `/portcos/{portco_uuid}/kpi-history/` | An entry that was not computed | The empty templates (health 100, debt 0 %, IP status `safe`), or KPIs from the sub-audits finished so far | Every KPI `null`, `available: false`, `reason`; computed entries add `available: true, reason: null`; new `group_audits` map |
| KPI, summaries and other group routes | A result that does not exist (no analysable repositories, a narrative never generated, no git statistics) | `409 audit_in_progress` | `404 data_not_available` (see [Errors](#errors)) |
| `/group-audits/{group_audit_uuid}/security-findings/` | A running group audit | The rows found so far, without a marker | The same rows with `meta.partial: true` and `meta.note` |
| `/group-audits/{group_audit_uuid}/financials/`, `/integration-assessment/` | A running group audit | `200` computed from the repositories finished so far | `409 audit_in_progress` (the app shows neither until completion) |
| `/group-audits/{group_audit_uuid}/benchmark/`, `/technical-debt/`, `/category-breakdown/`, `/development/`, `/dora/`, `/ai-native/`, `/repo-activity/`, `/supply-chain/` | A running group audit | `200` without a marker | The same answer with `meta.partial: true`, `meta.group_audit_state` and `meta.note`; before analysing starts, `409 audit_in_progress` |
| Group routes, `/portcos/{portco_uuid}/kpi-history/` | A group audit where audits ran and none succeeded (`... 0/N successful`) | Read as completed | Failed: `409 audit_failed`; KPI history `state: failed`, `reason: audit_failed` (see [Group audit endings](#group-audit-endings)) |
| Group routes, `/portcos/{portco_uuid}/kpi-history/` | A group audit that ended with nothing to analyse (no audits found, or all expired or deleted) | Read as completed | Ended without results: KPIs `404 data_not_available`; KPI history `state: empty`, `reason: no_repositories` |
| `/audits/{audit_uuid}/tier-benchmark/` (and any repository result endpoint whose read fails) | A read that fails on a completed audit | `409 audit_in_progress` | `503 temporarily_unavailable` with `Retry-After: 15` |

**Null instead of a made-up value**

| Endpoint | Field | Before | Now |
|----------|-------|--------|-----|
| `/audits/{audit_uuid}/scores/`, `/summaries/` | `dependencies_score` | `0`: the pipeline never computes it | `null` (not scored); the category breakdown no longer lists a `dependencies` subcategory |
| KPI `key-person` | `bus_factor` / `domains_at_risk`, `domain_bus_factor_at_risk`, `domain_bus_factor_total` | `0` when no domain is large enough to assess | `null` |
| KPI `key-person` `continuity_summary`, group development | `active_expertise_coverage_percent`, `expertiseCoverage` | `0` when not measured | `null` |
| KPI `innovation` | `innovation_percent`, `maintenance_percent`, `status`, `trend_*`, `metrics.*` percentages | 50 / 50 / `fair` placeholders, or 70 % `excellent` without any git data | `null`, with `data_availability: "unavailable"`; on the churn heuristic `status: null`, `data_availability: "fallback_heuristic"` and the band in `heuristic_band` |
| KPI `innovation` | `quarterly_effort_mix`, a quarter without commits | every percent `0.0` | every percent `null` (`metadata.total_commits` stays `0`) |
| `/audits/{audit_uuid}/complexity/` | rank and every figure when nothing was analysed; `avg_halstead_volume` when not stored | rank `A`, `0` | `null` |
| `/audits/{audit_uuid}/complexity/` | `avg_cyclomatic_complexity`, `median_complexity`, `min_complexity`, `max_complexity`, `std_dev`, `complexity_rank` | Per-file averages served as function statistics (CLI audits) | Recomputed from the per-function figures when the audit has them (`statistics_basis: "functions"`); with per-file averages only, median, min, max and standard deviation are `null` |
| `/audits/{audit_uuid}/dependencies/` v2, `/packages/`, `/supply-chain/` | `vulnerability_max_cvss_score` | `0.0` for a package with no scored advisory (the scanner's "no score" default) | `null`; a real CVSS base score is always above 0. The type (`number \| null`) is unchanged |
| `/audits/{audit_uuid}/executive/` | `release_data.release_velocity.avg_days_between_releases`, `release_frequency` | `0`, or 30 days assumed | `null` when unknown; `release_frequency` is `Unknown` without releases |
| `/audits/{audit_uuid}/executive/` | `repository_metrics.total_repository_lines`, `audit_lines_of_code`, `audit_coverage_percentage`, `documentation_lines` | `50000` on a read failure, `0` when not found or not measured | `null` when unknown (`documentation_lines` is not measured on this path) |

**Paths and texts**

| Endpoint | Field | Before | Now |
|----------|-------|--------|-----|
| `/audits/{audit_uuid}/flags/`, `/complexity/`, security findings, finding detail, the CSV of the in-app export | `file_path`, `function_metrics[].path`, `file` | The path on CodeDD's analysis machine (`/app/cache/<id>/<repo>/src/a.py`) | The repository-relative path (`src/a.py`) |
| `/audits/{audit_uuid}/flags/` | Flag texts (`reasons_of_flag`, `validation_*`, `mitigation_pointer`, `developer_comment`, `sonar_message`, `file_path`, …) | Words such as `when`, `has`, `get`, `match`, `update` and the characters `; $ { }` were stripped (`src/get/handler.py` read `src//handler.py`) | Served as stored; only control characters are dropped and whitespace runs collapsed |
| KPI `ip-security`, supply chain | `critical_issues[].identifier`, `component`, `recommended_fix` | The stored name with CodeDD's internal registry prefix (`$!pypi$!_cryptography`) | The display name (`cryptography`) |
| Every endpoint, with masking on | Developer names and e-mail addresses | Financials `summary.headline`, `summary.key_person.identifier` and `people.key_person.identifier` carried the real e-mail; an e-mail without an alias passed through everywhere | `dev N` or `unknown developer` everywhere (see [Developer identity masking](#developer-identity-masking)); Financials adds `summary.key_person.name` and `people.key_person.name`, and its headline says "one person" |
| Every endpoint, with masking on | E-mail-only fields (`email`, `developer_email`, `author_email`) | the `dev N` label, which read like an address | `null` (the field stays; the label is in the name fields). Review 2026-11-11 |

**Definitions and figures**

| Endpoint | Field | Change |
|----------|-------|--------|
| KPI `key-person` | `financial_impact` | Computed on the audited lines (`scope: "audited"`), the same figure as Financials (was the whole repository). The whole-repository scenario is added as `financial_impact_root` |
| KPI `key-person` | `exit_exposure`, `exposure_windows` | `exit_exposure` is now computed (was always `null` on the API); `exposure_windows` added (the Exit exposure card's view) |
| KPI `innovation` | `dora`, `data_availability`, `heuristic_band` | Added: the DORA summary the Overview shows, and how the figure was obtained |
| KPI `ip-security` | `status_scope`, `transitive_critical_vulnerabilities_count`, `transitive_high_vulnerabilities_count` | Added; the headline counts and `status` stay direct (`status_scope: "direct"`) |
| KPI `key-person` | `domains_at_risk` | Added as the canonical name; `bus_factor` stays as its deprecated alias |
| KPI `technical-debt`, `/financials/` | `fix_days_basis`, `flag_scope`, `derivations.capex_remediation_fix_days_basis`, `effort.total.basis` | Added labels (see [Definitions](#definitions)) |
| KPI `scalability` | `repo_weights` | Added: the per-repository sizes next to `repository_details` |
| `/technical-debt/` | `summary.overallDebtScore` | Documented: the average technical-debt subcategory score, not code health (see [Definitions](#definitions)) |
| `/flags/` | `openFlagCounts`, `openHours`, `originalHours`, `category_id`, `owasp`, `kit_type`, `type_basis` | Added. Headline counts and hours cover open flags; `totalTimeToFixAllFlags` covers every flag neither refuted nor dismissed, resolved included, and is not a headline figure |
| `/tier-benchmark/` | `cohort_status`, `score_key_map`, `subject_scores` | Added `cohort_status` and `score_key_map`; `subject_scores` is filled also without a tier (was `{}`) |
| `/audits/{audit_uuid}/`, group audit rows | `audited_lines`, `whole_repo_code_lines`, `audited_coverage_percent` | Added; `lines_of_code` is unchanged (audited lines) |
| `/portcos/{portco_uuid}/settings/` | `maturity_stage`, `mask_developer_identities`, `sources`, `currency`, `financial_inputs` | Added |
| `/financials/`, KPIs `technical-debt` and `key-person`, Portfolio Insights | `currency` | Added: `/financials/` states its currency at the top (every amount in it, `*_usd` fields included, is in it); a KPI whose costs are priced at the company's rate states `currency` when that is not USD; Insights money rows state each company's currency (they all said `USD`), a payload `mixed` when its companies differ. A `*_usd` field outside a block with its own `currency` is US dollars |
| `/benchmark/` | `pulse_overlay`, category label | `pulse_overlay` added; the `scalability` category is labelled "Performance & scalability" (it was "Performance"; the key is unchanged) |
| `/dora/` | `classification_summary_90`, `summary_90.deployment_frequency.window_days` | The 90-day classification is added; the 90-day summary of an estimated portfolio said `window_days: 30` |
| Security findings | `review_state`, `counts_as_open`, `confirmed_by`, `user_marked_valid`, `user_marked_timestamp`, `open_count`, `open_critical_high_count`, `?include_closed=` | Added (see [Security findings](#security-findings)); `vstat` is deprecated. Medium (Yellow) findings of the CodeDD AI analysis are now listed, as Medium / Not validated, next to SonarQube's: they count in the list and in effort, never in the critical/high KPI |
| Monitoring summary, metric history | AI share | Measurable repositories over the 3 complete months before the audit (was all history over every repository, e.g. 4.1 % now 13.6 %); the old figure is `ai_authored_pct_all_history`. Until a company's monitoring block is rebuilt, its points carry `basis: "all_repositories"`, `window: "all_history"` |
| Monitoring summary | Goals | A company without saved objectives is held to CodeDD's default goals (`goal_source: "default"`), so `goal_status`, `plan_status` and the Off-plan count can change; the band (CodeDD's fixed scale) is kept separate from the goal |
| `/audits/{audit_uuid}/dependencies/` v2, `/packages/` | `vulnerabilities[].severity`, `rated`, `withdrawn`, `kev`, `epss` | Were `null` for an ID the advisory store does not know. Now the OSV feed records' rated severity when the feed knows the ID (still `null` when its record is unrated or withdrawn); `withdrawn` `true` or `false`; `kev` / `epss` from the CVEs the feed records list. `/packages/` adds `severity_basis` |
| `/packages/` | `license_risk` filter | Lists only the third-party rows the licence totals count; it also listed import-only names (standard-library modules), runtime and company-owned packages |
| `/audits/{audit_uuid}/executive/` | `release_data.releases`, `current_version`, `last_release_date` | The 10 most recent releases, newest first, and the version and date of the newest (were the 10 oldest and the oldest) |
| `/audits/{audit_uuid}/development/` v2 | `git.release_stats.releases` | The 50 most recent, newest first (were the 50 oldest) |
| `/group-audits/{group_audit_uuid}/development/` | `developerActivity[].isBot`, `summary.totalActiveDevelopers`, `avgTeamSizePerRepo`, `monthlyCommits`, `rollingCommits` | `isBot` was always `false`, so bots were counted; monthly series ended at the save date or today, and a developer known only from the author statistics had its lifetime commits in one month. Now `isBot` is `true` for automation accounts (dependabot, renovate, `[bot]`, ...) and the counts exclude them; `monthlyCommits` are calendar months ending at the git snapshot (`developerActivityWindow`) |
| KPI `key-person` | `exposure_windows`, `exit_exposure`, `change_vs_year_ago_points` | Computed as of the group audit's completion date (was the read date), as Portfolio Insights; `exposure_windows` adds `reference_date` and `anchor` (`audit`, or `read_date` without a completion date) |
| `/financials/` | `security_flags` item labels, `summary.headline` | Item labels read, for example, "Red (3 open)" (they said "confirmed"): the open findings (confirmed, inconclusive, not validated); the headline says "the N people who committed in the last 2 years (no headcount saved)" unless a headcount is saved (was "your N engineers"; `summary.effort.team_size_window_years`, `team_size_basis`); the key person "accounts for X % of the audited-file contributions" (was "owns X % of the code") |
| `/dora/` | `classification_summary`, `classification_summary_90` | Count the same window as the metrics (ending at `metrics_as_of`) and the group's repositories (were counted back from now, across the whole organization); adds `window_end` and `window_anchor` (`end`, or `now` without a stored anchor) |
| `/audits/{audit_uuid}/executive/` | `release_data.releases[].type`, `type_basis` | `type` came from substrings of the tag (every `v1.x` release read `major`). It is now the version part that changed from the version before it (`v1.15.1` after `v1.15.0` is `patch`), `null` for a tag that is not a version number; `type_basis` (`previous_version`, `version_number`, `not_semver`) is added |
| `/audits/{audit_uuid}/executive/` | `domain_distribution.domains[]`, `not_audited_lines_of_code` | The lines of the files the audit did not review were labelled `Unclassified`; they are now the `Not audited` row (`audited: false`; every other row `audited: true`) and `not_audited_lines_of_code` is added |
| `/group-audits/{group_audit_uuid}/integration-assessment/`, `repo-activity` | `languageDomainLoc`, `languageComplexity`, `topLanguage` | `.h` headers were a separate `C/C++` language next to `C++` (and a C++ repository with many headers had `topLanguage: C`). Headers now join the repository's C-family language and `topLanguage` sums every extension of a language |
| Security findings, `/flags/` | `owasp` of `error_handling.swallowed` | Was `A09` (logging); now `A04` (Insecure Design: a mishandled exceptional condition). `error_handling.no_log` stays `A09` |

Value corrections change the `ETag` of every versioned representation they touch (see [revisions](#revisions)).

## Errors

Failures use a stable `error_code` you can branch on. Messages may be reworded; codes will not change within v1.

| HTTP | `error_code` | Meaning and what to do |
|------|--------------|------------------------|
| 400 | `invalid_parameter` | A parameter failed validation (including an unsupported schema version, a bad cursor, or an unknown metric, section or KPI name). The message names the parameter. Fix the request. |
| 401 | `invalid_api_key` | Missing, malformed, or unknown key. Check the header. |
| 401 | `api_key_expired` | Past its expiry. Rotate it. |
| 401 | `api_key_revoked` | Revoked by an administrator. Ask for a new one. |
| 403 | `insufficient_scope` | The key lacks the scope this endpoint needs. Mint a key with it. |
| 403 | `enterprise_plan_required` | No active Enterprise plan on the organization. |
| 403 | `portfolio_monitoring_required` | The organization's contract does not include Portfolio Monitoring, which every `read:monitoring` endpoint needs. |
| 404 | `resource_not_found` | The resource does not exist **or** is not covered by your key. These are deliberately indistinguishable. |
| 404 | `data_not_available` | The resource is in scope and nothing is still running, but this result does not exist: no group audit or repositories to read (including a group audit that ended with nothing to analyse, `Group audit finalized: ...`), a narrative or git statistics that were never produced. Retrying will not help. |
| 409 | `audit_in_progress` | The audit has not finished, so this result set does not exist yet. Retry later. |
| 409 | `audit_failed` | The audit ended without results: it failed or was cancelled (`is_failed: true` on the audit), or it is a group audit where audits ran and none succeeded (`Group audit failed: 0/N successful`). The audit is not running, so polling will not help; read an earlier completed audit (if CodeDD recovers the audit, for example a group audit whose finalization failed, its status changes). Same status as `audit_in_progress`: branch on `error_code`. |
| 429 | `rate_limit_exceeded` | Honour the `Retry-After` header. |
| 500 | `internal_error` | Our fault, including a read that failed (CodeDD answers 500 rather than serve a missing value as `null`). Retry with backoff; quote the `correlation_id`. |
| 503 | `temporarily_unavailable` | A backing service did not answer in time, or a figure is being rebuilt. Honour `Retry-After` (15 s). This includes a read that failed on a completed audit. |

`409 audit_in_progress` means "come back later"; `409 audit_failed` and `404 data_not_available` are final answers for this audit, not errors in your pipeline. A repository or group audit that ended without results (failed or cancelled; `is_failed: true` on `/audits/{audit_uuid}/`) answers `409 audit_failed` on its result endpoints: stop waiting for it.

```json
{
  "status": "error",
  "error_code": "insufficient_scope",
  "message": "This API key does not have the \"read:portfolio\" scope.",
  "correlation_id": "9c1f..."
}
```

### Rate limits

| Bucket | Limit |
|--------|-------|
| Standard endpoints, per key | 120 requests/minute |
| Heavy endpoints (the **heavy** tier in the tables above: flags, dependencies, files, audit history, every group-audit roll-up except the DORA week detail, KPI history, Insights, CVE coverage, CVE search), per key | 30 requests/minute |
| All endpoints, per organization | 600 requests/minute |

Limits are per **key**, not per IP address, so one pipeline cannot throttle another that happens to share an egress address. On a `429`, wait for `Retry-After` seconds; do not retry immediately.

## Building a reporting pipeline

A complete nightly extract:

```python
import os
import requests

API_KEY = os.environ['CODEDD_API_KEY']
BASE = 'https://api.codedd.ai/api/v1/enterprise'

session = requests.Session()
session.headers['Authorization'] = f'Bearer {API_KEY}'


def get(path, **params):
    response = session.get(f'{BASE}{path}', params=params, timeout=60)
    if response.status_code == 429:
        raise RuntimeError(f"Rate limited; retry after {response.headers['Retry-After']}s")
    if response.status_code == 409:
        return None          # Audit still running; nothing to extract yet.
    if response.status_code == 404 and response.json().get('error_code') == 'data_not_available':
        return None          # Finished, but this result does not exist.
    response.raise_for_status()
    return response.json()['data']


rows = []
for portco in get('/portcos/', limit=100)['portcos']:
    group_audits = get(f"/portcos/{portco['portco_uuid']}/group-audits/", limit=100)

    for group_audit in group_audits['group_audits']:
        group_uuid = group_audit['group_audit_uuid']

        debt = get(f'/group-audits/{group_uuid}/kpis/technical-debt/')
        if debt is None:
            continue

        for audit in get(f'/group-audits/{group_uuid}/audits/', limit=100)['audits']:
            # Skip repositories whose analysis has not produced results
            # (still running, or ended without results: is_failed).
            if not audit['is_completed']:
                continue

            scores = get(f"/audits/{audit['audit_uuid']}/scores/")
            rows.append({
                'portco': portco['name'],
                'group_audit': group_audit['name'],
                'repository': audit['name'],
                'audited_lines': audit['audited_lines'],
                'scores': scores['scores'],
                'portfolio_technical_debt': debt['data'],
            })

print(f'Extracted {len(rows)} repositories')
```

### Recommended practice

- **Store the key in a secret manager**, never in source control, a notebook, or a dashboard definition.
- **Rotate on a schedule** — quarterly is a reasonable default — and always after someone with access leaves.
- **Use one key per consuming system.** When something misbehaves you can revoke it without taking down every other integration.
- **Grant the narrowest scope** the tool needs.
- **Cache with `ETag`s** and only re-read what changed. Of a completed audit, only the architecture changes later (through curation or a stored architecture model), and its `ETag` changes with it.
- **Retry with exponential backoff** on `429` and `5xx`; never retry a `4xx` other than `429`.
- **Check `is_completed`** before extracting results, and treat `409 audit_in_progress` as "come back later" and `404 data_not_available` as "this result does not exist", not as errors.
- **Store `null` as unknown**, never as zero.

## Security and auditability

- Keys are stored only as SHA-256 digests. Neither a database dump nor a query log yields a usable credential.
- Every request is recorded against the key — method, route, resource, status, and outcome — including refusals. Ask support for an access export if you need to evidence who read what.
- Access is re-checked on every request against your live contract (and, for `read:monitoring`, the Portfolio Monitoring module), the key's scopes, and the resource's ownership. Nothing is inherited from when the key was created.
- Responses are marked `Cache-Control: private, no-store` so audit findings do not linger in an intermediary cache.
- All traffic must be HTTPS.

## Frequently asked

**Can I write data through this API?**
No. Every data endpoint is read-only. Audits are started in the app or through the [CodeDD CLI](/documentation/codedd-cli-guide).

**Can I use it with an MCP client?**
Not with an API key. AI assistants such as Claude or ChatGPT connect through the separate [MCP Server](/documentation/mcp-server), where each fund member signs in with their own CodeDD account and sees only what they see in the app. Enterprise API keys (`codedd_ent_...`) are never accepted by the MCP server, and no MCP tool can list, create, rotate or revoke API keys. The capability-served endpoints of this API return the same data as the MCP tool of the same name.

**What happens when our contract lapses?**
The API closes within a minute, and existing keys stop working. They resume if the contract is reinstated and has not expired. If only the Portfolio Monitoring module lapses, the `read:monitoring` endpoints answer `403 portfolio_monitoring_required` and every other endpoint keeps working.

**Can a key read another firm's data?**
No. A key is bound to one legal organization and can only reach portfolio companies, audits, and results that belong to it. Anything else returns `404`.

**Do invited team members access the API with their login?**
No. The data endpoints do not accept session JWTs — only an organization API key (`Authorization: Bearer codedd_ent_...`). Invited members do not get automatic API access; an **organization administrator** must create a key and share it with the tool (or person) that needs it. Keys are org-scoped: whoever holds the key can read the firm's entire entitled audit estate, not just the subset one member sees in the web UI.

**Who can create or revoke keys?**
Only administrators of the **legal organization** (the Enterprise billing entity), via Account → Enterprise plan → Data API keys, while on an active full Enterprise contract.

## Related documentation

- [Glossary](/documentation/glossary)
- [Portfolio Monitoring](/documentation/portfolio-monitoring)
- [MCP Server](/documentation/mcp-server)
- [Setting Up a Portfolio Organization](/documentation/setting-up-a-portfolio-organization)
- [CodeDD CLI Guide](/documentation/codedd-cli-guide)
