Phoenix Internal API (PAI)

PAI is the privileged internal API plane. It is not publicly reachable and is protected by:

Restricted: Access is limited to Phoenix Global Admins and Phoenix-owned services only.

Base URL

/internal/v1

Swagger

Internal Swagger UI (requires PAI key + allowlist):

Authentication

Provide a PAI key via one of:

Key Generation

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.

Core Endpoints

Endpoint Purpose
GET /internal/v1/cve/{cve_id}Full CVE details + raw NVD record
GET /internal/v1/cve/{cve_id}/github-pocsFull (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-profileFull high-profile list (no redaction)
GET /internal/v1/enterprise-watchlistFull watchlist entries
POST /internal/v1/packages/intelInternal package-version CVE + MPI malware intelligence
POST /internal/v1/calculate-scoreCustom PS-HP calculation
POST /internal/v1/calculate-pss-scoreCustom PS-OSS (PSS) calculation
POST /internal/v1/calculate-phs-scoreCustom PS-PHS calculation
POST /internal/v1/calculate-pvs-scoreCustom PS-PVS calculation
POST /internal/v1/calculate-adqe-scoreCustom ADQE calculation
POST /internal/v1/calculate-eol-riskCustom EOL SLR risk calculation
GET /internal/v1/scoring-weightsPS-HP/PS-OSS weights
GET /internal/v1/threat-actorsThreat actor intelligence
GET /internal/v1/eol-intelligenceFull EOL intelligence
GET /internal/v1/analysis/cweFull CWE analysis (mirror of /api/v1/analysis/cwe)
GET /internal/v1/analysis/cwe-top25Full CWE Top 25 analysis
GET /internal/v1/analysis/kev-cweFull KEV CWE analysis
GET /internal/v1/analysis/owasp-top10Full OWASP Top 10 analysis
GET /internal/v1/analysis/reference-threat-frequenciesFull, live reference threat-frequency distributions
GET /internal/v1/cvesUnrestricted CVE search — public filters plus sort, published_after/published_before, has_analysis; limit up to 1000; unredacted
GET /internal/v1/cves/by-productUnrestricted vendor/product CVE enumeration (limit up to 2000)
GET /internal/v1/cves/trendingTrending CVEs (news/social mention volume)
GET /internal/v1/cves/kevCISA KEV catalog, no tier field filtering (limit up to 10000)
GET /internal/v1/cves/homepage-intelHomepage 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).

Package Intelligence

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.

Request

{
  "ecosystem": "Maven",
  "name": "com.h2database:h2",
  "version": "1.3.176"
}

Response fields

FieldNotes
ecosystem, name, version, purlNormalised package identity.
cvesVersion-filtered OSV CVE/advisory records.
vulnerabilitiesAlias of cves.
malwareMPI and compromised-package intelligence.
malware.scan_id, malware.signals, malware.iocs, malware.rawInternal-only MPI evidence for Phoenix-owned services.
ps_oss_scoreDerived package risk score.
source_attributionData sources used in the response.

Advisory Endpoints

Gated by ENABLE_ADV_FETCH (default false). All return 404 when the flag is off.

Endpoint Purpose
GET /internal/v1/cves/{cve_id}/advisory-patchesAdvisory 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)

CWE / OWASP Top 10 / Threat-Frequency Analysis Endpoints

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/cweFull CWE analysis (mirror of /api/v1/analysis/cwe)
GET /internal/v1/analysis/cwe-top25Full CWE Top 25 analysis
GET /internal/v1/analysis/kev-cweFull KEV CWE analysis
GET /internal/v1/analysis/owasp-top10Full OWASP Top 10 analysis
GET /internal/v1/analysis/reference-threat-frequenciesFull, 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.

Org/Team Admin Tenancy

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

Organizations

EndpointAuthPurpose
GET /internal/v1/admin/organizationsglobal-admin or org-managerList organizations. Non-global admins see only their own org.
POST /internal/v1/admin/organizationsglobal-adminCreate org. Body: { slug, display_name, plan_tier, seat_limit }. 201.
GET /internal/v1/admin/organizations/{org_id}global-admin or org-managerGet org. 404 when not found or out of scope.
PATCH /internal/v1/admin/organizations/{org_id}global-adminUpdate org metadata (display_name, plan_tier, seat_limit, status).
POST /internal/v1/admin/organizations/{org_id}/licenseglobal-adminSet org plan tier + seat limit. Audited. Body: { tier, seat_limit, product }.

Teams

EndpointAuthPurpose
GET /internal/v1/admin/organizations/{org_id}/teamsglobal-admin or org-managerList teams. Non-global admins see only teams in their scope.
POST /internal/v1/admin/organizations/{org_id}/teamsglobal-adminCreate 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-managerGet team. Caller must have the team in scope.
POST /internal/v1/admin/organizations/{org_id}/teams/{team_id}/licenseglobal-adminSet team tier override + seat cap. Tier clamped to org tier.
DELETE /internal/v1/admin/organizations/{org_id}/teams/{team_id}global-adminDelete team. 204.

Members

GET /internal/v1/admin/organizations/{org_id}/members

Auth: 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}/members

Auth: Global admin or tenant_admin of that org.

Add an existing user (by user_sub) to an org.

FieldTypeRequiredNotes
user_substryesCognito user sub
user_idintnoInternal DB user ID
rolestrnoDefault member. Values: member, viewer, team_admin, tenant_admin
team_iduuidnoAssign 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}/team

Reassign a member to a team. Enterprise orgs only (403 ENTERPRISE_REQUIRED for non-enterprise).

Request body: { "team_id": uuid }

Invitations

EndpointAuthPurpose
GET /internal/v1/admin/organizations/{org_id}/invitationsglobal-admin or org-managerList invitations. team_admin auto-scoped. Query param: team_id.
POST /internal/v1/admin/organizations/{org_id}/invitationsglobal-admin or tenant_adminCreate invite. Org admins cannot create tenant_admin invites.
DELETE /internal/v1/admin/organizations/{org_id}/invitations/{invite_id}global-admin or tenant_adminRevoke invite. 204. 404 when not found.

POST /internal/v1/admin/organizations/{org_id}/users — Org-scoped user creation

Create 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.

Request body

FieldTypeRequiredNotes
emailstr (email)yesNew user's email address
full_namestryesDisplay name
rolestryesmember, viewer, or team_admin. tenant_admin not permitted here.
team_iduuidnoAssign to a team immediately
industrystrnoDefault generic
passwordstrnoTemporary password; Cognito email invite sent regardless

Response 201

{ "user_sub": "cognito-sub-uuid", "role": "member", "org_id": "...", "team_id": null }

Errors

StatusCodeMeaning
400INVALID_ROLErole is not member, viewer, or team_admin
400Password does not meet Cognito policy
403TEAM_ADMIN_DIRECT_CREATE_FORBIDDENCaller is a team_admin
403TENANT_ADMIN_ESCALATIONCaller is tenant_admin and attempted to assign tenant_admin role
404org_id not found or out of caller scope
409SEAT_LIMITOrganization has no available seats
409USER_ALREADY_EXISTSEmail already registered in Cognito

Caller Scope Helpers

EndpointPurpose
GET /internal/v1/admin/org-scopeReturn 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-teamsMap user_sub -> {org, team} across all tenants. Global admin only.

Admin Registrations (org-scoped)

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/registrations

List 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/pending

Convenience alias — equivalent to GET /registrations?status=pending. Same org-scoping rules.

Response 200: Array of UserRegistrationListItem with status=pending.

Neutral Defaults

PAI calculation endpoints apply neutral defaults when parameters are omitted: