OpenAI authentication, API keys & identity — atlas
Status: DOCUMENTED; bearer auth, org/project header semantics and the invalid-key / missing-auth / scope-error shapes are LIVE_VERIFIED (2026-09-18). Admin keys, service accounts, workload identity federation and mTLS are DOCUMENTED / ACCOUNT_RESTRICTED (not provisioned on our account).
Sources: API overview → Authentication · Production best practices → API keys · Admin APIs guide · Workload identity federation guide + token exchange reference · Mutual TLS · IP allowlist · Supported countries · Error codes · OpenAPI schemas ProjectApiKey, AdminApiKey, ProjectServiceAccount, Invite, Role.
Last verified: 2026-09-18.
1. Credential types
| Credential | Prefix / form | Scope | Created by | Notes |
|---|---|---|---|---|
| Project API key (user-owned) | sk-proj-… |
one project; inherits the owning user's project access (`owner_project_access: active | inactive` — key stops working if the user leaves) | user in dashboard |
| Project API key (service-account-owned) | sk-proj-… |
one project, not tied to a human | POST /v1/organization/projects/{p}/service_accounts (returns api_key.value once) or POST …/service_accounts/{id}/api_keys (expires_at optional) |
best for production workloads; service account role `owner |
| Legacy user API key | sk-… |
the user across all their orgs/projects → needs OpenAI-Organization / OpenAI-Project headers to pick the target |
dashboard (legacy) | usage untracked if created before 2023-12-20 |
| Admin API key | sk-admin-… (object: organization.admin_api_key, owner type: user) |
Administration endpoints only (/v1/organization/*, /v1/projects/*); refused elsewhere |
org owner at Settings → Organization → Admin keys or POST /v1/organization/admin_api_keys (name, optional expires_at) |
redacted_value: sk-admin...def; list/retrieve/delete via API |
| Workload identity access token | opaque/JWT bearer, ≤ 1 h | project + service account of the mapping; optional narrowing scope (e.g. api.model.read api.model.request) |
POST https://auth.openai.com/oauth/token (RFC 8693 token exchange) |
no refresh token; cannot call Admin endpoints, DELETE /v1/models/{id}, POST /v1/images/request_audit |
| X.509 workload identity | client certificate → bearer | same as above | POST https://mtls.auth.openai.com/oauth/token over mTLS, then API calls to mtls.api.openai.com with the certificate |
bearer not cert-bound (no DPoP/cnf) |
| Realtime ephemeral client secret | ek_… |
one Realtime session, short TTL | POST /v1/realtime/client_secrets |
for browsers (WebRTC / WS subprotocol openai-insecure-api-key.<ek>) |
| Webhook signing secret | whsec_… |
one webhook endpoint | endpoint creation / rotate_secret |
not an API credential; see docs/openai/webhooks.md |
2. Sending credentials
Authorization: Bearer <key or access token>
OpenAI-Organization: org-… # optional; only for multi-org users / legacy user keys
OpenAI-Project: proj_… # optional; only for legacy user keysObserved with a project key (2026-09-18): a mismatching OpenAI-Organization → 401 mismatched_organization ("OpenAI-Organization header should match organization for API key"); an unknown OpenAI-Project → 401 invalid_project ("No such project"). Bogus key → 401 invalid_api_key (type invalid_request_error); empty header → 401 "Missing bearer authentication in header" (code null). Revocation propagates within seconds; other auth changes ≤ 15 min.
Base URLs: https://api.openai.com/v1 (global), regional https://{us,eu,au,ca,jp,in,sg,kr,gb,ae}.api.openai.com/v1 (see data-residency-and-regions.md), https://mtls.api.openai.com/v1 (mTLS), wss://api.openai.com/v1/realtime (Realtime WS), https://auth.openai.com/oauth/token and https://mtls.auth.openai.com/oauth/token (token exchange).
SDK env vars: OPENAI_API_KEY, OPENAI_ADMIN_KEY, OPENAI_ORG_ID, OPENAI_PROJECT_ID, OPENAI_BASE_URL, OPENAI_WEBHOOK_SECRET, OPENAI_LOG.
3. Organization / project model & roles
| Level | Roles | Powers |
|---|---|---|
| Organization | owner — all reader rights + billing + members; reader — make API requests, view org info, create/update/delete resources |
Invite.role, OrganizationUser.role |
| Project | owner, member |
ProjectUser.role, ProjectServiceAccount.role |
| Custom roles | `Role{permissions[], resource_type: api.organization | api.project, predefined_role}; bound to users or groups (/organization/{users,groups}/{id}/roles, /projects/{p}/{users,groups}/{id}/roles`) |
| Groups | organization.group with members; can hold org or project roles; SCIM can provision (scim.enabled audit event) |
/v1/organization/groups |
Scopes (identifiers seen in docs/spec/error messages): admin-side api.management.read (+ write implied), api.usage.read, api.audit_logs.read, api.roles.read, api.groups.read/write, api.mtls.read/write, api.external_storage.read; API-side api.model.request, api.model.read, api.responses.write, api.files.read/write, api.batch.read/write, api.vector_store.read, api.videos.read/write, api.voices.read/write, api.agents.read/write, api.vaults.read/write, api.webhooks.read, api.traces.read, api.safety.read, api.safety.alerts.read, api.apps.read/write. A missing scope yields 403 ({"error": "<string>"} or code: insufficient_permissions) — audit logs yield 401.
4. Governance controls (documented)
| Control | Where | Effect |
|---|---|---|
| Key expiry | per key expires_at; org/project max key lifetime (project ≤ org) |
new keys must expire within the limit |
| API Key Governance | org/project settings | allow only service-account keys / only user keys / block new key creation; org restrictions win |
| IP allowlist | Settings → Organization → Security → IP allowlist | outside IP → 401 ip_not_authorized; audit events ip_allowlist.* |
| Model permissions | POST /v1/organization/projects/{p}/model_permissions (allow_list / deny_list) |
denied model → 404 model_not_found |
| Hosted tool permissions | …/hosted_tool_permissions |
restrict web search / code interpreter / MCP etc. |
| Spend limits & alerts | org/project spend_limit, spend_alerts |
429 organization_spend_limit_exceeded / project_spend_limit_exceeded |
| Mutual TLS | certificates API + mtls.api.openai.com host; CEL filters on subject fields |
enforce client certs per org/project |
| Supported countries | policy page (≈ 190 countries/territories; e.g. no China, Russia, Iran, North Korea, Hong Kong, Belarus, Venezuela) | 403 unsupported_country_region_territory; Ukraine "with certain exceptions" |
| Data residency / retention | project region + retention policy | see data-residency-and-regions.md |
5. Workload identity federation (WIF) — quick reference
- Org owner creates a Workload Identity Provider (OIDC issuer + audience + JWKS, or X.509 using active mTLS roots) — max 50 providers/org, 50 mappings/provider. Supported issuers documented: AWS, GCP, Azure/Entra, GitHub Actions, Kubernetes, Oracle Cloud, SPIFFE (JWT-SVID only), X.509; arbitrary OIDC issuers not yet supported.
- Add a service account mapping (attribute conditions on raw claims like
sub,repository, or derivedopenai.*attributes via CEL; optional Permissions = scopes). Exactly one enabled mapping must match. - Workload exchanges its token:
curl https://auth.openai.com/oauth/token -H "Content-Type: application/json" -d '{
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt", # or …:id_token ; X.509: urn:openai:params:oauth:token-type:x509 (no subject_token)
"subject_token": "'"$EXTERNAL_JWT"'",
"identity_provider_id": "'"$IDP_ID"'", "service_account_id": "'"$SA_ID"'" }'
# → {"access_token":"…","issued_token_type":"urn:ietf:params:oauth:token-type:access_token","token_type":"Bearer","expires_in":3600,"expires_at":1789045200,"scope":"api.model.read api.model.request"}- Use
Authorization: Bearer <access_token>; renew beforeexpires_at(token never outlives the subject token / certificate; lifetime ≤ 1 h; no refresh token). Errors:invalid_subject_token(JWT/cert verification),invalid_grant(attribute conditions, mapping, provider), missing/unsupported parameters;scopein the request grants nothing. Audit events:workload_identity_provider.*,workload_identity_provider_mapping.*,tenant.workload_identity.*. SDKs: PythonOpenAI(workload_identity=…), Nodenew OpenAI({ workloadIdentity })(openai/auth/subject-token-providers), both mutually exclusive withapiKey.
6. Key hygiene (from the docs + this atlas' rules)
Load keys from env/secret managers, never client-side; rotate on a schedule (create → switch → verify → revoke); log x-request-id; prefer service-account keys with expiry for servers; use restricted keys (scopes) for narrow tasks; monitor with GET /v1/organization/usage/* + audit logs (api_key.created/updated/deleted, login.failed).