Authentication
The Bubbles API accepts two kinds of credential:
- Personal access tokens (PATs) — a long-lived bearer token you create for your own scripts and server-to-server integrations.
- OAuth 2.1 — the authorization-code + PKCE flow for third-party apps acting on behalf of a Bubbles user, including dynamic client registration.
Both present a bearer token on the API:
Authorization: Bearer <token>Personal access tokens
A PAT is a long-lived bearer token you create for your own scripts. Creating one requires a workspace
with paid-feature access (otherwise you’ll see 403 pro_plan_required), and you may hold at most
10 active tokens (an 11th returns 409 invalid_request).
Create a key (recommended: in the app)
The simplest way is from the Bubbles app — no session handling required:
- In Bubbles, open Settings → API keys.
- Click Create key, name it, and copy the token (
bbls_pat_…). It is shown exactly once — store it now; it is never returned again.
Then use it as a bearer token on any data endpoint — see the Quickstart for your first call, or the API Reference for the full surface.
Managing keys programmatically
The key-management endpoints (POST, GET, and DELETE /v1/api-keys) are internal to the Bubbles
app: they authenticate with your signed-in Bubbles session token, which the app sends as
Authorization: Bearer <session token> — this is the app’s own session credential, not a browser
cookie and not a personal access token (a PAT cannot manage keys — it is rejected with 401).
Because that session credential isn’t something you hold outside the app, create and revoke keys from
Settings → API keys (above) rather than by hand.
Revoking a key takes effect immediately: the revoked token then returns the distinct 401 token_revoked envelope on use (versus 401 unauthorized for an unknown token). Listing shows token
previews only — never the secret, which is shown exactly once at creation.
If the account that owns a credential is deleted or disabled, every credential tied to it
(PATs, OAuth tokens, and app sessions) is rejected fail-closed with the distinct 401 account_deleted envelope — across all endpoint categories — even if the token itself has not
expired. This check reads the account’s live state on every request, so a disabled account loses
programmatic access immediately regardless of any cleanup job. Deleting an account also revokes its
credentials at the source, so no live access remains.
OAuth 2.1
Third-party apps use the authorization-code grant with PKCE. The authorization server advertises its endpoints at two discovery documents, so a compliant client needs no hardcoded URLs:
curl "https://api-tom.usebubbles.com/.well-known/oauth-authorization-server"curl "https://api-tom.usebubbles.com/.well-known/oauth-protected-resource"The authorization-server document returns the authorization_endpoint, token_endpoint,
registration_endpoint, revocation_endpoint, jwks_uri, the supported grant types
(authorization_code, refresh_token), code_challenge_methods_supported: ["S256"], and
scopes_supported.
1. Register a client (Dynamic Client Registration)
Register once to obtain a client_id. Public clients (mobile/native/SPA, and MCP clients) register with
token_endpoint_auth_method: "none" and rely on PKCE; confidential clients request
token_endpoint_auth_method: "client_secret_basic" and receive a client_secret.
POST /oauth/register — dynamic client registration
curl -X POST "https://api-tom.usebubbles.com/oauth/register" \ -H "Content-Type: application/json" \ -d '{"client_name":"My integration","redirect_uris":["https://example.com/callback"],"grant_types":["authorization_code","refresh_token"],"response_types":["code"],"token_endpoint_auth_method":"none"}'{ "client_id": "fc69b0dd-3ccc-4ba5-910c-1c4c3323708f", "client_id_issued_at": 1783102178, "client_name": "My integration", "redirect_uris": ["https://example.com/callback"], "token_endpoint_auth_method": "none", "grant_types": ["authorization_code", "refresh_token"], "scope": "bubbles:read"}2. Build the PKCE verifier and challenge
Generate a high-entropy code_verifier, then derive the code_challenge as the base64url-encoded
SHA-256 of the verifier (method S256 — the only method supported):
CODE_VERIFIER=$(openssl rand -base64 60 | tr -d '\n=+/' | cut -c1-64)CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" \ | openssl dgst -binary -sha256 \ | openssl base64 | tr '+/' '-_' | tr -d '=')3. Send the user to the authorization endpoint
Redirect the user’s browser to /oauth/authorize with your client_id, the redirect URI, a random
state, the requested scope, and the PKCE challenge. The user signs in — or signs up for a new
Bubbles account if they don’t have one yet — and then sees a consent screen naming your app and
the scopes; on approval they are redirected back to your redirect_uri with a one-time code.
GET https://api-tom.usebubbles.com/oauth/authorize ?response_type=code &client_id=$CLIENT_ID &redirect_uri=https://example.com/callback &scope=bubbles:read &state=$STATE &code_challenge=$CODE_CHALLENGE &code_challenge_method=S2564. Exchange the code for tokens
Your callback exchanges the code plus the original code_verifier at the token endpoint:
curl -X POST "https://api-tom.usebubbles.com/oauth/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d grant_type=authorization_code \ -d code=$AUTH_CODE \ -d redirect_uri=https://example.com/callback \ -d client_id=$CLIENT_ID \ -d code_verifier=$CODE_VERIFIER{ "access_token": "eyJhbGciOi…", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "eyJhbGciOi…", "scope": "bubbles:read"}The access_token is a signed JWT you can verify against the keys at /oauth/jwks. Present it as
Authorization: Bearer <access_token> on the API.
5. Refresh — with rotation
Access tokens are short-lived. Exchange the refresh_token for a new access token. Refresh tokens
rotate: each refresh returns a new refresh_token and invalidates the one you sent, so always
persist the newest value.
curl -X POST "https://api-tom.usebubbles.com/oauth/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d grant_type=refresh_token \ -d refresh_token=$REFRESH_TOKEN \ -d client_id=$CLIENT_ID6. Revoke
Revoke a refresh or access token when the user disconnects your app:
curl -X POST "https://api-tom.usebubbles.com/oauth/revoke" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d token=$REFRESH_TOKEN \ -d client_id=$CLIENT_IDScopes
| Scope | Grants |
|---|---|
bubbles:read | Read your bubbles, including transcripts, comments and AI summaries |
webhooks:manage | Create and manage webhook subscriptions on your behalf |
meetings:join | Send the Bubbles notetaker to join and record a meeting on your behalf |