PAI Access
How to generate, use, and revoke Phoenix Internal API (PAI) credentials
What is PAI
PAI (Phoenix Internal API) is the unrestricted internal counterpart to the public
/api/v1/* surface, served under /internal/v1/*. It exposes full,
un-shaped scoring and enrichment data (no tier-based percentage shaping or field
redaction) for other Phoenix services and trusted automation to consume. Because PAI
bypasses data_shield.py tier enforcement, every credential and every
/internal/v1/* call is gated to global admins only.
Generating a key pair (global-admin only)
PAI credentials are a two-part pair, generated together and shown on screen exactly once:
- API key (
phx_pai_...) — the identifier. - Companion token (
phx_pait_...) — a separate high-entropy secret. Both are required on the simple call path; leaking one alone is not enough to authenticate.
- Sign in as a global admin and open Admin Dashboard.
- Select the API Keys tab.
- Fill in an optional name and optional IP/domain restrictions, then submit.
- The API key and companion token are displayed once in the "Generated" panel — copy both immediately. A best-effort email with the companion token is also sent to your admin account (delivery failures never block generation).
Call path 1: simple headers
Best for occasional callers. Send both secrets as headers on every request to any
/internal/v1/* endpoint:
curl https://phxintel.security/internal/v1/scoring-weights \ -H "x-api-key: phx_pai_..." -H "x-pai-token: phx_pait_..."
The server checks, in order: IP/domain allowlist (if configured on the key) → API key hash match → companion token hash match → key not revoked.
Call path 2: session JWT
Best for high-volume callers — exchange the root pair once for a short-lived signed session token, then replay only the token on subsequent calls instead of the long-lived secrets:
TOKEN=$(curl -s -X POST https://phxintel.security/internal/v1/pai/session \ -H "x-api-key: phx_pai_..." -H "x-pai-token: phx_pait_..." | jq -r .access_token) curl https://phxintel.security/internal/v1/scoring-weights \ -H "Authorization: Bearer $TOKEN"
The session token is a signed JWT (default TTL: 900 seconds) carrying the key's own IP/domain allowlist, so the same per-key restrictions apply on both paths. Sessions cannot be re-minted from another session token — only from the original API key + companion token pair.
POST /internal/v1/pai/session returns 404.
IP and domain restrictions
When generating a key, you may optionally scope it to specific caller IP addresses or domains. Restrictions are enforced identically on the simple-header path and embedded into minted session JWTs, so a stolen session token cannot be replayed from an unlisted origin. Leaving the fields blank allows calls from any origin within the global PAI allowlist.
Revocation
From the API Keys tab's PAI keys list, click Revoke next to any key. Revocation:
- Immediately fails the simple header path (both secrets stop working).
- Bumps the key's revocation epoch, so any outstanding session JWTs fail on their next use — there is no need to wait for them to expire.
Security notes
- Both secrets are hashed at rest (never stored or logged in plaintext); comparisons are constant-time.
- Audit logs record only the key's trailing suffix, never the full key or companion token.
- Generation, listing, and revocation are all gated server-side by
require_global_admin— this page's client-side check is a UX convenience only, not the security boundary.