docs/zoommate.md
The ZoomMate provider tracks credit usage against a budget cap from zoommate.zoom.us. ZoomMate is a credits-based quota — there is no session/weekly rate window, just a single primary "Credits" window.
CodexBar's ZoomMate branding uses Zoom's official core palette: Bloom (#0B5CFF) as the primary
color, with Dawn (#B4D0F8) and Midnight (#00053D) as supporting colors. Provenance:
Zoom Brand Center, Visual identity — Color,
retrieved 2026-07-18 (page last modified 2025-08-28).
CodexBar imports your ZoomMate/Zoom session cookies from Chrome's cookie jar (covering
zoommate.zoom.us, ai.zoom.us, and the parent zoom.us domain, where Zoom's SSO session cookies
actually live) and exchanges them for a short-lived bearer token via the same
cookie-to-token bootstrap endpoint ZoomMate's own web app uses internally
(GET https://ai.zoom.us/ai-computer/api/v1/login/?continue=...) — no manual paste required. This
automatic path intentionally tries Chrome only to avoid surprise prompts from other browser stores;
other browsers must use manual capture.
Because the bearer token is minted from your (much longer-lived) session cookies, this keeps working
as long as you stay signed in to ZoomMate in Chrome. A minted token is reused from an in-memory cache
across refreshes until it nears expiry, then re-minted from the same cookies — no re-paste required.
If the Chrome session cookie is missing, CodexBar reports noSession; if Zoom rejects an existing
session, CodexBar reports invalidCredentials.
Chrome's cookie decryption key lives in the macOS Keychain, so CodexBar only reads Chrome's cookie
store during user-initiated refreshes (a menu or Settings refresh, or codexbar cookie). Once a
fresh import validates — the cookie-to-token mint succeeds — the validated host-scoped cookie headers are saved to
CodexBar's shared Keychain cookie cache (com.steipete.codexbar.cache, account cookie.zoommate,
same as other cookie providers) and reused before re-importing from Chrome. Background refreshes and
the bundled codexbar CLI run entirely from those cached headers, so they never touch Chrome's cookie
store or trigger a Keychain prompt. If Zoom rejects the cached session, CodexBar drops it and retries
one fresh Chrome import (user-initiated contexts only; background refreshes report noSession until
the next user refresh).
Set Cookie source to Manual in the ZoomMate provider settings, then paste a full curl
command captured from DevTools:
credits/status request
(GET https://ai.zoom.us/ai-computer/api/v1/credits/status).curl command into the ZoomMate capture field in CodexBar settings.Unlike some other manual-paste providers, the forwarded header allowlist for ZoomMate includes
Authorization — that's the required credential (see "Common errors" below). Cookie and
selected browser headers may also be forwarded, but Origin and Referer are always replaced with
the fixed https://zoommate.zoom.us value. Captures are accepted only for the HTTPS
/ai-computer/api/v1/credits/status endpoint on ai.zoom.us or zoommate.zoom.us — DevTools may
show the request on either host, since both currently serve the same first-party API. A captured
Cookie header is bound to that request's host: CodexBar tries the captured host first and never
forwards its cookies if a non-auth failure falls back to the sibling host.
The ZoomMate bearer token itself is short-lived — typically about an hour. Session cookies live far longer, so:
curl command when the pasted token expires
(surfaced as invalidCredentials — see below). This is expected behavior, not a bug — switching
to Automatic mode avoids it entirely.Automatic mode uses the same first-party ZoomMate web-client flow a signed-in browser uses:
CodexBar imports Zoom session cookies from the local Chrome cookie jar, partitions them into the
headers a browser would send to each fixed API host, exchanges one of those host-scoped headers for
a short-lived bearer token, and then uses that bearer token for the credits/status and
credits/history requests.
The minted bearer token is never persisted — it is held only in a process-lifetime in-memory cache.
The validated host-scoped cookie headers (and only those headers — never the bearer) are persisted to CodexBar's
existing shared Keychain cookie cache after a successful mint, the same cache and lifecycle other
cookie providers (Claude web, Perplexity, OpenCode, …) already use, so background refreshes and the
bundled CLI can reuse the session without rereading Chrome. ZoomMate logs intentionally omit
cookies, bearer tokens, nak values, and raw response bodies. The
cookie read spans the parent zoom.us domain because Zoom's SSO session cookies are parent-scoped,
but the imported set is then narrowed to only cookies a browser would actually attach to
ai.zoom.us / zoommate.zoom.us (RFC 6265 domain-matching), dropping cookies host-scoped to
unrelated *.zoom.us siblings that these endpoints never receive.
All authentication and credit requests use fixed HTTPS URLs on the two first-party API hosts:
automatic mode tries ai.zoom.us first, while manual mode starts with the host in the accepted cURL
capture; either mode falls back to the other host on non-auth failures. Each request receives only
the cookies scoped to its destination host. The hosts currently serve the same /ai-computer/ API
interchangeably and either may retire in the future, so the provider works with both; auth rejections
(401/403) never trigger the fallback since the host answered and the session is the problem.
zoommate.zoom.us additionally provides the product UI and
the fixed web-client continue, Origin, and Referer values. The parent zoom.us scope is used
only to discover parent-scoped SSO cookies; it does not authorize requests to arbitrary Zoom
subdomains. ZoomMate does not currently expose a
documented public API, so this provider depends on the product's first-party web-client endpoints.
This cookie-to-bearer shape follows an existing CodexBar precedent in the Factory provider's WorkOS
cookie exchange. The minted bearer is cached in memory (keyed by a SHA-256 of the cookie session)
and reused until it nears its JWT exp; a token whose expiry can't be read is never cached, and a
401/403 from a downstream request evicts the cached token so the next refresh mints fresh.
CodexBar sends up to a few GET requests per refresh:
GET https://ai.zoom.us/ai-computer/api/v1/credits/status
GET https://ai.zoom.us/ai-computer/api/v1/credits/history?app_id=demo_app&limit=50&page=<n>&sort_by=time&sort_order=desc&start_time=<ISO8601>&end_time=<ISO8601>
(Automatic requests retry once on zoommate.zoom.us with the same path if ai.zoom.us fails with a
non-auth error. Manual requests start on the captured host and retry the other host without the
captured host's cookies — see "Auth & privacy" above.)
The credits/status response's data.credit_status object is decoded into a
ZoomMateCreditStatus struct. The credits/history request is paginated (looping on page until
page * limit + records.length reaches the response's flat data.total, or a page's records are
entirely older than the requested start_time) to cover the last 30 days; real accounts have
modest history (tens of records total), so this is normally 1–2 requests. app_id is sent as a
fixed placeholder matching ZoomMate's own web UI — it does not scope the result set to a particular
integration. A failed credits/history fetch is non-fatal: it never blocks the primary
credits/status snapshot, it just means the history dashboard (below) is omitted for that refresh.
| Source field | CodexBar mapping |
|---|---|
used_credit / budget_cap | Primary window usedPercent (clamped 0–100) |
cycle_end_date | Primary window reset time (epoch ms) |
is_unlimited / budget_cap <= 0 | usedPercent forced to 0, reset countdown omitted |
There is no secondary window; resetDescription is always "Credits".
The same login bootstrap response used to mint the bearer token (GET .../login/?continue=...) also carries a data.user_profile object with the signed-in
user's account details. CodexBar reads user_profile.email from it and surfaces it as the
provider's accountEmail — the same identity field Codex/Claude populate — so the menu card's
account row shows who is signed in, matching those providers' presentation. loginMethod is
set to "Cookie" whenever an email was resolved this way. This is purely additive enrichment:
a missing/absent user_profile or email never fails the token mint, it just leaves identity
unset. Manual (.web cURL-capture) mode has no equivalent bootstrap call, so accountEmail
stays nil there.
When ZoomMate is enabled and history data is available, the menu's Credits card shows a Today/30d credits dashboard and pacing verdict inline, directly under the credits progress bar — on both ZoomMate's own tab and the Overview aggregate view, matching Claude's/Codex's inline dashboards. It includes:
InlineUsageDashboardContent component Claude/Codex render
their inline dashboards with: Today (emphasized — the current calendar day's summed cost
from credits/history, or 0 if nothing posted yet today) and 30d credits (the sum across the
full 30-day window). This mirrors Codex's "Today" / "30d cost" tile pair and Claude's "Today" /
"30d spend" tile, swapping the "$"/cost unit for "credits" since ZoomMate has no dollar-cost
concept.credits/history's per-event ledger summed by day), capped at the most recent 30 days, rendered
in ZoomMate's brand color (#0B5CFF) — matching Codex/Claude's own bar density and per-provider
coloring exactly.credits/status's
budget_cap, cumulative used_credit, and the current billing cycle's
cycle_start_date/cycle_end_date — comparing actual usage against the expected
linear-elapsed-fraction of the cycle. This reuses UsagePace's existing stage thresholds (on
track / slightly ahead / ahead / far ahead / slightly behind / behind / far behind) rather than
a ZoomMate-specific scale, so the wording matches other CodexBar pacing indicators.is_deleted history records are excluded from both the KPI tiles and the mini-bars; still-running
sessions (is_running: true) are included since their cost reflects consumption so far. The
30-day window is enforced independently at both fetch time (the request's start_time) and
display time (dailyBreakdown() filters to the trailing 30 calendar days regardless of what the
fetch returned), so the chart's calendar span is guaranteed either way. The pacing line only needs
the always-fetched credits/status snapshot, so it can appear even in refreshes where
credits/history fails or returns nothing. The inline section is gated on having either a
non-empty daily breakdown or a computable pacing verdict — an empty/failed history fetch silently
omits the section instead of showing an empty dashboard.
ZoomMate's descriptor points at Zoom's public status page, zoomstatus.com, which is an Atlassian Statuspage.io site — the same platform Claude's status page already uses. This means the overall-status row and its "Updated …" subtitle work through CodexBar's existing shared Statuspage.io fetch/parse path with no ZoomMate-specific fetcher code.
Zoom's status page lists ~300+ components, dominated by heavy per-region duplication for services unrelated to ZoomMate (Zoom Phone, Contact Center, and CX each repeated across many regions). Showing all of them in ZoomMate's status drill-down would be noise, so ZoomMate's component submenu is filtered to a named allowlist:
The allowlist matches component/group names exactly (case-sensitive) against whatever the live API returns, and is tolerant of any subset being renamed or removed on Zoom's side: a missing name is silently omitted, never an error, and if none of the allowlisted names are present the submenu falls back to the same "components not loaded" empty state every other provider already has (no ZoomMate-specific empty-state UI). "Zoom Meetings" and "Zoom Workflows" are themselves groups in Zoom's data (each with their own child components); an allowlisted group is shown with its full existing child list when expanded, same as it would be for any other provider.
This allowlist lives in ZoomMate's provider descriptor metadata. Every other provider has no descriptor allowlist and keeps showing every component their feed returns, unchanged.
codexbar usage --provider zoommate
The CLI reuses the host-scoped cookie headers cached by a previous validated refresh; it does not read Chrome's
cookie store itself. If no cached session exists yet (noSession), refresh once from the app or
seed the cache from the terminal with codexbar cookie --provider zoommate (add
--allow-keychain-prompt to acknowledge that Chrome cookie decryption may prompt).
ZoomMate provides no token-cost data and is not yet supported in the CodexBar widget (this includes the credits history dashboard and status page above — both are app-menu-only).
| Error | Cause | Fix |
|---|---|---|
noCapture | Manual mode is selected but the capture is empty, off-domain, or lacks a parseable Authorization header | Paste a fresh cURL capture of the HTTPS credits/status request from ai.zoom.us or zoommate.zoom.us |
noSession | Automatic mode found no cached session and no ZoomMate/Zoom session cookies it may read (background refreshes and the CLI never read Chrome directly) | Sign in to ZoomMate in Chrome and refresh once from the app (or codexbar cookie --provider zoommate), or switch to Manual and paste a capture |
invalidCredentials | HTTP 401/403 — the token expired (~hourly) or was revoked | Re-sign-in (auto) or re-paste a fresh capture (manual) |
apiError | Any other non-200 HTTP status | Check ZoomMate's status; retry later |
parseFailed | HTTP 200 body did not contain the expected credit_status shape | Open a CodexBar issue with a redacted response sample |
Sources/CodexBarCore/Providers/ZoomMate/ZoomMateProviderDescriptor.swift — provider metadata (including statusPageURL and the status-component allowlist) and the unified fetch strategy (calls both credits/status and credits/history)Sources/CodexBarCore/Providers/ZoomMate/ZoomMateUsageFetcher.swift — credits/status request, cURL parsing, and cookie-to-token mintingSources/CodexBarCore/Providers/ZoomMate/ZoomMateCreditsHistoryFetcher.swift — credits/history request, paginated with a date-boundary stop, and the ZoomMateCreditsHistorySnapshot modelSources/CodexBarCore/Providers/ZoomMate/ZoomMateModels.swift — response decoding, error taxonomy, window mapping, daily-bucket aggregation (dailyBreakdown()), today's-total lookup (todayCreditsUsed(now:calendar:)), and pacing verdict computationSources/CodexBarCore/Providers/ZoomMate/ZoomMateCookieImporter.swift — Chrome cookie-jar import (macOS only)Sources/CodexBar/InlineUsageDashboardContent.swift — shared Today/30d KPI-tile + mini-bar view also used by Claude/Codex/OpenRouter/etc.; ZoomMate renders through this same componentSources/CodexBar/MenuCardView.swift — renders the generic inline-dashboard slot for credits-only stacked cardsSources/CodexBar/StatusItemController+Menu.swift — statusComponentsSubmenuProviders and descriptor-backed filterStatusComponentsSources/CodexBar/Providers/ZoomMate/ZoomMateProviderImplementation.swift — settings pickers and bindingsSources/CodexBar/Providers/ZoomMate/ZoomMateSettingsStore.swift — cookie source and capture persistence