GPTMap

openai-node v7.20.0 Explained: Environment-Variable Vault Credentials, External Storage, and Safety Cases

Six PRs in one openai-node release: environment_variable vault credentials, external storage management, safety case retrieval with two webhook events, a SIP media security field, and a legacy GET fix — each traced to PR and tag sources.

TL;DR
openai-node v7.20.0 (2026-09-19, verified): vault credentials gain environment_variable (sandbox sees a placeholder; proxy swaps in the secret on 443/8443); client.admin.organization.externalStorage manages customer storage (AWS/Azure); safety cases land with warning/deactivation webhook events; incoming-call events gain optional sip_media_security (rtp/srtp); legacy GET options fixed (#2771).
openai-node v7.20.0 (2026-09-19) is a feature release of the official JavaScript/TypeScript SDK: the vault credential system gains the environment_variable auth type (placeholder injection with proxy-side secret substitution), the admin namespace gains a customer-managed external storage configuration resource (AWS/Azure), the safety namespace gains a case retrieval endpoint plus two matching webhook events (safety.warning_issued / safety.deactivation_issued), three incoming-call events gain an optional sip_media_security field, and legacy GET calls no longer serialize request options into the query string.

openai-node shipped v7.20.0 on 2026-09-19 with six PRs in a single release — five API features and one fix. The features extend three lines that already had foundations: the Agents API vault credential system gains a third auth type, the admin namespace gains an external storage management surface, and the safety family extends from alerts to a case retrieval endpoint with two matching webhook events. Alongside those come a SIP media security field for incoming-call events and a wide-reaching fix for legacy GET calls. This article walks each item back to its PR and type sources (all verified on 2026-09-20 via the GitHub API against release notes, PR diffs, and the v7.20.0 tag); example code comes from the official doc strings and type definitions in the tag.

As of 2026-09-20 (checked via GitHub releases), the latest openai-python release remains v3.16.2 (2026-09-18); none of the six changes in this batch have appeared in the Python release notes yet.

1. Release at a Glance

PRTypeWhat it addsPrimary source file
#2768featenvironment_variable vault credential type with networking configbeta/agents/vaults/credentials.ts
#2773featadmin external storage configuration management (AWS/Azure)admin/organization/external-storage.ts
#2774featsafety case retrieval endpointsafety/cases.ts, safety/safety.ts
#2772featsafety.warning_issued / safety.deactivation_issued webhook eventswebhooks/webhooks.ts
#2770featoptional sip_media_security on three incoming-call eventswebhooks/webhooks.ts
#2771fixpreserve request options in legacy GET calls57 source files (see section 6)

2. A Third Vault Credential Type: environment_variable (#2768)

Until now, Agents API vault credentials had two auth types: mcp_oauth (OAuth credentials with refresh metadata) and static_bearer (static bearer tokens). PR #2768 expands the CredentialAuth union to three values, adding environment_variable — officially described as an HTTP credential used only in OpenAI-hosted environments. It works fundamentally differently from the other two:

  1. At creation time, the real secret (secret_value) is stored in the vault.
  2. Sandbox code receives a placeholder in the environment variable, not the secret — injected under the name given by secret_name.
  3. Code uses that variable unchanged in outgoing requests.
  4. The egress proxy substitutes the placeholder with the real secret for allowed HTTPS destinations on ports 443 and 8443.

The official doc string draws the boundary explicitly: the real secret is never returned in the credential resource, is not available to sandbox code for local computation, and the doc string names signing a request as an example of what it cannot be used for.

const credential = await client.beta.agents.vaults.credentials.create('vault_id', {
  name: 'service-api-key',
  auth: {
    type: 'environment_variable',
    secret_name: 'SERVICE_API_KEY',
    secret_value: process.env.SERVICE_API_KEY ?? '',
    networking: { type: 'limited', allowed_hosts: ['api.example.com'] },
  },
});
// credential.object === 'vault.credential'

networking: Two Layers of Constraints on Substitution Destinations

The environment_variable type requires a networking field with exactly one of two values:

ValueMeaningAdditional requirement
unrestrictedSubstitution allowed for destinations permitted by the environment network policyRequires environment.network.access set to restricted with explicit allowed_domains
limitedSubstitution only for the listed hostsallowed_hosts: 1-16 distinct hostnames or IPv4 addresses (lowercase, no scheme/path/port/wildcard; IPv6 unsupported)

The doc string is equally explicit about what networking does not do: these permissions do not grant network access to the environment. The environment's own network policy remains independently in effect, and both layers must allow a destination before the substitution path works.

Remaining constraints (all from the tag's type doc strings): the credential name must be 1-256 UTF-8 bytes after trimming; secret_name accepts ASCII letters, digits, and underscores, must start with a letter or underscore (e.g., SERVICE_API_KEY), with the CODEX_ prefix and managed proxy/certificate variable names reserved; secret_value is write-only, must be nonempty, and must not contain carriage returns, newlines, or NUL bytes; update is a rotation semantic (CredentialAuthRotateParam) and supports the new type. The resource remains at client.beta.agents.vaults.credentials with the same five operations — create / retrieve / update / list (cursor pagination) / delete — and the OpenAI-Beta: agents=v1 header.

3. External Storage Management: client.admin.organization.externalStorage (#2773)

PR #2773 adds an external storage resource under the admin namespace. The create doc string is a single sentence: "Register one customer-managed external storage configuration." Five operations map to five endpoints:

  • create — POST /organization/external_storage
  • retrieve — GET /organization/external_storage/{id}
  • list — GET /organization/external_storage (cursor pagination with after / order (asc, desc) / project_id)
  • delete — DELETE /organization/external_storage/{id}
  • validate — POST /organization/external_storage/{id}/validate

The configuration object (object always organization.external_storage) carries id, created_at, geography, project_id, provider, and a three-value status enum: pending / validated / unhealthy. Providers come in two flavors:

FieldAWSAzure
Required at creationbucket, role_arnaccount_name, container, resource_group, subscription_id, tenant_id
Returned on the resourceplus account_id, external_id, regionplus region

The SDK doc string offers only that one-line positioning for create; how this resource binds to specific products is not explained inside the SDK, and as of 2026-09-20 this site has not verified further details from a loadable source, so no inference is made here. Authentication follows the rest of the admin namespace (adminAPIKeyAuth).

// Official JSDoc example (from external-storage.ts)
const externalStorageConfiguration =
  await client.admin.organization.externalStorage.create({
    project_id: 'proj_123',
    provider: {
      bucket: 'bucket',
      role_arn: 'role_arn',
      type: 'aws',
    },
  });

4. Safety Cases: Retrieval Endpoint and Two New Webhook Events (#2774 / #2772)

The 2026-09-03 SDK batch landed the safety family's first half: GET /v1/safety/alerts/{id} plus the safety.alert.created / safety.org_alert.created events. This batch's #2774 adds a cases sub-resource alongside alerts under client.safety:

const safetyCase = await client.safety.cases.retrieve('case_id');
// GET /v1/safety/cases/{id}
// safetyCase.object === 'safety.case'
// safetyCase.notice.type === 'warning' | 'deactivation'
// safetyCase.reason === string | null

The returned SafetyCase object carries id, created_at, entity_identifier, notice (with a warning / deactivation type), an object fixed to safety.case, and a nullable reason string.

PR #2772 complements the endpoint with two new webhook events, quoting the official doc strings:

  • safety.warning_issued — "Sent when a warning is issued for a safety identifier in your organization."
  • safety.deactivation_issued — "Sent when a deactivation is issued for a safety identifier in your organization."

Both events share the same payload.data shape: a single id field whose doc string states the purpose verbatim — "The safety case ID to pass to GET /v1/safety/cases/{id}". That hop from event to retrieval endpoint is written into the official sources, and the event names (warning_issued / deactivation_issued) match the case's two notice.type values.

Consumption uses the existing unwrap (unchanged in this release):

const event = await client.webhooks.unwrap(
  req.body,              // raw payload string
  req.headers,           // request headers (webhook-signature / webhook-timestamp / webhook-id)
  process.env.OPENAI_WEBHOOK_SECRET, // omit to use client.webhookSecret
);
if (
  event.type === 'safety.warning_issued' ||
  event.type === 'safety.deactivation_issued'
) {
  const safetyCase = await client.safety.cases.retrieve(event.data.id);
}

One precise type-level fact: the two new events were added to UnwrapWebhookEvent (the event union unwrap returns — 21 event types counted in the v7.20.0 tag), but the static subscription union on the event_types field of WebhookCreateParams / WebhookUpdateParams still lists the original 18 values after this release — the SDK's type layer does not yet offer the new events as subscription options at endpoint creation (verified against the tag sources on 2026-09-20). Treat client.webhooks.event_types.list() as the practical source of truth (it returns a plain string array with no static enum constraint).

5. SIP Media Security on Incoming-Call Events (#2770)

PR #2770 adds one optional field, sip_media_security, to the data of three incoming-call webhook events:

  • live.call.incoming (LiveCallIncomingWebhookEvent)
  • live.transport.incoming (LiveTransportIncomingWebhookEvent)
  • realtime.call.incoming (RealtimeCallIncomingWebhookEvent)

The type is an open union: 'rtp' | 'srtp' | (string & {}). The official field description, in full: the media protection selected on the SIP leg during SDP negotiation — srtp indicates SRTP and rtp indicates unencrypted RTP; omitted when unknown; the field does not describe SIP signaling security nor confirm that media has flowed; clients should handle unrecognized values as unknown. Two cautions: the live.transport.incoming event itself predates this release (only the field is new), and the field is optional, so consumer code cannot assume it is present.

6. Fix: Preserve Request Options in Legacy GET Calls (#2771)

The release's only fix: "Preserve request options in legacy GET calls." Background: a set of list-style GET methods historically used the first parameter for both query parameters and request options. This PR adds overloads plus a runtime normalizer (normalizeRequestOptionsForQuery): transport options appearing in the first argument — headers, maxRetries, timeout, signal, idempotencyKey — are recognized as request options and are no longer serialized into the URL query string.

The blast radius: 57 non-test source files, covering all 21 admin/organization resources (API keys, audit logs, certificates, groups, invites, the projects family, roles, spend alerts, users, and more) plus batches, the beta/agents family, beta/assistants, beta/threads, chat/completions, containers, conversations, evals, files, fine-tuning, images, responses, skills, vector-stores, videos, and webhooks.

Runtime behavior tightens as well — two patterns now throw TypeError:

  • Query parameters and request options mixed into one object — "Query parameters and request options must be passed as separate arguments."
  • Transport overrides (method, path, body, etc.) passed in the query position — "Pass transport overrides in the explicit request options argument."

The correct shape keeps each in its place:

// First argument: query params only. Second argument: request options.
const page = await client.admin.organization.externalStorage.list(
  { project_id: 'proj_123' },
  { timeout: 30_000, maxRetries: 2 },
);

If existing code passed timeout / signal in the first argument and it silently did nothing, behavior changes after this upgrade (the options start taking effect). If code depended on options being serialized into the query string, audit it.

7. Common Errors and Troubleshooting

  • Sandbox receives a placeholder instead of the secret: by design for environment_variable. Check three things — the outbound target is HTTPS, the port is 443 or 8443, and both the networking config and the environment network policy allow the destination. Local-computation uses (like signing) are officially unsupported.
  • secret_name rejected: check the rules — ASCII letters/digits/underscores, starts with a letter or underscore, no CODEX_ prefix and no reserved proxy/certificate names.
  • safety.warning_issued missing from the endpoint-creation event list: the SDK's static union does not include it yet (see section 4); trust client.webhooks.event_types.list(). The unwrap-side types already support it.
  • list calls throw TypeError after upgrading: split query parameters and request options into separate arguments (see the two error messages in section 6).
  • External storage configuration stays pending: the validate endpoint triggers validation; status is pending / validated / unhealthy, and the SDK offers no further troubleshooting detail beyond that.

8. Next Steps

Key points

  • environment_variable vault credential (#2768): the CredentialAuth union expands from mcp_oauth + static_bearer to three values. It is an HTTP credential for OpenAI-hosted environments only: sandbox code receives a placeholder in the named environment variable, and the egress proxy substitutes the real secret for allowed HTTPS destinations on ports 443 and 8443. The real secret is never returned in the resource, cannot be read by sandbox code, and cannot be used for local computation such as signing a request
  • networking and constraints (#2768): CredentialNetworking is unrestricted (requires environment.network.access set to restricted with explicit allowed_domains) or limited (1-16 distinct hostnames or IPv4 addresses, lowercase, no scheme/path/port/wildcard, IPv6 unsupported). secret_name allows ASCII letters, digits, and underscores starting with a letter or underscore; the CODEX_ prefix and managed proxy/certificate names are reserved. secret_value is write-only, must be nonempty, and cannot contain CR/LF/NUL. Rotation supports the new type
  • External storage management (#2773): client.admin.organization.externalStorage exposes create (POST /organization/external_storage — registers one customer-managed external storage configuration), retrieve, list (cursor pagination), delete, and validate (POST .../{id}/validate). Configurations carry geography, project_id, and a status of pending/validated/unhealthy; providers are AWS (bucket + role_arn) and Azure (account_name/container/resource_group/subscription_id/tenant_id), authenticated with an admin API key
  • Safety cases and new events (#2774 / #2772): client.safety.cases.retrieve maps to GET /v1/safety/cases/{id} and returns a safety.case object (entity_identifier, nullable reason, notice.type of warning or deactivation). The new webhook events safety.warning_issued and safety.deactivation_issued (doc strings: sent when a warning/deactivation is issued for a safety identifier in your organization) carry the case ID to pass to that endpoint in payload.data.id. The alerts and cases sub-resources coexist under client.safety
  • SIP media security field (#2770): the data of live.call.incoming, live.transport.incoming, and realtime.call.incoming each gain an optional sip_media_security — an open union of rtp / srtp / unrecognized strings. Official description: the media protection selected on the SIP leg during SDP negotiation, omitted when unknown; it does not describe SIP signaling security nor confirm that media has flowed, and clients should treat unrecognized values as unknown (the live.transport.incoming event itself predates this release — only the field is new)
  • Legacy GET fix (#2771): list-style GET methods gain overloads plus a runtime normalizer — transport options mixed into the first argument (headers/maxRetries/timeout/signal/idempotencyKey) are preserved as request options instead of being serialized into the URL query. Mixing query params with request-only options now throws TypeError (Query parameters and request options must be passed as separate arguments.). The change spans 57 source files (21 admin/organization resources plus batches, beta/agents, chat/completions, containers, evals, responses, videos, and more)

Frequently asked questions

static_bearer is a static bearer-token credential for MCP servers; environment_variable is an HTTP credential used only in OpenAI-hosted environments. The core difference is secret visibility: the environment variable that sandbox code reads contains a placeholder, not the real secret. Code uses the placeholder unchanged in outgoing requests, and the egress proxy replaces it with the real secret for allowed HTTPS destinations on ports 443 and 8443. The real secret never appears in the credential resource, cannot be read by sandbox code, and cannot be used for local computation — the official doc string explicitly calls out signing a request as a non-option.

Official references

Related articles

Subscribe to GPTMap Weekly

One email every Monday: curated OpenAI updates, deep dives, and best practices. No ads, unsubscribe anytime.

Submitting opens Buttondown in a new tab to confirm your subscription.

GPTMap EditorialPublished 2026-09-20 10 min read
Test environment (EEAT)
Last tested: 2026-09-20
Model used: gpt-5.6