PAI is the privileged internal API plane. It is not publicly reachable and is protected by:
PAI_IP_ALLOWLIST)X-PAI-Key)/internal/v1
Internal Swagger UI (requires PAI key + allowlist):
GET /internal/docsGET /internal/openapi.jsonProvide a PAI key via one of:
X-PAI-Key: phx_pai_...X-API-Key: phx_pai_...Authorization: PAIKey phx_pai_...POST /api/v1/admin/pai/request-key POST /api/v1/admin/pai/verify-and-generate
verify-and-generate accepts optional per-key restrictions:
{
"verification_token": "token-from-email",
"name": "Internal Analytics",
"ip_allowlist": ["10.0.0.0/8", "192.168.1.10"],
"domain_allowlist": ["internal.example.com", "*.svc.example.com"]
}
For backwards compatibility, mixed entries submitted in ip_allowlist are split into IP/CIDR and domain restrictions by the backend. Domain restrictions are matched against X-Forwarded-Host, Host, Origin, and Referer; use them as an additional guard with the global IP allowlist, not as a replacement for network controls.
Email verification is required before PAI key issuance. MFA verification is planned but not yet available.
| Endpoint | Purpose |
|---|---|
GET /internal/v1/cve/{cve_id} | Full CVE details + raw NVD record |
GET /internal/v1/cve/{cve_id}/github-pocs | Full (uncapped) GitHub PoC list + PS-HP-aligned popularity summary |
GET /internal/v1/phoenix-score/{cve_id} | Full PS-HP output with components |
GET /internal/v1/high-profile | Full high-profile list (no redaction) |
GET /internal/v1/enterprise-watchlist | Full watchlist entries |
POST /internal/v1/packages/intel | Internal package-version CVE + MPI malware intelligence |
POST /internal/v1/calculate-score | Custom PS-HP calculation |
POST /internal/v1/calculate-pss-score | Custom PS-OSS (PSS) calculation |
POST /internal/v1/calculate-phs-score | Custom PS-PHS calculation |
POST /internal/v1/calculate-pvs-score | Custom PS-PVS calculation |
POST /internal/v1/calculate-adqe-score | Custom ADQE calculation |
POST /internal/v1/calculate-eol-risk | Custom EOL SLR risk calculation |
GET /internal/v1/scoring-weights | PS-HP/PS-OSS weights |
GET /internal/v1/threat-actors | Threat actor intelligence |
GET /internal/v1/eol-intelligence | Full EOL intelligence |
GET /internal/v1/analysis/cwe | Full CWE analysis (mirror of /api/v1/analysis/cwe) |
GET /internal/v1/analysis/cwe-top25 | Full CWE Top 25 analysis |
GET /internal/v1/analysis/kev-cwe | Full KEV CWE analysis |
GET /internal/v1/analysis/owasp-top10 | Full OWASP Top 10 analysis |
GET /internal/v1/analysis/reference-threat-frequencies | Full, live reference threat-frequency distributions |
GET /internal/v1/cves | Unrestricted CVE search — public filters plus sort, published_after/published_before, has_analysis; limit up to 1000; unredacted |
GET /internal/v1/cves/by-product | Unrestricted vendor/product CVE enumeration (limit up to 2000) |
GET /internal/v1/cves/trending | Trending CVEs (news/social mention volume) |
GET /internal/v1/cves/kev | CISA KEV catalog, no tier field filtering (limit up to 10000) |
GET /internal/v1/cves/homepage-intel | Homepage intelligence bundle |
The /internal/v1/cves* group (added 2026-07-08) mirrors the public
/api/v1/cves read surface, unredacted and with higher limits, gated only by
PAI auth (require_pai_access + pai_enabled).
POST /internal/v1/packages/intel
Returns the internal package-version intelligence payload for a package coordinate. This is the PAI twin of the premium public package intelligence endpoint.
Gated by enable_package_intel_api (default false). When the flag is off
the endpoint returns 404.
{
"ecosystem": "Maven",
"name": "com.h2database:h2",
"version": "1.3.176"
}
| Field | Notes |
|---|---|
ecosystem, name, version, purl | Normalised package identity. |
cves | Version-filtered OSV CVE/advisory records. |
vulnerabilities | Alias of cves. |
malware | MPI and compromised-package intelligence. |
malware.scan_id, malware.signals, malware.iocs, malware.raw | Internal-only MPI evidence for Phoenix-owned services. |
ps_oss_score | Derived package risk score. |
source_attribution | Data sources used in the response. |
Gated by ENABLE_ADV_FETCH (default false). All return 404 when the flag is off.
| Endpoint | Purpose |
|---|---|
GET /internal/v1/cves/{cve_id}/advisory-patches | Advisory patch suggestions for a CVE |
GET /internal/v1/advisories/{advisory_id} | CVEs + full advisory records for an advisory ID (422 on malformed ID) |
GET /internal/v1/advisories/patch-month/{patch_month} | Advisory entries for a patch month (YYYY-MM; 422 on bad format, 400 when > 500 records) |
Added 2026-07-10. PAI mirrors of the five public /api/v1/analysis/* endpoints (see
PUBLIC_API.html). These routes are not themselves gated by
ENABLE_CWE_OWASP_THREAT_TIERED_API — that flag only governs whether the public
routes shape their payload. The PAI mirrors always return the full, unshaped payload
(AccessMode.FULL), regardless of the flag's state, because PAI access is itself the Enterprise
gate (require_pai_access) and there is no end-user tier context on a PAI call.
| Endpoint | Purpose |
|---|---|
GET /internal/v1/analysis/cwe | Full CWE analysis (mirror of /api/v1/analysis/cwe) |
GET /internal/v1/analysis/cwe-top25 | Full CWE Top 25 analysis |
GET /internal/v1/analysis/kev-cwe | Full KEV CWE analysis |
GET /internal/v1/analysis/owasp-top10 | Full OWASP Top 10 analysis |
GET /internal/v1/analysis/reference-threat-frequencies | Full, live reference threat-frequency distributions — note: this route always reads the true, current reference_threat_frequencies.json, unlike the public route, whose underlying data source is itself flag-gated (legacy snapshot when the flag is off). |
No additional tier parameter or header is required beyond the PAI key itself. Missing/unavailable
underlying data returns 404 (this router's existing convention — see
GET /internal/v1/eol-intelligence), not the public routes' 503.
Flag: enable_org_team_admin_scoping (default false). All endpoints in this section return 404 when the flag is off.
Auth: Global admin or org manager (is_org_manager Cognito group) for the target org — enforced by resolve_admin_scope + ensure_target_org_in_scope.
Related: docs/Individual_Feature/2026-06-29-org-team-tenancy-plan-3-tenancy-router.md
| Endpoint | Auth | Purpose |
|---|---|---|
GET /internal/v1/admin/organizations | global-admin or org-manager | List organizations. Non-global admins see only their own org. |
POST /internal/v1/admin/organizations | global-admin | Create org. Body: { slug, display_name, plan_tier, seat_limit }. 201. |
GET /internal/v1/admin/organizations/{org_id} | global-admin or org-manager | Get org. 404 when not found or out of scope. |
PATCH /internal/v1/admin/organizations/{org_id} | global-admin | Update org metadata (display_name, plan_tier, seat_limit, status). |
POST /internal/v1/admin/organizations/{org_id}/license | global-admin | Set org plan tier + seat limit. Audited. Body: { tier, seat_limit, product }. |
| Endpoint | Auth | Purpose |
|---|---|---|
GET /internal/v1/admin/organizations/{org_id}/teams | global-admin or org-manager | List teams. Non-global admins see only teams in their scope. |
POST /internal/v1/admin/organizations/{org_id}/teams | global-admin | Create team. Body: { slug, display_name, seat_cap? }. 201. 409 SLUG_TAKEN on duplicate. |
GET /internal/v1/admin/organizations/{org_id}/teams/{team_id} | global-admin or org-manager | Get team. Caller must have the team in scope. |
POST /internal/v1/admin/organizations/{org_id}/teams/{team_id}/license | global-admin | Set team tier override + seat cap. Tier clamped to org tier. |
DELETE /internal/v1/admin/organizations/{org_id}/teams/{team_id} | global-admin | Delete team. 204. |
GET /internal/v1/admin/organizations/{org_id}/membersAuth: Global admin or tenant_admin of that org. team_admin callers are auto-scoped to their own teams.
Query param: team_id (optional uuid) — filter by team. Overridden by auto-scoping for team_admin callers.
Response 200: { "members": [...] }
POST /internal/v1/admin/organizations/{org_id}/membersAuth: Global admin or tenant_admin of that org.
Add an existing user (by user_sub) to an org.
| Field | Type | Required | Notes |
|---|---|---|---|
user_sub | str | yes | Cognito user sub |
user_id | int | no | Internal DB user ID |
role | str | no | Default member. Values: member, viewer, team_admin, tenant_admin |
team_id | uuid | no | Assign to a specific team |
Errors: 400 invalid role; 403 TENANT_ADMIN_ESCALATION (tenant_admin caller cannot assign tenant_admin role); 409 seat limit.
Response 201: { "added": bool, "already_member": bool }
PATCH /internal/v1/admin/organizations/{org_id}/members/{user_sub}Auth: Global admin or tenant_admin of that org.
Update a member's role. Request body: { "role": "member|viewer|team_admin|tenant_admin" }
Errors: 400 invalid role; 403 TENANT_ADMIN_ESCALATION; 404 member not found; 409 LAST_ADMIN (cannot demote the last tenant_admin).
DELETE /internal/v1/admin/organizations/{org_id}/members/{user_sub}Auth: Global admin or tenant_admin of that org.
Remove a member. 409 LAST_ADMIN when removing the last tenant_admin. 204 on success.
PUT /internal/v1/admin/organizations/{org_id}/members/{user_sub}/teamReassign a member to a team. Enterprise orgs only (403 ENTERPRISE_REQUIRED for non-enterprise).
Request body: { "team_id": uuid }
| Endpoint | Auth | Purpose |
|---|---|---|
GET /internal/v1/admin/organizations/{org_id}/invitations | global-admin or org-manager | List invitations. team_admin auto-scoped. Query param: team_id. |
POST /internal/v1/admin/organizations/{org_id}/invitations | global-admin or tenant_admin | Create invite. Org admins cannot create tenant_admin invites. |
DELETE /internal/v1/admin/organizations/{org_id}/invitations/{invite_id} | global-admin or tenant_admin | Revoke invite. 204. 404 when not found. |
POST /internal/v1/admin/organizations/{org_id}/users — Org-scoped user creationCreate a brand-new user and enroll them in the org. Provisions the user in Cognito (sends invite email) and inserts a users DB row.
Auth: Global admin or tenant_admin of that org. team_admin callers always receive 403 TEAM_ADMIN_DIRECT_CREATE_FORBIDDEN.
| Field | Type | Required | Notes |
|---|---|---|---|
email | str (email) | yes | New user's email address |
full_name | str | yes | Display name |
role | str | yes | member, viewer, or team_admin. tenant_admin not permitted here. |
team_id | uuid | no | Assign to a team immediately |
industry | str | no | Default generic |
password | str | no | Temporary password; Cognito email invite sent regardless |
{ "user_sub": "cognito-sub-uuid", "role": "member", "org_id": "...", "team_id": null }
| Status | Code | Meaning |
|---|---|---|
| 400 | INVALID_ROLE | role is not member, viewer, or team_admin |
| 400 | — | Password does not meet Cognito policy |
| 403 | TEAM_ADMIN_DIRECT_CREATE_FORBIDDEN | Caller is a team_admin |
| 403 | TENANT_ADMIN_ESCALATION | Caller is tenant_admin and attempted to assign tenant_admin role |
| 404 | — | org_id not found or out of caller scope |
| 409 | SEAT_LIMIT | Organization has no available seats |
| 409 | USER_ALREADY_EXISTS | Email already registered in Cognito |
| Endpoint | Purpose |
|---|---|
GET /internal/v1/admin/org-scope | Return caller's org-admin scope. Response: { is_global_admin, is_org_manager, tenant_id, member_role, plan_tier }. Used by UI to gate the Organization-Team tab. |
GET /internal/v1/admin/member-teams | Map user_sub -> {org, team} across all tenants. Global admin only. |
Auth: require_admin (admin or global-admin Cognito group).
These are /api/v1/admin/ endpoints (not PAI). Documented here because their scoping behaviour changed as part of the org/team tenancy feature.
GET /api/v1/admin/registrationsList user registrations.
Scope: Global admin sees all rows. Non-global admins see only rows belonging to their primary org (get_user_primary_org). Non-global admins with no org membership receive an empty list.
Query parameters: status (optional), limit (default 100, max 1000), offset (default 0).
Response 200: Array of UserRegistrationListItem.
GET /api/v1/admin/registrations/pendingConvenience alias — equivalent to GET /registrations?status=pending. Same org-scoping rules.
Response 200: Array of UserRegistrationListItem with status=pending.
PAI calculation endpoints apply neutral defaults when parameters are omitted:
null and excluded via renormalization.unknown status.