Zoom account connections
Hourmere can connect a personal Zoom identity under Conference accounts, separately from
sign-in, Google/Microsoft calendar connections, and the passive Zoom location label on event
types. The identity-only choice does not create or read meetings. Optional automatic meeting
authorization is a separate explicit choice; see automatic Zoom
for lifecycle, private host selection, safe retry and acceptance limits. The user’s own API
keys can list Zoom connections and select their own meeting-mode connection for automatic
Zoom through REST on event types they actually host, with the same organization checks.
Read-only MCP list_conference_connections also accepts enabled MCP OAuth grants.
OAuth authorization, code exchange and disconnect require a live browser session;
explicit Zoom credential refresh remains session-only. Cookie-authenticated writes require
the app’s own origin. MCP OAuth grants do not authenticate REST settings mutations.
Configure an optional confidential client
Section titled “Configure an optional confidential client”Connections are disabled by default. A deployment administrator must separately register and approve a suitable user-authorized Zoom Marketplace OAuth client. Creating or modifying a real client, accepting terms and granting real-account access require their own authorization; the automated tests use synthetic provider responses only.
Real-account acceptance was performed on 2026-10-19 against a Zoom development-mode app. Its scope was creating, rescheduling and cancelling a real meeting. Production Zoom Marketplace publication is a separate step for the hosted service; self-hosters use their own Zoom apps.
- For identity-only use configure only granular
user:read:user. Enabling separate meeting authorization requires an independently approved compatible client configuration andZOOM_MEETINGS_ENABLED=true; follow automatic Zoom. Never grant meeting access implicitly to the identity action, and never add admin/unrelated scopes. Zoom’s basic authorization query can include the required and optional scopes configured in Marketplace. - Enable the client’s S256 PKCE capability, while using its confidential client ID and secret. Hourmere sends client credentials only through HTTP Basic authentication and the verifier in the token body, as in Zoom’s confidential PKCE example. It does not use a public-client ID or expose a client secret to the browser.
- Register exactly
{PUBLIC_URL}/api/conference/zoom/callback. No dynamic redirect URL or application return URL is accepted. Production requires HTTPS. Hourmere permits HTTP loopback for synthetic/local testing; whether a particular Marketplace client accepts that redirect and confidential PKCE combination must be verified separately with the real client. A passing mock test does not establish Zoom client acceptance. - Set
ZOOM_CLIENT_ID,ZOOM_CLIENT_SECRETandCONFERENCE_ENCRYPTION_KEYtogether in the deployment’s private environment. The encryption key must be a separate canonical base64 encoding of exactly 32 random bytes, independent of the authentication, calendar and webhook keys. Keep it stable and back it up securely with the database. Key loss or replacement requires reconnection..env.examplecontains empty placeholders only. - Before inviting real Zoom users, set
ZOOM_WEBHOOK_SECRET_TOKENto the app’s webhook Secret Token and configure both its Event notification endpoint URL and Deauthorization Notification Endpoint URL as{PUBLIC_URL}/api/conference/zoom/events. Validate and save the publicly reachable HTTPS endpoint; see automatic Zoom for build-flow details and delivery limitations. This endpoint remains enabled independently of OAuth and automatic meeting flags. Zoom documents deauthorization delivery for public apps; private/development apps do not trigger it. - Sign in, open Conference accounts, and explicitly choose Connect Zoom. Do not use a provider email address as evidence of a verified Hourmere sign-in identity.
Hourmere uses the official fixed authorize/token/users-me hosts. The advanced authorization
query includes scope=user:read:user, empty optional_scope, and omits
include_granted_scopes. Requests cannot select a broader scope. Each connection stores its explicitly requested mode. Identity mode conservatively
requires every code exchange and refresh response to report exactly user:read:user;
meeting mode requires its distinct exact set from automatic Zoom. An absent, duplicated, unknown or broader scope fails closed. This is
Hourmere’s conservative policy, not a claim that OAuth or Zoom universally guarantees a scope
field on every response. Check actual client response compatibility before enabling a real
integration; do not work around the policy by requesting broader access.
Provider api_url, personal meeting URLs, PMI, start URLs and arbitrary error bodies are
ignored. Tokens, authorization codes, state and the PKCE verifier are never returned by the
metadata API or logged. Only the official authorization URL carries a new opaque state and
challenge to the browser.
Refresh, reconnect and disconnect
Section titled “Refresh, reconnect and disconnect”- Refresh Zoom account explicitly rotates credentials and updates identity metadata. The
durable
refreshingclaim clears stored credentials before the provider request. Transient transport failures retry at most three times with bounded backoff; exhausted transient failures restore the original encrypted credentials as active for a later attempt. Other token rejections and malformed successful responses also preserve the previous grant for later recovery; an unknown rotation may later resolve toinvalid_grant. That explicit invalid grant or a successful scope/identity mismatch requires reconnection. A crashed or uncommittable rotation remains incomplete until the next worker or explicit refresh acquires a valid operation lease and marks it as requiring reconnection. An incomplete connection still offers Refresh Zoom account. If refresh reports that reconnection is required, choose Reconnect Zoom. An operation busy with another Zoom identity reports a neutral retry message. A confirmed rotation followed only by a transient identity lookup error retains the new token and reports the metadata error. - Reconnect Zoom replaces existing local access and requires the same Zoom identity. Its saved identity/meeting permission mode and stable connection ID remain unchanged. A Zoom identity already owned by another Hourmere account cannot be silently transferred, even when email addresses match.
- Disconnect Zoom removes local credentials and all that user’s pending connection attempts. It retains the identity record for future references and does not revoke the entire Marketplace app grant. Remove the app separately in Zoom to revoke that grant.
- Remove the app in Zoom also clears local Zoom connection credentials and identity metadata when the operator configured the signed event endpoint and Zoom delivers the deauthorization notification. As with local disconnect, automatic event types keep their activation and configuration, but the saved meeting account is unavailable until the host authorizes and selects one; other Zoom users are unchanged. This does not delete existing bookings or provider meetings.
- At most five identities can be connected per Hourmere user at once, including accounts
requiring reconnection or with an incomplete refresh. Disconnecting releases capacity. The
default metadata list excludes disconnected records; after a local disconnect, a later
explicit grant for the same Zoom identity reuses its stable ID. App deauthorization erases
that identity association, so a later grant creates a new connection. Reconnecting a disconnected record also checks capacity. Pending authorization attempts are
limited to five per user, expire after ten minutes, and are single-use and session-bound.
If another token operation is busy, the callback returns to Conference accounts with a
retry message before inspecting or consuming state or exchanging the code. Start
Connect/Reconnect again after that operation finishes. A replay of an already claimed
or completed attempt also reports
busywhile the owner’s lease is occupied; this does not prove that the attempt is reusable. After the operation finishes, that replay reportsinvalid_statewithout another exchange. Only an untouched, unexpired attempt with a still-valid provider code can be retried successfully. - Deleting the Hourmere user removes credentials, pending attempts, Zoom ID, display name and email from the retained record. A new Hourmere user, including one with the same email, cannot inherit the old connection ID. A future explicit authorization creates a new ID.
User operations and deletion use the same owner-first row-lock order. Durable claims prevent callback replay and rotating-token reuse after failed commits. Provider calls have a bounded deadline and response size. The optional signed event endpoint handles URL validation and app deauthorization without calling Zoom or storing event payloads. Automatic personal and individual single-host team events require an explicit meeting-mode connection, a default-off instance flag and the actual host’s session or own API-key selection. They reconcile one pinned meeting through reschedule/cancellation, hold final mail/ICS until ready and can pin one optional ordinary Google copy. Collective/round-robin automatic Zoom is unsupported. An unknown create is never retried with another POST: inspect/remove any orphan in Zoom before replacing the booking. See automatic Zoom for the complete contract and real-client acceptance limits.
Reference API
Section titled “Reference API”All writes require an empty JSON object and a live owner browser session:
| Method | Path | Purpose |
|---|---|---|
| GET | /api/v1/conference-connections |
Own safe metadata and availability; never tokens |
| POST | /api/ |
New minimum-scope authorization attempt |
| POST | /api/ |
Explicit new meeting authorization, if enabled |
| POST | /api/ |
Explicit owner upgrade to meeting authorization |
| POST | /api/ |
Replace access for the same identity |
| POST | /api/ |
Explicit credential rotation and metadata refresh |
| DELETE | /api/ |
Local disconnect; retain stable record |
Callback redirects use a fixed allowlist of outcomes under /app/conferences. Responses use
Cache-Control: no-store and Referrer-Policy: no-referrer.
A refresh conflict returns HTTP 409 with error: "zoom_refresh_in_progress" and the neutral
errorAlias: "zoom_operation_busy". Prefer the alias in new clients and accept the old code
for older servers. The old code is deprecated but remains unchanged for all of /api/v1;
the alias is additive, and removing or renaming the old code requires a new API version.
The conflict covers any token operation owned by the same Hourmere user,
including another connection’s authorization or refresh; wait briefly and try again.
Protocol references, checked 19 October 2026: Zoom OAuth, Zoom PKCE, advanced scope query, granular scopes, webhook verification, app deauthorization.
Refresh and abandoned operations
Section titled “Refresh and abandoned operations”The Conference accounts page offers Refresh Zoom account for both active and
refreshing connections. An owner-scoped operation lease serializes Zoom token changes,
including changes for different Zoom identities belonging to that Hourmere user.
If an operation still owns a live lease, explicit refresh returns HTTP 409:
{ "error": "zoom_refresh_in_progress", "errorAlias": "zoom_operation_busy", "message": "Zoom account operations are busy. Try again shortly."}New clients should prefer the neutral errorAlias and accept zoom_refresh_in_progress
when talking to older servers. The legacy code is deprecated, but its error value remains
for all of /api/v1; the alias is additive, and removing or renaming the old code requires
a new API version (API conventions). The conflict means an owner
operation is busy; it does not identify which connection or operation holds the lease.
Wait briefly before retrying.
A refresh that acquires a valid lease and still finds refreshing can prove that the old
rotation was abandoned. It changes the connection to needs_reauth, clears the old
operation and credentials, and returns zoom_needs_reauth without calling Zoom. The page
then tells the user to choose Reconnect Zoom. This explicit path also recovers
identity-only connections, which have no meeting worker to discover the abandoned refresh.
Callback replay and busy
Section titled “Callback replay and busy”GET /api/conference/zoom/callback returns to /app/conferences with a fixed result code.
For a syntactically valid state, it tries to acquire the owner’s token-operation lease
before inspecting the OAuth attempt. An occupied lease returns busy immediately; that
request neither consumes state nor exchanges a provider code.
The result describes lease contention, not the state of the submitted attempt:
| Submitted attempt | While the owner’s lease is occupied | After the lease is released |
|---|---|---|
| Unclaimed, unexpired, same-session state with a valid provider code | busy, with the attempt unchanged |
Can complete normally |
| Already claimed by an exchange still in progress | busy, without a second exchange |
invalid_state, without a second exchange |
| Already completed or otherwise invalid state with valid syntax | busy, without another exchange |
invalid_state, without another exchange |
In particular, replaying the same callback while its first exchange is in progress returns
busy, rather than invalid_state. The same can happen when a different exchange for that
owner holds the lease. A busy result is not evidence that a state or provider code remains
valid or reusable. State claims are durable, single-use and session-bound; they clear the
stored PKCE verifier before provider I/O. Completion, failure or a failed final commit does
not make a claimed attempt reusable. Expiry is checked when claiming; an exchange claimed
before expiry may finish successfully after expiry.
The browser offers a neutral retry message and the user can start Connect Zoom or
Reconnect Zoom again after the busy operation finishes. The original callback can only
succeed on retry if its state was never claimed, its session and attempt remain valid, and
the provider code is still valid. Do not automatically replay a claimed callback or infer
successful authorization from busy.