API Refresh
May 31, 2026 ยท View on GitHub
This document is the single source of truth for outbound ChatGPT API refresh behavior in codex-auth.
All API refresh requests are issued through curl.
codex-auth resolves curl from PATH.
codex-auth does not translate platform proxy settings. The curl child process inherits the parent process environment, and curl applies its own proxy environment variable handling.
Endpoints
Usage Refresh
- method:
GET - URL:
https://chatgpt.com/backend-api/wham/usage - headers:
Authorization: Bearer <tokens.access_token>ChatGPT-Account-Id: <chatgpt_account_id>User-Agent: codex-auth/<version>
Account Metadata Refresh
- method:
GET - URL:
https://chatgpt.com/backend-api/accounts - headers:
Authorization: Bearer <tokens.access_token>ChatGPT-Account-Id: <chatgpt_account_id>User-Agent: codex-auth/<version>
The account metadata response is parsed from items[].id and items[].name. name: null and name: "" are both normalized to account_name = null. An empty items array, or an items array with no usable id, is treated as unusable and leaves stored account_name values unchanged.
Usage Refresh Rules
- foreground refresh uses the usage API by default.
--skip-apireads only the newest local~/.codex/sessions/**/rollout-*.jsonl.- by default,
listand interactiveswitchrefresh all stored accounts before rendering, using stored auth snapshots underaccounts/ - when one of those per-account foreground usage requests returns a non-
200HTTP status, the correspondinglist/switchrow shows that response status in both usage columns until a later successful refresh replaces it - when a stored account snapshot cannot make a ChatGPT usage request because it is missing the required ChatGPT auth fields, the corresponding
list/switchrow showsMissingAuthin both usage columns until a later successful refresh replaces it - with
--skip-api, foreground refresh still uses only the active local rollout data because local session files do not identify the other stored accounts listand interactiveswitchuse the API-backed path by default;--apiis accepted as an explicit equivalentlist --skip-apiand interactiveswitch --skip-apidisable the foreground usage API path for that commandswitch --livestill excludes errored rows from candidate selection, and it also skips candidates whose current displayed 5h or weekly value is already0%- single-shot
switch --skip-apiskips the pre-render refresh round entirely and shows the stored registry picker directly switch <query>always resolves selectors locally from stored data and does not accept--live,--api, or--skip-api- interactive
remove, includingremove --live, always stays local-only and never makes foreground usage API requests remove <query>andremove --allalways resolve selectors from stored local data and do not accept--live- single-shot
switchdoes not perform another foreground usage refresh after the new account is activated - in
switch --live, a successful selection patches the current picker state in memory instead of rebuilding it from disk; the active account andSwitched to ...message both come from the persisted registry state after the local switch succeeds, while the current display keeps its existing usage/account overlays, including any overlay already shown on the newly active row, until the next scheduled live refresh reapplies fresh data asynchronously - in
remove --live, a successful delete also patches the current picker state in memory; removed rows disappear immediately, surviving overlays stay in place until the next scheduled refresh, and the surviving active account plus theRemoved ...summary come from the persisted registry state after removal succeeds
Account Name Refresh Rules
- Account-name refresh uses the account API by default.
- A usable ChatGPT auth context with both
access_tokenandchatgpt_account_idis required. If either value is missing, refresh is skipped before any request is sent. chatgpt_account_idis the stored ChatGPT account context. It normally comes fromtokens.account_idor JWTchatgpt_account_id; for phone-login auth files that omit both legacy fields, it can be anorg-...organization id selected from JWTorganizations[].- Organization fallback prefers
is_default = true; if no default organization is present, it uses the first non-empty organization id. loginrefreshes immediately after the new active auth is ready.- Single-file
importrefreshes immediately for the imported auth context. listand interactiveswitchrefresh account names by default;--apiis accepted as an explicit equivalent.list --skip-apiand interactiveswitch --skip-apiskip account-name refresh and use stored metadata only.switch <query>always stays local-only and does not accept--live,--api, or--skip-api.remove <query>andremove --allalways stay local-only and do not accept--live.listand interactiveswitchload the request auth context from the current activeauth.jsonwhen they do refresh.- stored snapshots without a usable
access_tokenorchatgpt_account_idare skipped.
At most one account metadata request is attempted per grouped user scope in a given refresh pass.
Request failures and unparseable responses are non-fatal and leave stored account_name values unchanged.
Refresh Scope
Grouped account-name refresh always operates on one chatgpt_user_id scope at a time.
loginand single-fileimportstart from the just-parsed auth infolistand interactiveswitchstart from the current active auth info when foreground refresh is enabled
That scope includes:
- all records with the same
chatgpt_user_id
chatgpt_user_id is the user identity for this flow. A single user may have multiple workspace chatgpt_account_id values, and those values can be legacy account ids or organization fallback ids.
This means a free, plus, or pro record can still trigger a grouped Team-name refresh when it belongs to the same chatgpt_user_id as Team records.
Account metadata refresh is attempted only when:
- the scope contains more than one record
- the scope contains at least one Team record
- at least one Team record in that scope still has
account_name = null
Apply Rules
After a successful account metadata response:
- returned entries are matched by
chatgpt_account_id - matched records overwrite the stored
account_name, even when a Team record already had an older value - in-scope Team records, or in-scope records that already had an
account_name, are cleared back tonullwhen they are not returned by the response - records outside the scope are left unchanged
Examples
Example 1:
- active record:
user@example.com / Team #1 / account_name = null - same grouped scope:
user@example.com / Team #2 / account_name = null
Running codex-auth list should issue an account metadata request. If the API returns:
team-1 -> "Workspace Alpha"team-2 -> "Workspace Beta"
Then both grouped Team records are updated.
Example 2:
- active record:
user@example.com / Pro / account_name = null - same grouped scope:
user@example.com / Team #1 / account_name = null - same grouped scope:
user@example.com / Team #2 / account_name = "Old Workspace"
Running codex-auth list should still issue an account metadata request, because the grouped scope still has missing Team names. If the API returns:
team-1 -> "Prod Workspace"team-2 -> "Sandbox Workspace"
Then:
Team #1is filled withProd WorkspaceTeam #2is overwritten fromOld WorkspacetoSandbox Workspace