paperclipai/paperclip · error · CodexQuotaAuthError

chatgpt wham api returned ${resp.status}

Error message

chatgpt wham api returned ${resp.status}

What it means

Thrown by fetchCodexQuota (quota.ts) when the ChatGPT WHAM usage endpoint (https://chatgpt.com/backend-api/wham/usage) returns a non-2xx status. Before throwing the generic Error it classifies the response text; if the body looks like a Codex auth-refresh failure it throws a CodexQuotaAuthError instead, so callers can distinguish auth problems from transient API errors.

Source

Thrown at packages/adapters/codex-local/src/server/quota.ts:295

}

export async function fetchCodexQuota(
  token: string,
  accountId: string | null,
): Promise<QuotaWindow[]> {
  const headers: Record<string, string> = {
    Authorization: `Bearer ${token}`,
  };
  if (accountId) headers["ChatGPT-Account-Id"] = accountId;

  const resp = await fetchWithTimeout("https://chatgpt.com/backend-api/wham/usage", { headers });
  if (!resp.ok) {
    const message = `chatgpt wham api returned ${resp.status}`;
    const responseText = await readResponseTextPrefix(resp);
    const authFailure = classifyCodexAuthRefreshFailure({
      errorMessage: [message, responseText].filter(Boolean).join("\n"),
    });
    if (authFailure) throw new CodexQuotaAuthError(message, authFailure);
    throw new Error(message);
  }
  const body = (await resp.json()) as WhamUsageResponse;
  const windows: QuotaWindow[] = [];

  const rateLimit = body.rate_limit;
  if (rateLimit?.primary_window != null) {
    const w = rateLimit.primary_window;
    windows.push({
      label: "5h limit",
      usedPercent: normalizeCodexUsedPercent(w.used_percent),
      resetsAt:
        typeof w.reset_at === "number"
          ? unixSecondsToIso(w.reset_at)
          : (w.reset_at ?? null),
      valueLabel: null,
      detail: null,
    });

View on GitHub (pinned to 67001ec6eb)

Solutions

  1. If caught as CodexQuotaAuthError, re-authenticate (re-run codex login) and retry.
  2. For 429, back off quota polling and retry after the rate-limit window.
  3. For 5xx, retry with exponential backoff; the WHAM API is eventually consistent.
  4. Verify the token and accountId passed to fetchCodexQuota come from a current auth.json (decodeJwtPayload for account_id).
Defensive patterns

Strategy: try-catch

Type guard

function isCodexQuotaAuthError(e: unknown): e is CodexQuotaAuthError {
  return e instanceof Error && /chatgpt wham api returned/.test(e.message) && Object.prototype.hasOwnProperty.call(e, "authFailure");
}

Try / catch

try {
  const windows = await fetchCodexQuota(token, accountId);
} catch (e) {
  if (e instanceof CodexQuotaAuthError) {
    // token expired/revoked: re-authenticate then retry once
    await refreshCodexAuth();
    return fetchCodexQuota(newToken, accountId);
  }
  if (e instanceof Error && /chatgpt wham api returned 429|5\d\d/.test(e.message)) {
    // transient: back off and retry
    await sleep(backoffMs); backoffMs *= 2;
    continue;
  }
  throw e;
}

Prevention

When it happens

Trigger: fetchWithTimeout resolves but resp.ok is false. Common statuses: 401/403 (token expired or revoked — may surface as CodexQuotaAuthError), 429 (rate limited), 5xx (ChatGPT backend outage), or 404 (account/endpoint changed).

Common situations: Codex subscription access token expired and refresh failed; ChatGPT backend incident; the account_id header is wrong/missing for a multi-account token; rate-limited by frequent quota polling.

Related errors


AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12). Data as JSON: /api/errors/56f2b013f739da59. Report an issue: GitHub.