Skip to content

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).

The simplest way is from the Bubbles app — no session handling required:

  1. In Bubbles, open Settings → API keys.
  2. 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:

Terminal window
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

Terminal window
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):

Terminal window
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=S256

4. Exchange the code for tokens

Your callback exchanges the code plus the original code_verifier at the token endpoint:

Terminal window
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.

Terminal window
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_ID

6. Revoke

Revoke a refresh or access token when the user disconnects your app:

Terminal window
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_ID

Scopes

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