guides/workload-identity-federation/admin-api.md +0 −348 deleted
File Deleted View Diff
1# Manage Codex workload identity with the Admin API
2
3> For the complete documentation index, see [llms.txt](/llms.txt). Markdown versions of documentation pages are available by appending `.md` to the page URL.
4
5Use the organization Admin API to manage Codex workload identity providers and
6federation rules from infrastructure tooling or CI. The API exposes the same
7provider and rule model as the OpenAI Admin Portal.
8
9The API calls federation rules `mappings` in paths and response objects. This
10page uses **federation rule** for the product concept and `mapping` only when it
11refers to an API field or path.
12
13These endpoints manage the Codex workload identity federation beta for managed
14 ChatGPT workspaces. To request access, contact your OpenAI representative or
15 [OpenAI
16 Support](https://help.openai.com/en/articles/6614161-how-can-i-contact-support).
17 These endpoints do not replace the existing OpenAI API workload identity
18 provider and service account mapping APIs.
19
20## Prerequisites
21
22You need:
23
24- Workload identity federation enabled for your organization and managed
25 ChatGPT workspace.
26- An [Admin API key](https://platform.openai.com/settings/organization/admin-keys)
27 whose owner is an active administrator allowed to manage workload identity.
28- The ID of the managed ChatGPT workspace.
29- The OpenAI user ID of an existing active human or service account in that
30 workspace.
31- The issuer, audience, and claims for the workload's OIDC token or SPIFFE
32 JWT-SVID.
33
34The WIF endpoints use resource IDs instead of names. They do not list or create
35ChatGPT workspaces or principals. Supply those IDs from your provisioning system.
36If you do not manage those resources programmatically, use the OpenAI Admin
37Portal to create or select the principal and connect that workload.
38
39Set the Admin API key in your environment:
40
41```bash
42export OPENAI_ADMIN_KEY="<admin-api-key>"
43```
44
45Admin API keys are long-lived credentials. Store the key in a secrets manager,
46do not commit it, and do not use it for Codex runtime authentication.
47
48## Endpoints
49
50All requests use `https://api.openai.com` and an Admin API key in the bearer
51authorization header.
52
53| Operation | Method and path |
54| ---------------------------- | ----------------------------------------------------------------------------------------- |
55| List providers | `GET /v1/organization/workload_identity/providers` |
56| Create a provider | `POST /v1/organization/workload_identity/providers` |
57| Get a provider | `GET /v1/organization/workload_identity/providers/{provider_id}` |
58| Update or disable a provider | `POST /v1/organization/workload_identity/providers/{provider_id}` |
59| Archive a provider | `DELETE /v1/organization/workload_identity/providers/{provider_id}` |
60| List rules | `GET /v1/organization/workload_identity/providers/{provider_id}/mappings` |
61| Create a rule | `POST /v1/organization/workload_identity/providers/{provider_id}/mappings` |
62| Get a rule | `GET /v1/organization/workload_identity/providers/{provider_id}/mappings/{mapping_id}` |
63| Update or disable a rule | `POST /v1/organization/workload_identity/providers/{provider_id}/mappings/{mapping_id}` |
64| Archive a rule | `DELETE /v1/organization/workload_identity/providers/{provider_id}/mappings/{mapping_id}` |
65
66List responses use `{ "object": "list", "data": [...] }`. The endpoints do
67not use pagination.
68
69## Create an OIDC provider
70
71Create one provider for each issuer and trust boundary that you want to manage
72independently. Replace the example issuer and audience with exact values from a
73sample token. Inspect the token's `iat` and `exp` claims locally, then choose an
74accepted assertion lifetime that covers the issuer's expected `exp - iat`
75range. OpenAI checks that full duration, not the token's remaining validity.
76
77For Microsoft Entra, do not assume a one-hour assertion. [Access-token lifetimes
78vary](https://learn.microsoft.com/en-us/entra/identity-platform/access-tokens#token-lifetime),
79and Microsoft does not support [configuring managed-identity token
80lifetimes](https://learn.microsoft.com/en-us/entra/identity-platform/configurable-token-lifetimes).
81Replace `MAX_ASSERTION_LIFETIME_SECONDS` with an approved integer from 1 through
82176,400. This provider limit is separate from the lifetime of the OpenAI access
83token that a federation rule issues.
84
85```bash
86MAX_ASSERTION_LIFETIME_SECONDS="<accepted-issuer-lifetime-seconds>"
87
88jq -n \
89 --argjson max_assertion_lifetime_seconds "$MAX_ASSERTION_LIFETIME_SECONDS" \
90 '{
91 name: "entra-production",
92 type: "oidc",
93 issuer: "https://login.microsoftonline.com/00000000-0000-0000-0000-000000000000/v2.0",
94 audience: "api://openai-codex-production",
95 description: "Production Codex workloads in Microsoft Azure",
96 max_assertion_lifetime_seconds: $max_assertion_lifetime_seconds,
97 check_jti: true
98 }' > provider.json
99
100curl --fail-with-body --silent --show-error \
101 https://api.openai.com/v1/organization/workload_identity/providers \
102 -H "Authorization: Bearer $OPENAI_ADMIN_KEY" \
103 -H "Content-Type: application/json" \
104 --data @provider.json \
105 --output provider-response.json
106
107PROVIDER_ID="$(jq -r .id provider-response.json)"
108printf 'Created provider %s\n' "$PROVIDER_ID"
109```
110
111Expected output begins with an identity-provider ID:
112
113```text
114Created provider idp_...
115```
116
117By default, an OIDC provider uses discovery at its issuer URL. Use `custom_url`
118when the public discovery document lives elsewhere, `jwks_uri` for an
119explicit public JWKS URL, or `jwks_local: true` with `jwks` to upload public
120keys. Do not include private key material.
121
122## Create a SPIFFE JWT-SVID provider
123
124Set `type` to `spiffe_jwt`, set `issuer` to the canonical trust domain, and
125provide either a public bundle URL or an uploaded SPIFFE bundle. A SPIFFE rule
126must also set `audiences`.
127
128```json
129{
130 "name": "spiffe-production",
131 "type": "spiffe_jwt",
132 "issuer": "spiffe://example.com",
133 "jwks_uri": "https://spiffe.example.com/bundle.json",
134 "max_assertion_lifetime_seconds": 3600,
135 "check_jti": true
136}
137```
138
139For an uploaded bundle, set `jwks_local` to `true`, replace `jwks_uri` with the
140`jwks` object, and include at least one public key whose `use` is `jwt-svid`.
141
142## Create a federation rule
143
144A rule targets one existing principal and can match one or many external
145workload identities. This example accepts one Azure managed identity subject:
146
147```bash
148export WORKSPACE_ID="<managed-chatgpt-workspace-id>"
149export PRINCIPAL_ID="<existing-openai-user-id>"
150
151jq -n \
152 --arg workspace_id "$WORKSPACE_ID" \
153 --arg principal_id "$PRINCIPAL_ID" \
154 '{
155 name: "entra-payments-production",
156 description: "Production payments workload",
157 workspace_id: $workspace_id,
158 principal_id: $principal_id,
159 external_subject: "11111111-2222-3333-4444-555555555555",
160 audiences: ["api://openai-codex-production"],
161 access_token_lifetime_seconds: 600,
162 enabled: true
163 }' > rule.json
164
165curl --fail-with-body --silent --show-error \
166 "https://api.openai.com/v1/organization/workload_identity/providers/$PROVIDER_ID/mappings" \
167 -H "Authorization: Bearer $OPENAI_ADMIN_KEY" \
168 -H "Content-Type: application/json" \
169 --data @rule.json \
170 --output rule-response.json
171
172FEDERATION_RULE_ID="$(jq -r .id rule-response.json)"
173printf 'Created federation rule %s\n' "$FEDERATION_RULE_ID"
174```
175
176Expected output begins with a mapping ID. This is the value Codex uses as
177`OPENAI_FEDERATION_RULE_ID`:
178
179```text
180Created federation rule idpm_...
181```
182
183For a set of allowed subjects in one rule, omit `external_subject` and use a CEL
184condition:
185
186```json
187{
188 "condition": "assertion.sub in [\"workload-a\", \"workload-b\"]"
189}
190```
191
192Set at least one of `external_subject`, `claims`, or `condition`. All configured
193identity checks must pass. See the [federation rule
194reference](https://developers.openai.com/api/docs/guides/workload-identity-federation/federation-rules) for
195cardinality, CEL, audience, scope, and lifetime behavior.
196
197## List and reconcile resources
198
199List providers before creating one so your automation can compare the intended
200configuration with the current state:
201
202```bash
203curl --fail-with-body --silent --show-error \
204 https://api.openai.com/v1/organization/workload_identity/providers \
205 -H "Authorization: Bearer $OPENAI_ADMIN_KEY" | jq .
206```
207
208Then list rules under a provider:
209
210```bash
211curl --fail-with-body --silent --show-error \
212 "https://api.openai.com/v1/organization/workload_identity/providers/$PROVIDER_ID/mappings" \
213 -H "Authorization: Bearer $OPENAI_ADMIN_KEY" | jq .
214```
215
216The API does not define an idempotency-key contract. Store returned IDs in your
217approved configuration state, read the current resource before changing it,
218and update by ID. Do not create a replacement on every run.
219
220## Update or disable a resource
221
222Updates use `POST` with only the fields you want to change. This example changes
223the rule lifetime:
224
225```bash
226curl --fail-with-body --silent --show-error \
227 -X POST \
228 "https://api.openai.com/v1/organization/workload_identity/providers/$PROVIDER_ID/mappings/$FEDERATION_RULE_ID" \
229 -H "Authorization: Bearer $OPENAI_ADMIN_KEY" \
230 -H "Content-Type: application/json" \
231 -d '{"access_token_lifetime_seconds": 300}' | jq .
232```
233
234Disable a rule for an immediate stop:
235
236```bash
237curl --fail-with-body --silent --show-error \
238 -X POST \
239 "https://api.openai.com/v1/organization/workload_identity/providers/$PROVIDER_ID/mappings/$FEDERATION_RULE_ID" \
240 -H "Authorization: Bearer $OPENAI_ADMIN_KEY" \
241 -H "Content-Type: application/json" \
242 -d '{"enabled": false}' | jq .
243```
244
245Set `enabled` to `false` on the provider path to stop every rule under that
246provider. Disablement blocks new exchanges and revokes access tokens issued
247through the resource. You can turn it back on after its principal, workspace,
248binding, and provider are active.
249
250Ordinary rule edits affect only new exchanges. Tokens issued before the edit can
251remain valid until their TTL ends. Provider trust edits revoke issued tokens
252before the new trust takes effect.
253
254## Archive a resource
255
256`DELETE` archives a provider or rule instead of erasing it. Archival blocks new
257exchanges, revokes issued tokens, hides the resource from normal list results,
258and cannot be undone.
259
260Archive a rule:
261
262```bash
263curl --fail-with-body --silent --show-error \
264 -X DELETE \
265 "https://api.openai.com/v1/organization/workload_identity/providers/$PROVIDER_ID/mappings/$FEDERATION_RULE_ID" \
266 -H "Authorization: Bearer $OPENAI_ADMIN_KEY"
267```
268
269Archive a provider:
270
271```bash
272curl --fail-with-body --silent --show-error \
273 -X DELETE \
274 "https://api.openai.com/v1/organization/workload_identity/providers/$PROVIDER_ID" \
275 -H "Authorization: Bearer $OPENAI_ADMIN_KEY"
276```
277
278Archiving a provider revokes access for its Codex rules. You must remove any
279non-Codex product mapping before you can archive that provider. This protects
280existing OpenAI API workload identity configuration.
281
282## Provider fields
283
284Create requires `name` and `issuer`. Update accepts the mutable fields except
285`type`.
286
287| Field | Type and behavior |
288| -------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
289| `name` | Non-empty display name. |
290| `type` | `oidc` by default, or `spiffe_jwt`. You cannot change it after creation. |
291| `issuer` | Exact OIDC `iss` URL or canonical SPIFFE trust domain. |
292| `audience` | Optional provider-level audience. Set a rule audience when this is absent. |
293| `description` | Optional administrator description. |
294| `custom_url` | Optional public HTTPS OIDC discovery URL. OIDC only. |
295| `jwks_uri` | Optional public HTTPS JWKS or SPIFFE bundle URL. |
296| `jwks_local` | Set to `true` when supplying `jwks`. |
297| `jwks` | Uploaded public JWKS object, up to 100 keys and 1 MiB. |
298| `custom_ca_certificate` | Optional PEM CA bundle for JWKS HTTPS, up to 256 KiB. |
299| `attribute_conditions` | Optional bounded CEL condition applied before rule matching. Use `assertion` for the verified claims. |
300| `max_assertion_lifetime_seconds` | Accepted upstream assertion lifetime, 1 through 176,400 seconds. OIDC uses the full `exp - iat`. Default: 3,600. |
301| `check_jti` | When `true`, reject a repeated non-empty JWT `jti`. Default: `false`. |
302| `enabled` | Update-only switch that accepts or blocks exchanges. |
303
304Discovery and explicit or uploaded keys are alternative verification modes.
305Issuer, discovery, and JWKS URLs have validation requirements described in the
306[workload identity overview](https://developers.openai.com/api/docs/guides/workload-identity-federation#manage-jwks-and-key-rotation).
307
308## Federation rule fields
309
310Create requires `workspace_id` and `principal_id`, plus at least one identity
311check. You cannot change the workspace or principal after creation.
312
313| Field | Type and behavior |
314| ------------------------------- | ---------------------------------------------------------------------------------------------------- |
315| `workspace_id` | Existing managed ChatGPT workspace ID. Create-only. |
316| `principal_id` | Existing active OpenAI user or service-account ID in the workspace. Create-only. |
317| `external_subject` | Exact `sub` or one trailing-`*` prefix, up to 4,096 bytes. |
318| `claims` | Up to 32 exact top-level scalar claims. Do not include `sub`. |
319| `audiences` | One through 32 unique accepted audiences. Required for SPIFFE and when the provider has no audience. |
320| `condition` | Bounded CEL boolean condition over `assertion`, up to 16 KiB. |
321| `scopes` | Optional subset of the four supported Codex scopes. Omit to use the default set. |
322| `access_token_lifetime_seconds` | 60 through 3,600 seconds. Default: 3,600. |
323| `name` | Optional display name. |
324| `description` | Optional administrator description. |
325| `enabled` | Whether the rule accepts exchanges. Default: `true`. |
326
327Provider responses use `workload_identity_provider`; rule responses use
328`workload_identity_mapping`. Both include `id`, `enabled`, `created_at`, and
329`updated_at`. Timestamps are Unix seconds.
330
331## Limits and errors
332
333An organization can have up to 50 non-archived providers. A provider can have up
334to 50 non-archived rules. The API returns:
335
336- `400` for request field errors, provider trust settings, rule conditions, scopes, or
337 inactive principal membership.
338- `403` when the Admin API key owner cannot manage workload identity.
339- `404` when the organization has no tenant association or the requested
340 resource is outside the organization and tenant boundary.
341- `409` for provider or rule limits, subject conflicts, inactive bindings, or
342 lifecycle conflicts.
343
344Treat `404` as non-disclosing: the service does not reveal a provider or rule
345owned by another organization or tenant. Retry transient `429` and `5xx`
346responses with bounded delays that increase after each attempt. Do not retry a
347validation or permission error without changing the request or administrator
348state.