The key that funds platform calls
OPENAI_API_KEY is the server-side credential Receipt uses when a request is not paid for by an organization’s own provider key. Four paths read it: a chat answer, the thread title generated on a first turn, attachment embedding, and a background Factory run authorized against platform credit. It is a process-level secret: only server modules read it, and it carries neither prefix the web build exposes to the client (VITE_, ENABLE_), so it is never inlined into the browser bundle. A platform-funded background run receives it inside its sandbox as a credentials file at ~/.codex/auth.json, not as an environment variable. It is not the same thing as an organization key, which stays encrypted in the database. See Bring your own key for the other half.
Without it, a platform-funded chat turn fails before any provider is contacted:
There is no platform-funded Anthropic route. Anthropic models execute only against an organization’s own Anthropic key, and background runs are OpenAI-only. See Models and providers.
The BYOK wrapping key
BYOK_ENCRYPTION_KEY_B64 is the AES-256-GCM wrapping key for organization provider API keys. It must decode to exactly 32 bytes. The cipher uses a 12-byte IV and key version 1, and the key-resolution log line carries only a fingerprint of the provider key it resolved — the first 12 hex characters of its SHA-256 — never the key itself.
Missing required environment variable BYOK_ENCRYPTION_KEY_B64.BYOK_ENCRYPTION_KEY_B64 must be valid base64.BYOK_ENCRYPTION_KEY_B64 must decode to exactly 32 bytes.Unsupported BYOK key version: {n}when decrypting a row written under a different key version.
Strict mode
CHAT_REQUIRE_BYOK is compared literally against the string true; anything else, including unset, leaves it off. The cloud template ships CHAT_REQUIRE_BYOK=false with the comment “Optional strict mode. Set true only when every model request must use a workspace-managed provider key instead of the platform-credit gateway.” Read that comment as an organization or workspace provider key: the resolver reads the request’s workspace scope when the turn carries a workspace and the organization scope otherwise. See Bring your own key for the two scopes, and for the organization-scope summary that can skip a provider even in strict mode.
Normally, when the organization’s stored provider-key snapshot says no key exists, credential resolution stops there and the turn runs on platform credit. Strict mode skips that short-circuit and turns a missing key into a 403 carrying “This provider requires an organization API key, but no key is configured.”
That snapshot is only trusted once it has been written at least once. An organization whose policy row still carries the schema default is never short-circuited, and falls through to the per-scope key lookup regardless of this setting.
This is the only “require a key” control that runs. The Require organization provider key toggle on the compliance screen is not enforced server-side; its only reader is a client-side filter over a model list that the composer does not currently mount. See Model policy.
Throttling
The limit is per signed-in user, not per organization and not per key. There is no token budget anywhere; dollar-denominated budgets are covered in Usage and spend.
Each must be a positive integer or resolution throws
Expected {NAME} to be a positive integer. Every request is treated as a paid-tier request in this release, so only the two PAID_ values ever apply.
Model defaults
Each variable below is an override; unset, the identifier in the second column is used. The first two name models for chat-side calls. The rest name models for the Receipt runtime and for Factory, the engine that runs a background objective as a set of sandboxed tasks — see Factory engine.
The two chat-side variables take a catalog model id — the defaults carry the
openai/ prefix — while the runtime and Factory variables take a bare provider model name. Name a model the catalog does not know and resolution is denied with the machine reason unknown_model; name one the catalog knows but the gateway cannot route, and the call fails with a 502 instead.
Both templates’ comment on
CHAT_TITLE_GENERATION_MODEL says that title generation “follows the thread model” when the variable is unset. It does not: the resolver ignores the thread model it is passed and uses the default above.The Replay recap is the one model call that never falls back to platform credit. It resolves an organization key for the provider implied by the model id, gives up after 20 seconds and does not retry; with no key the dialog reads “Connect a {OpenAI|Anthropic} key in organization settings to generate AI summaries.”OPENAI_MAX_RETRIES (default 3) and OPENAI_RETRY_BASE_MS (default 500) drive exponential backoff capped at 8 seconds plus jitter, and only rate-limit failures are retried — a 401 or an invalid key is permanent. RECEIPT_OPENAI_KEY_DEBUG_MODE (default masked) controls the adapter’s openai.key.debug line: off suppresses it, masked prints a fingerprint plus a truncated preview, and full also prints the whole key. Leave it at masked.
Spend and cost visibility
WORKSPACE_USAGE_TARGET_MARGIN_PERCENT is the margin applied when a paid plan’s seat budget is built, and it is required on a cloud instance — the first paid-plan policy resolution throws Missing WORKSPACE_USAGE_TARGET_MARGIN_PERCENT when it is absent. Per-plan overrides take precedence when set: WORKSPACE_USAGE_PLUS_TARGET_MARGIN_PERCENT, and the same shape for PRO, SCALE and ENTERPRISE. A value must parse as a number between 0 and 100, or you get Invalid numeric value for {NAME} or Expected {NAME} to be between 0 and 100.
ALLOW_USER_COST_DISPLAY defaults to false and decides whether a turn’s cost is exposed to the person who sent it. A turn paid for with an organization’s own key exposes its cost regardless of this flag.
What a refused call returns
Every chat API failure returns the same envelope, at the status in the table below:error.code is a stable transport code, error.i18nKey selects the sentence the client renders, and details.tag is the server-side error class. retryable is set from the failure class alone, not from anything about your configuration.
Three details worth knowing before you build anything against this envelope:
- No
Retry-Afterheader is set. The retry hint travels aserror.i18nParams.retryAfterSeconds. - Code and i18n key can differ. Platform-credit exhaustion returns the code
error_chat_quota_exceededbut the keyerror_chat_platform_credit_exhausted; a denial returns the codeerror_chat_model_not_allowedwith one oferror_chat_model_not_allowed,error_chat_provider_key_missing,error_chat_provider_model_key_incompatibleorerror_chat_model_requires_paid_plan. Branch on the key, not the code. - In standard context mode the key changes, to
error_chat_context_window_exceeded_max_available: “This conversation has reached the standard context limit ({maxTokens}tokens). Switch to Max to continue with the larger window.”
The platform-credit sentence tells users to go to “Billing & Credits”. No page has that name — the settings rail item and page title are both Billing.
Machine reasons behind a 403
A denial carries a machine reason internally. The reason picks the sentence the user reads and decides whether the failure is reported to error tracking, but it stays inside the server: it is not part of the envelope, and the structured log line written for the request records the error tag, code and i18n key rather than the reason string. Useerror.i18nKey to tell denials apart from the outside.
Denials are additive: when a provider, a model and a compliance flag all rule a request out, the reason joins them with commas, as in
policy_denied:provider,model,compliance.
free_tier_model_denied cannot fire in this release — plan-based model access always resolves to allowed. The classifier also still recognises a model_not_supported_for_provider_key reason and maps it to “This model cannot be used with your organization provider API key. Choose another model from that provider or remove the provider key.”, but no code path emits that reason today; the equivalent case is reported as policy_denied:provider_not_supported_by_byok.Self-hosted differences
A self-hosted build setsVITE_APP_INSTANCE_MODE=self_hosted, which changes what the gateway charges for and what it requires.
Bringing your own key is optional on a self-hosted instance in the same sense as on cloud: without one, requests fall through to whatever
OPENAI_API_KEY you configured on the server. What changes is that nothing meters or reserves against a credit balance first.
For the variables outside the model path — identity, database, storage, email and the runtime’s job settings — see Configuration, secrets, and keys.
Next step: return to the LLM Gateway overview for how the pieces on this page fit together.