API Reference
The Bubbles API is a REST API served under the /v1 prefix. Responses are JSON. Set the two
session-stable values once in your shell; per-call ids (like a bubble id) are inline placeholders you
substitute as you go:
export BASE_URL="https://api-tom.usebubbles.com" # the origin serving the APIexport BUBBLES_TOKEN="bbls_pat_…" # a personal access token (see Authentication)Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
POST | /v1/api-keys | session | Create a personal access token (shown exactly once). Requires a workspace with paid-feature access (403 pro_plan_required otherwise); at most 10 active keys per user (409 invalid_request). |
GET | /v1/api-keys | session | List the caller’s active API keys (previews only; the secret is never returned). |
DELETE | /v1/api-keys/{id} | session | Revoke an API key. A revoked token yields the distinct 401 token_revoked envelope on use. |
GET | /v1/me | pat | The authenticated user (id, email, name, picture_url). |
GET | /v1/teams | pat | The caller’s teams with plan, the caller’s role, and the channels visible to them. Matches in-product membership: archived teams and hidden guest memberships (joined via a public bubble link) are excluded. |
GET | /v1/bubbles | pat | The caller’s bubbles, newest first — the bubbles they participate in (same completeness as the product home screen; public-but-unjoined bubbles do not list but are fetchable by id). Excludes archived, draft, template, and expired-hidden bubbles. Each row includes the bubble’s AI meeting-notes/summary text (summary, capped at 1000 characters; null when there are no notes) — the same text GET /v1/bubbles/{id} returns. Unknown query parameters are ignored; malformed values yield 400 invalid_request. |
GET | /v1/bubbles/search | pat | Search the caller’s bubbles by keyword, returning matching bubble summaries ranked by relevance. Matches fuzzily (prefix/typo-tolerant) against each bubble’s title, its author’s name, and its AI-generated meeting notes/summary — so a keyword that appears only inside the notes still finds the bubble; title matches rank highest, then author, then notes. Searches the SAME readable set as GET /v1/bubbles — a bubble the caller cannot read is never returned (a notes-only match on an unreadable bubble never surfaces). Each result row includes the bubble’s AI meeting-notes/summary text (summary, capped at 1000 characters; null when there are no notes) so an agent can read why it matched inline. q is required (non-empty, ≤256 chars); an empty or overlong q yields 400 invalid_request. Supports the same team_id/space_id/type/created_after/created_before filters as the list endpoint. Pagination is by opaque cursor (relevance-ordered); rows exclude transcripts and comments (fetch a bubble by id for those). |
GET | /v1/bubbles/{id} | pat | A single bubble by full uuid or by the short id used in app URLs. Nonexistent, archived, and unreadable bubbles are indistinguishable: all answer the identical 404 not_found envelope. |
GET | /v1/bubbles/{id}/transcript | pat | The bubble’s diarized transcript. 404 not_found when the bubble has no transcript (yet); the same uniform 404 for unreadable/nonexistent bubbles. |
GET | /v1/bubbles/{id}/comments | pat | The bubble’s comments in reading order (oldest first). Archived and draft threads/comments are excluded, matching what a reader sees in the product. |
GET | /v1/openapi.json | none | The OpenAPI 3.1 document describing this API, generated from this same registry. Unauthenticated; its servers entry is derived from the request host at runtime (no hardcoded base URL). |
POST | /v1/webhooks | pat | Create a webhook subscription for recording-ready events; the signing secret is returned exactly once. Requires the webhooks:manage scope (PATs carry it implicitly) and paid-feature access. Also accepts an app session token (the settings UI). |
GET | /v1/webhooks | pat | List the caller’s webhook subscriptions (the signing secret is omitted). Requires the webhooks:manage scope. |
GET | /v1/webhooks/{id} | pat | Fetch one webhook subscription by id (the signing secret is omitted); 404 not_found if it does not exist or is not the caller’s. Requires the webhooks:manage scope. |
PATCH | /v1/webhooks/{id} | pat | Update a webhook subscription’s url, events, filters, or status. Each field you include is replaced wholesale (a partial update at the top level, not a deep merge): sending filters replaces the entire filters object — e.g. PATCH { "filters": { "only_my_bubbles": false } } drops a previously-set bubble_types — and an empty { "filters": {} } clears all filters. Omit a field to leave it unchanged. status accepts "active" only — used to re-enable a subscription auto-disabled after repeated delivery failures; there is no manual disable. Requires the webhooks:manage scope. |
DELETE | /v1/webhooks/{id} | pat | Delete a webhook subscription. Requires the webhooks:manage scope. |
POST | /v1/webhooks/{id}/test | pat | Queue a synthetic webhook.test delivery through the signed delivery path (202 queued), to verify a receiver end-to-end. Requires the webhooks:manage scope. |
POST | /v1/meetings/join | pat | Send the Bubbles notetaker to join and record a meeting by link. Requires the meetings:join scope (PATs carry it implicitly). meeting_url must be a Google Meet, Microsoft Teams, Zoom, Webex, or GoTo link — any other link is rejected with 400 invalid_request and no notetaker is sent. team_id is the workspace the recording is saved into. Returns the created meeting-join resource; poll GET /v1/meetings/join/{id} for the live status and GET /v1/bubbles for the recording as it materializes. |
GET | /v1/meetings/join/{id} | pat | Read a dispatched join's live status. {id} is the join id returned by POST /v1/meetings/join. Requires the meetings:join scope. Returns { id, type:"meeting_join", status, bot_in_call, fatal_reason, team_id, updated_at } where status is one of dispatched | joining | in_waiting_room | in_call | recording | done | fatal (fatal_reason carries the reason, e.g. meeting_requires_sign_in). A join you do not own, or that does not exist, returns 404 not_found (indistinguishable). The meeting URL / passcode is never returned. |
POST | /v1/signup/start | none | Begin an agent-driven signup: submit an email, and Bubbles emails a verification code. The response is identical whether or not an account already exists (it never reveals whether the email is registered before the code is verified — the unified flow logs in existing users and signs up new ones). Returns a signup_session to pass to the next steps and relay_to_user text to show the user. Call again to resend. A malformed/disallowed/temporary email is rejected with 400 invalid_request. Per-IP throttled. |
POST | /v1/signup/verify | none | Verify the emailed code for a signup_session from POST /v1/signup/start. On success returns a new signup_session advanced to the workspace step (logging in an existing account or signing up a new one — the outcome is only revealed here, after the code is verified). A wrong or expired code returns 401 unauthorized (same rules as the web flow). |
POST | /v1/signup/workspaces | none | List the existing workspaces the verified email may join by its domain — only after the emailed code is verified (a not-yet-verified session returns 409). Only the domain-matched workspaces the verified user is allowed to see are returned; nothing workspace-related is exposed before verification. |
POST | /v1/signup/workspace | none | Join a listed existing workspace (action:"join", team_id) or create a new one (action:"create", workspace_name) — the same createTeamSnapshot / self-invite path the web flow uses. A team_id that is not joinable by the verified email domain is refused with 403 forbidden. On create, the response also carries the new workspace's free Pro-trial status with its benefits (pro_trial) and, when the verified email is a non-public company domain, a domain_join offer; the agent may then call action:"enable_domain_join" to let anyone with that verified email domain auto-join the workspace (only the user's own verified non-public company domain is ever accepted — never a public or consumer email domain). Returns the resulting team_id and advances to the calendar step. |
POST | /v1/signup/calendar | none | Return the Google and Microsoft work-calendar connection links plus the benefit explanation, so the agent can relay them to the user (connecting a calendar lets Bubbles auto-join and take notes). Signup is complete with or without connecting — the user can skip. |
GET | /v1/signup/start | none | GET alias of POST /v1/signup/start — same handler, same validation, same JSON. Begin an agent-driven signup by passing the email in the query string; Bubbles emails a verification code and returns a signup_session. Per-IP throttled. |
GET | /v1/signup/verify | none | GET alias of POST /v1/signup/verify — verify the emailed code for a signup_session via the query string. Same 401 on a wrong/expired code as the POST. |
GET | /v1/signup/workspaces | none | GET alias of POST /v1/signup/workspaces — list the domain-matched joinable workspaces for a verified signup_session (409 before the code is verified). Nothing workspace-related is exposed before verification. |
GET | /v1/signup/workspace | none | GET alias of POST /v1/signup/workspace — join a listed workspace (action=join&team_id=…), create one (action=create&workspace_name=…), or (after create, for a non-public verified domain) open it to domain-join (action=enable_domain_join). Same authz + refusals as the POST (a non-joinable team_id → 403; only the user's own verified non-public company domain is ever accepted). |
GET | /v1/signup/calendar | none | GET alias of POST /v1/signup/calendar — the calendar-connect links + the MCP connect handoff for a verified signup_session. Signup is complete with or without connecting. |
The machine-readable contract is served at GET /v1/openapi.json (OpenAPI 3.1, unauthenticated),
generated from the same route registry that serves these endpoints.
Authentication header
Every data endpoint takes a personal access token as a bearer token:
Authorization: Bearer $BUBBLES_TOKENThe API-key management endpoints (/v1/api-keys) are internal to the Bubbles app — they authenticate
with the app’s signed-in session token (Authorization: Bearer), not a PAT; create and revoke keys
from Settings → API keys. See Authentication.
The authenticated user
GET /v1/me — the token owner
curl "https://api-tom.usebubbles.com/v1/me" \ -H "Authorization: Bearer $BUBBLES_TOKEN"{ "id": "be0c0582-…", "email": "you@example.com", "name": "Ada Lovelace", "picture_url": null}Teams
GET /v1/teams — your teams, plans, roles, and channels
curl "https://api-tom.usebubbles.com/v1/teams" \ -H "Authorization: Bearer $BUBBLES_TOKEN"{ "data": [ { "id": "6114bc38-…", "name": "Acme", "plan": "free", "role": "admin", "channels": [{ "id": "955e6f13-…", "name": "General" }] } ]}Bubbles
GET /v1/bubbles returns your bubbles newest-first. It is paginated (see below) and accepts these
filters: team_id, space_id, type (meeting_recording | screen_recording | custom_image),
created_after, and created_before (both exclusive ISO-8601 instants). Unknown parameters are
ignored; malformed values return 400 invalid_request. To find bubbles by keyword rather than by
recency, use Search.
GET /v1/bubbles — first page
curl "https://api-tom.usebubbles.com/v1/bubbles?limit=2" \ -H "Authorization: Bearer $BUBBLES_TOKEN"GET /v1/bubbles — filter by type
curl "https://api-tom.usebubbles.com/v1/bubbles?type=meeting_recording&limit=2" \ -H "Authorization: Bearer $BUBBLES_TOKEN"{ "data": [ { "id": "1dd21223-…", "type": "meeting_recording", "title": "Weekly sync", "url": "https://tom.usebubbles.com/…", "created_at": "2026-07-03T15:45:49.193Z", "team_id": "6114bc38-…", "channel_id": "955e6f13-…", "author_id": "ceaf1bf2-…", "thumbnail_url": "https://…/weekly-sync.thumb.png", "duration_seconds": 1834.5, "comment_count": 0, "expires_at": null, "has_summary": true } ], "has_more": true, "next_cursor": "eyJ2IjoxLCJjcmVhdGVkX2F0Ijo…"}Search
GET /v1/bubbles/search finds your bubbles by keyword, ranked by relevance — use it when you know a
topic, project, person, or something discussed rather than a bubble’s position in the list. It matches
fuzzily (prefix- and typo-tolerant) against each bubble’s title, its author’s name, and its
AI-generated meeting notes/summary, so a keyword that appears only inside the notes still finds the
bubble (title matches rank highest, then author, then notes). It searches the same readable set as
GET /v1/bubbles, so a bubble you can’t read is never returned. Each row carries the bubble’s notes
text in summary (capped at 1,000 characters; null when there are no notes) so you can read why it
matched without a second request. q is required (non-empty, ≤256 characters); an empty or overlong
q returns 400 invalid_request. It accepts the same team_id, space_id, type, created_after,
and created_before filters as the list endpoint and paginates by the same opaque cursor.
GET /v1/bubbles/search — ranked matches with notes text
curl "https://api-tom.usebubbles.com/v1/bubbles/search?q=weekly" \ -H "Authorization: Bearer $BUBBLES_TOKEN"{ "data": [ { "id": "1dd21223-…", "type": "meeting_recording", "title": "Weekly sync", "url": "https://tom.usebubbles.com/…", "created_at": "2026-07-03T15:45:49.193Z", "team_id": "6114bc38-…", "channel_id": "955e6f13-…", "author_id": "ceaf1bf2-…", "thumbnail_url": "https://…/weekly-sync.thumb.png", "duration_seconds": 1834.5, "comment_count": 0, "expires_at": null, "has_summary": true, "summary": "1. Shipped the read endpoints.\n2. Adversary probes next.\n3. PR against dev." } ], "has_more": false, "next_cursor": null}A single bubble
Fetch by full uuid or by the short id from an app URL. Nonexistent, archived, and unreadable bubbles
all return the identical 404 not_found envelope, so ids cannot be probed.
GET /v1/bubbles/{id}
curl "https://api-tom.usebubbles.com/v1/bubbles/$BUBBLE_ID" \ -H "Authorization: Bearer $BUBBLES_TOKEN"{ "id": "1dd21223-…", "type": "meeting_recording", "title": "Weekly sync", "url": "https://tom.usebubbles.com/…", "created_at": "2026-07-03T15:45:49.193Z", "team_id": "6114bc38-…", "channel_id": "955e6f13-…", "channel_name": "General", "author": { "id": "be0c0582-…", "name": "Ada Lovelace", "email": "you@example.com" }, "participants": [{ "id": "be0c0582-…", "name": "Ada Lovelace", "email": "you@example.com" }], "media": { "video_url": "https://…/weekly-sync.mp4", "thumbnail_url": "https://…/weekly-sync.thumb.png", "screenshot_url": null, "duration_seconds": 1834.5, "is_processed": true }}Transcript
The diarized transcript. format=json (default) returns one segment per diarized entry with speaker
and start/end seconds; format=text returns the plain-text, speaker-grouped rendition. A bubble with
no transcript returns the same uniform 404 not_found.
GET /v1/bubbles/{id}/transcript (json)
curl "https://api-tom.usebubbles.com/v1/bubbles/$BUBBLE_ID/transcript" \ -H "Authorization: Bearer $BUBBLES_TOKEN"{ "bubble_id": "1dd21223-…", "format": "json", "segments": [ { "speaker": { "id": "be0c0582-…", "name": "Ada Lovelace" }, "text": "Morning everyone, thanks for joining.", "start_time_seconds": 0, "end_time_seconds": 6 } ]}Comments
Comments in reading order (oldest first); archived and draft threads are excluded. Paginated.
GET /v1/bubbles/{id}/comments
curl "https://api-tom.usebubbles.com/v1/bubbles/$BUBBLE_ID/comments?limit=2" \ -H "Authorization: Bearer $BUBBLES_TOKEN"{ "data": [ { "id": "912b19a1-…", "thread_id": "5084a9d4-…", "bubble_id": "1dd21223-…", "kind": "text", "body": "Great progress — shipping today.", "video_url": null, "image_url": null, "created_at": "2026-07-03T15:12:30.193Z", "author": { "id": "be0c0582-…", "name": "Ada Lovelace" }, "is_pinned": true, "timestamp_seconds": 12.5, "url": "https://tom.usebubbles.com/…" } ], "has_more": false, "next_cursor": null}Meetings
Send the Bubbles AI notetaker to join and record a live meeting by its link. This is a write endpoint — it dispatches the notetaker to join the call on your behalf and saves the resulting recording into a workspace you choose.
| Method & path | Purpose |
|---|---|
POST /v1/meetings/join | Send the notetaker to join and record a meeting by link. |
GET /v1/meetings/join/{id} | Read a dispatched join’s live status. |
Requires the meetings:join scope. Personal access tokens carry it implicitly; an OAuth token must
have been granted it on the consent screen. Supported meeting links are Google Meet,
Microsoft Teams, Zoom, Webex, and GoTo — any other link is rejected with 400 invalid_request and
no notetaker is sent.
curl -X POST "https://api-tom.usebubbles.com/v1/meetings/join" \ -H "Authorization: Bearer $BUBBLES_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "meeting_url": "https://meet.google.com/abc-defg-hij", "team_id": "6114bc38-…" }'A 201 Created returns the dispatched meeting-join resource (never a bare 2xx). team_id is
required — the workspace to record into (see GET /v1/teams). The returned id is the join id you
poll for status (below); poll GET /v1/bubbles for the recording itself as it materializes after
the meeting:
{ "id": "d49615f3-…", "type": "meeting_join", "status": "dispatched", "meeting_url": "https://meet.google.com/abc-defg-hij", "team_id": "6114bc38-…", "created_at": "2026-07-05T16:31:14.317Z"}Other responses: 403 forbidden if you cannot record for that workspace, or your token lacks the
meetings:join scope; 429 rate_limited if you exceed the hourly dispatch limit or your team’s
free-plan weekly recording cap.
Poll the join status
Read the live status of a dispatched join with its id. Requires the same meetings:join scope.
curl "https://api-tom.usebubbles.com/v1/meetings/join/{id}" \ -H "Authorization: Bearer $BUBBLES_TOKEN"{ "id": "d49615f3-…", "type": "meeting_join", "status": "recording", "bot_in_call": true, "fatal_reason": null, "team_id": "6114bc38-…", "updated_at": "2026-07-05T16:32:03.114Z"}status advances through dispatched → joining → in_waiting_room → in_call → recording →
done, or fatal if the notetaker could not record (for example a meeting that requires signing
in — fatal_reason carries the reason). bot_in_call is true while the notetaker is present in
the call. The meeting URL and any passcode are never returned by this endpoint. A join you do
not own, or one that does not exist, returns the identical 404 not_found — the two are
indistinguishable.
Signing up (agent-driven)
The recommended way to onboard through an assistant is the MCP connector at
https://api-tom.usebubbles.com/mcp (see MCP Server), which signs the user in inside the conversation; this HTTP
flow is the fallback for clients that cannot add an MCP connector. It lets an AI assistant create an account for a user over plain HTTP — no
dashboard needed — and handles the whole flow: email verification, choosing or creating a workspace,
and calendar setup. The five steps are unauthenticated and carry state in a short-lived,
single-purpose signup_session token returned by each step — it is not an API credential (the
read/write endpoints above reject it). Every response includes a next_step and a relay_to_user
message the agent can show the user.
- Start — submit an email; Bubbles emails a verification code.
POST /v1/signup/start — email the verification code
curl -X POST "https://api-tom.usebubbles.com/v1/signup/start" \ -H "Content-Type: application/json" \ -d '{"email": "you@example.com"}'- Verify the code —
POST /v1/signup/verifywith{ signup_session, code }. - Resolve workspaces —
POST /v1/signup/workspaceswith{ signup_session }lists any workspaces the verified email can join by its domain. - Join or create —
POST /v1/signup/workspacewith{ signup_session, action: "join", team_id }or{ signup_session, action: "create", workspace_name }. - Connect a calendar —
POST /v1/signup/calendarreturns the Google and Microsoft connect links and the reasons to connect (so Bubbles can auto-join and take notes on your meetings); the user can skip. Signup is complete either way.
The full request/response schemas for each step are in the endpoint table and field reference above.
Pagination
List endpoints (/v1/bubbles, /v1/bubbles/{id}/comments) return an envelope of
{ data, has_more, next_cursor }. Pass limit (integer 1–100, default 25) to size a page. When
has_more is true, pass the opaque next_cursor back as ?cursor= to fetch the next page:
curl "https://api-tom.usebubbles.com/v1/bubbles?cursor=$NEXT_CURSOR" \ -H "Authorization: Bearer $BUBBLES_TOKEN"Cursors are opaque keyset positions — treat them as configuration and never parse or construct one. They stay stable across concurrent inserts and deletes.
Field reference
Every field returned by (or accepted by) these endpoints, typed. Every response field is always
present — an unset value is null, never a missing key. T | null marks a nullable value; T[]
an array; object-typed fields link to their own DTO. These are the same schemas published in
GET /v1/openapi.json (components.schemas), so a code generator produces the exact
types shown here.
BubblesPerson
A person on a bubble (author or participant).
| Field | Type | Description |
|---|---|---|
id | string | The Bubbles user id. |
name | string | null | Display name, or null when unknown. |
email | string | null | Email, or null when unknown/withheld. |
BubblesMedia
The bubble's media assets. Absolute https URLs; unset assets are null.
| Field | Type | Description |
|---|---|---|
video_url | string | null | The processed video URL, or null (e.g. an image bubble). |
thumbnail_url | string | null | The thumbnail/preview image URL, or null. |
screenshot_url | string | null | The screenshot/still-image URL, or null. |
duration_seconds | number | null | Media duration in seconds, or null. |
is_processed | boolean | null | Whether media processing finished, or null when unknown. |
BubblesMeeting
Meeting metadata, present only for meeting_recording bubbles (else null).
| Field | Type | Description |
|---|---|---|
platform | string | null | The meeting platform (e.g. google_meet, zoom), or null. |
start_time | string | null | Scheduled/actual meeting start (ISO-8601 UTC), or null. |
Bubble
A single bubble (GET /v1/bubbles/{id}). Every field is always present; "unset" is null.
| Field | Type | Description |
|---|---|---|
id | string | The bubble uuid. |
type | "meeting_recording" | "screen_recording" | "custom_image" | The bubble classification. |
title | string | null | The bubble title, or null. |
url | string | The app deep link to the bubble. |
created_at | string | null | Creation instant (ISO-8601 UTC), or null for legacy bubbles. |
team_id | string | null | Owning team id, or null. |
channel_id | string | null | Owning channel (product: space) id, or null. |
channel_name | string | null | Owning channel name, or null. |
author | BubblesPerson | null | The author (for meetings, the calendar organizer), or null. |
participants | BubblesPerson[] | People who participated in the bubble. |
media | BubblesMedia | The bubble's media assets. |
summary | string | null | The AI-generated summary text, when generated, else null. |
action_items | string | null | The AI-generated action items as plain text (v1), when generated, else null. |
has_transcript | boolean | Whether a transcript is available for this bubble. |
comment_count | integer | Number of comments on the bubble. |
is_public | boolean | Whether the bubble is shared via a public link. |
embed_code | string | null | HTML embed snippet, or null when not embeddable. |
expires_at | string | null | Expiry instant (ISO-8601 UTC), or null when it does not expire. |
archived | boolean | Whether the bubble is archived. |
creator_id | string | null | The raw creator user id, or null. |
response_required | boolean | Whether the bubble requests a response from viewers. |
has_external_participants | boolean | Whether anyone outside the team participated. |
external_participant_email_domains | string[] | Email domains of external participants. |
meeting | BubblesMeeting | null | Meeting metadata for meeting_recording bubbles, else null. |
BubblesSummary
A list-item bubble (GET /v1/bubbles), derived from summaries — no hydration.
| Field | Type | Description |
|---|---|---|
id | string | The bubble uuid. |
type | "meeting_recording" | "screen_recording" | "custom_image" | The bubble classification. |
title | string | null | The bubble title, or null. |
url | string | The app deep link to the bubble. |
created_at | string | null | Creation instant (ISO-8601 UTC), or null. |
team_id | string | null | Owning team id, or null. |
channel_id | string | null | Owning channel id, or null. |
author_id | string | null | Raw creator id (no name — list is unhydrated), or null. |
thumbnail_url | string | null | The thumbnail image URL, or null. |
duration_seconds | number | null | Media duration in seconds, or null. |
comment_count | integer | Number of comments on the bubble. |
expires_at | string | null | Expiry instant (ISO-8601 UTC), or null. |
has_summary | boolean | Whether an AI meeting-notes summary exists (see summary). |
summary | string | null | The AI-generated meeting notes/summary text (the same text GET /v1/bubbles/{id} returns as summary), or null when there are no notes. Capped at 1000 characters for row size — a truncated value ends with an ellipsis; fetch the bubble by id for the full text. |
User
The authenticated user (GET /v1/me).
| Field | Type | Description |
|---|---|---|
id | string | The Bubbles user id. |
email | string | null | Email, or null. |
name | string | null | Display name, or null. |
picture_url | string | null | Avatar URL, or null. |
TeamChannel
A channel (product: space) visible to the caller.
| Field | Type | Description |
|---|---|---|
id | string | The channel id. |
name | string | null | The channel name, or null. |
Team
A team from GET /v1/teams, with the caller's role and visible channels.
| Field | Type | Description |
|---|---|---|
id | string | The team id. |
name | string | null | The team name, or null. |
plan | "free" | "pro" | The team's plan. |
role | "admin" | "member" | "guest" | null | The caller's role in this team, or null. |
channels | TeamChannel[] | Channels (spaces) visible to the caller. |
TranscriptSpeaker
The speaker of a transcript segment.
| Field | Type | Description |
|---|---|---|
id | string | null | The Bubbles user id when the speaker is a known user, else null. |
name | string | null | The diarized speaker name when available, else null. |
TranscriptSegment
One diarized transcript entry.
| Field | Type | Description |
|---|---|---|
speaker | TranscriptSpeaker | null | The segment speaker, or null. |
text | string | The spoken text of the segment. |
start_time_seconds | number | Segment start, seconds from media start. |
end_time_seconds | number | Segment end, seconds from media start. |
TranscriptJson
A diarized transcript (format=json, the default).
| Field | Type | Description |
|---|---|---|
bubble_id | string | The bubble uuid this transcript belongs to. |
format | "json" | The transcript format. |
segments | TranscriptSegment[] | The diarized segments in order. |
TranscriptText
A plain-text transcript (format=text).
| Field | Type | Description |
|---|---|---|
bubble_id | string | The bubble uuid this transcript belongs to. |
format | "text" | The transcript format. |
text | string | The plain-text rendition: `Speaker: …` blocks separated by blank lines. |
Transcript
GET /v1/bubbles/{id}/transcript — json (default) or text per the format query param.
One of: TranscriptJson | TranscriptText
.
CommentAuthor
The author of a comment.
| Field | Type | Description |
|---|---|---|
id | string | The Bubbles user id. |
name | string | null | Display name, or null. |
Comment
A comment on a bubble (GET /v1/bubbles/{id}/comments).
| Field | Type | Description |
|---|---|---|
id | string | The comment id. |
thread_id | string | The containing thread id. |
bubble_id | string | The bubble uuid the comment is on. |
kind | "text" | "video" | "image" | The comment kind. |
body | string | null | Text-comment body; null for video/image comments. |
video_url | string | null | Video-comment media URL, or null. |
image_url | string | null | Image-comment media URL, or null. |
created_at | string | Creation instant (ISO-8601 UTC). |
author | CommentAuthor | The comment author. |
is_pinned | boolean | Whether the containing thread is pinned. |
timestamp_seconds | number | null | The anchored video timestamp, or null when not anchored. |
url | string | Deep link into the app opening this comment. |
WebhookSubscriptionFilters
Optional post-match filters on a webhook subscription.
| Field | Type | Description |
|---|---|---|
bubble_types (optional) | string[] | Only deliver for these bubble types. |
only_my_bubbles (optional) | boolean | Only deliver for bubbles the subscriber created. |
WebhookSubscription
A webhook subscription (GET /v1/webhooks[/{id}]); the signing secret is omitted.
| Field | Type | Description |
|---|---|---|
id | string | The subscription id (wh_…). |
url | string | The receiver URL deliveries are POSTed to. |
events | string[] | Subscribed event types (bubble.ready | meeting_recording.ready | screen_recording.ready). |
team_id | string | null | Team scope, or null (all teams). |
space_id | string | null | Channel scope, or null. |
filters | WebhookSubscriptionFilters | null | Post-match filters, or null. |
status | "active" | "disabled" | Delivery status; auto-disabled after repeated failures. |
created_at | string | Creation instant (ISO-8601 UTC). |
last_success_at | string | null | Last successful delivery instant, or null. |
last_failure_at | string | null | Last failed delivery instant, or null. |
disabled_at | string | null | When the subscription was disabled, or null. |
disabled_reason | string | null | Why it was disabled (e.g. delivery_failures), or null. |
WebhookSubscriptionCreated
POST /v1/webhooks 201 response — the same shape plus the signing secret, shown exactly once.
| Field | Type | Description |
|---|---|---|
id | string | The subscription id (wh_…). |
url | string | The receiver URL deliveries are POSTed to. |
events | string[] | Subscribed event types. |
team_id | string | null | Team scope, or null. |
space_id | string | null | Channel scope, or null. |
filters | WebhookSubscriptionFilters | null | Post-match filters, or null. |
status | "active" | "disabled" | Delivery status. |
created_at | string | Creation instant (ISO-8601 UTC). |
last_success_at | string | null | Last successful delivery instant, or null. |
last_failure_at | string | null | Last failed delivery instant, or null. |
disabled_at | string | null | When disabled, or null. |
disabled_reason | string | null | Why disabled, or null. |
secret | string | The signing secret (whsec_…) for HMAC verification — returned ONCE. |
ApiKeySummary
A managed API key (GET /v1/api-keys); the secret is never returned.
| Field | Type | Description |
|---|---|---|
id | string | The key id. |
name | string | The key name. |
token_preview | string | A safe-to-show token preview (bbls_pat_ab12…). |
created_at | string | Creation instant (ISO-8601 UTC). |
last_used_at | string | null | Last-used instant, or null if never used. |
ApiKeyCreated
POST /v1/api-keys 201 response — the full token is shown exactly once.
| Field | Type | Description |
|---|---|---|
id | string | The key id. |
name | string | The key name. |
token | string | The full personal access token (bbls_pat_…) — shown ONCE; store it now. |
token_preview | string | A safe-to-show token preview. |
created_at | string | Creation instant (ISO-8601 UTC). |
MeetingJoinCreated
POST /v1/meetings/join 201 response — the dispatched notetaker resource.
| Field | Type | Description |
|---|---|---|
id | string | The join id; poll GET /v1/meetings/join/{id}. |
type | "meeting_join" | The resource type. |
status | string | Initial status (dispatched). |
meeting_url | string | The normalized meeting link the notetaker was sent to. |
team_id | string | The workspace the recording is saved into. |
created_at | string | Dispatch instant (ISO-8601 UTC). |
MeetingJoinStatus
GET /v1/meetings/join/{id} — a dispatched join's live status. The meeting URL/passcode is never returned.
| Field | Type | Description |
|---|---|---|
id | string | The join id. |
type | "meeting_join" | The resource type. |
status | "dispatched" | "joining" | "in_waiting_room" | "in_call" | "recording" | "done" | "fatal" | The live join status. |
bot_in_call | boolean | Whether the notetaker is currently in the call. |
fatal_reason | string | null | The failure reason when status is fatal (e.g. meeting_requires_sign_in), else null. |
team_id | string | null | The workspace the recording is saved into, or null. |
updated_at | string | null | Last status update instant (ISO-8601 UTC), or null. |
BubblesList
GET /v1/bubbles — cursor-paginated list of bubble summaries.
| Field | Type | Description |
|---|---|---|
data | BubblesSummary[] | The page of bubbles. |
has_more | boolean | Whether another page exists. |
next_cursor | string | null | Opaque cursor for the next page, or null when exhausted. |
CommentList
GET /v1/bubbles/{id}/comments — cursor-paginated list of comments.
| Field | Type | Description |
|---|---|---|
data | Comment[] | The page of comments. |
has_more | boolean | Whether another page exists. |
next_cursor | string | null | Opaque cursor for the next page, or null. |
TeamList
GET /v1/teams — the finite list of the caller's teams.
| Field | Type | Description |
|---|---|---|
data | Team[] | The caller's teams. |
ApiKeyList
GET /v1/api-keys — the finite list of the caller's active keys.
| Field | Type | Description |
|---|---|---|
data | ApiKeySummary[] | The caller's active API keys. |
WebhookSubscriptionList
GET /v1/webhooks — the finite list of the caller's subscriptions.
| Field | Type | Description |
|---|---|---|
data | WebhookSubscription[] | The caller's webhook subscriptions. |
CreateApiKeyRequest
POST /v1/api-keys request body.
| Field | Type | Description |
|---|---|---|
name | string | A human-readable name for the key (1–100 chars). |
CreateWebhookRequest
POST /v1/webhooks request body.
| Field | Type | Description |
|---|---|---|
url | string | The https receiver URL to POST deliveries to. |
events | string[] | Event types to subscribe to (at least one). |
team_id (optional) | string | Restrict to one team. |
space_id (optional) | string | Restrict to one channel. |
filters (optional) | WebhookSubscriptionFilters | Optional post-match filters. |
UpdateWebhookRequest
PATCH /v1/webhooks/{id} request body — all fields optional. status accepts "active" only (re-enable).
| Field | Type | Description |
|---|---|---|
url (optional) | string | A new receiver URL. |
events (optional) | string[] | A new event set. |
status (optional) | "active" | Set to "active" to re-enable an auto-disabled subscription. |
JoinMeetingRequest
POST /v1/meetings/join request body.
| Field | Type | Description |
|---|---|---|
meeting_url | string | A Google Meet, Microsoft Teams, Zoom, Webex, or GoTo meeting link. |
team_id | string | The workspace to save the recording into (see GET /v1/teams). |
SignupStartRequest
POST /v1/signup/start request body.
| Field | Type | Description |
|---|---|---|
email | string | The work email to sign up / send the verification code to. |
SignupStartResponse
POST /v1/signup/start response — a verification code was emailed. The response is identical whether or not an account already exists (it never reveals whether the email is registered before the code is verified).
| Field | Type | Description |
|---|---|---|
next_step | "verify" | The next call to make. |
agent_context | string | Agent-facing guidance for this step: what happened, what to tell the user, and what to ask/call next. |
relay_to_user | string | Plain-language text for the agent to show the user. |
signup_session | string | Opaque token to pass to the next step. |
SignupVerifyRequest
POST /v1/signup/verify request body.
| Field | Type | Description |
|---|---|---|
signup_session | string | The token from POST /v1/signup/start. |
code | string | The verification code Bubbles emailed the user. |
SignupVerifyResponse
POST /v1/signup/verify response — email verified.
| Field | Type | Description |
|---|---|---|
next_step | "workspaces" | The next call to make. |
agent_context | string | Agent-facing guidance for this step: what happened, what to tell the user, and what to ask/call next. |
relay_to_user | string | Plain-language text for the agent to show the user. |
signup_session | string | Opaque token advanced to the workspace step. |
SignupWorkspacesRequest
POST /v1/signup/workspaces request body.
| Field | Type | Description |
|---|---|---|
signup_session | string | A verified signup token. |
SignupWorkspace
A workspace the verified email may join by domain match.
| Field | Type | Description |
|---|---|---|
id | string | The workspace (team) id — pass as team_id to join. |
name | string | The workspace name. |
SignupWorkspacesResponse
POST /v1/signup/workspaces response — the domain-matched joinable set.
| Field | Type | Description |
|---|---|---|
next_step | "workspace" | The next call to make. |
workspaces | SignupWorkspace[] | Joinable workspaces (may be empty → create a new one). |
agent_context | string | Agent-facing guidance for this step: what happened, what to tell the user, and what to ask/call next. |
relay_to_user | string | Plain-language text for the agent to show the user. |
signup_session | string | Opaque token to pass to the next step. |
SignupWorkspaceRequest
POST /v1/signup/workspace request body — join a listed workspace, create a new one, or (after create, if the response offered it) enable domain-join.
| Field | Type | Description |
|---|---|---|
signup_session | string | A verified signup token. |
action | "join" | "create" | "enable_domain_join" | Join an existing workspace, create a new one, or (after creating, for a non-public verified domain the response offered) allow anyone with that verified email domain to auto-join. |
team_id (optional) | string | For action:"join" — a listed, domain-matched workspace id. |
workspace_name (optional) | string | For action:"create" — the new workspace name. |
SignupProTrial
The new workspace's free Pro-trial status (present only when creating).
| Field | Type | Description |
|---|---|---|
active | boolean | Whether the new workspace is on a free Pro trial. |
plan | string | The workspace plan (e.g. "pro"). |
benefits | string[] | The Pro plan benefits to relay to the user. |
SignupDomainJoin
Whether the user may open their new workspace to their verified email domain (present only when creating).
| Field | Type | Description |
|---|---|---|
available | boolean | True when the verified email domain is a non-public company domain that can be offered for auto-join. |
domain | string | The user's verified email domain. |
SignupWorkspaceResponse
POST /v1/signup/workspace response — workspace ready.
| Field | Type | Description |
|---|---|---|
next_step | "calendar" | The next call to make. |
team_id (optional) | string | The workspace the user joined or created. |
pro_trial (optional) | SignupProTrial | Free Pro-trial status (present when creating). |
domain_join (optional) | SignupDomainJoin | Domain-join offer status (present when creating). |
domain_join_enabled (optional) | boolean | True on an action:"enable_domain_join" response — the domain is now open for auto-join. |
domain (optional) | string | The domain opened for auto-join (enable_domain_join response). |
agent_context | string | Agent-facing guidance for this step: what happened, what to tell the user, and what to ask/call next. |
relay_to_user | string | Plain-language text for the agent to show the user. |
signup_session | string | Opaque token advanced to the calendar step. |
SignupCalendarRequest
POST /v1/signup/calendar request body.
| Field | Type | Description |
|---|---|---|
signup_session | string | A verified signup token. |
SignupMcpTool
One tool the connected MCP server exposes.
| Field | Type | Description |
|---|---|---|
name | string | The tool name (e.g. "search_bubbles"). |
title | string | The human-readable tool title. |
kind | "read" | "write" | Whether the tool only reads data or performs a write action. |
summary | string | A one-line summary of what the tool does. |
SignupMcpConnect
How to connect an AI assistant to Bubbles over MCP, plus the connected server's tools. Reuses the same OAuth 2.1 / dynamic-client-registration / RFC-9728 flow as the documented MCP server — no second connect surface.
| Field | Type | Description |
|---|---|---|
endpoint | string | The Bubbles MCP server URL to add to an MCP client. |
oauth_protected_resource | string | The RFC 9728 protected-resource metadata URL an MCP client discovers the authorization server from. |
scopes | string[] | The OAuth scopes the connected client may be granted. |
how_to_connect | string | Provider-agnostic, step-by-step connect instructions (add the URL, one-time browser consent) for the agent to relay. |
tools | SignupMcpTool[] | The tools the connected MCP server exposes. |
SignupCalendarResponse
POST /v1/signup/calendar response — calendar connect links + MCP connect + skip.
| Field | Type | Description |
|---|---|---|
next_step | "done" | Signup is complete after this step. |
why | string[] | The benefits of connecting a work calendar. |
google_connect_url | string | Link for the user to connect Google Calendar. |
microsoft_connect_url | string | Link for the user to connect Microsoft/Outlook Calendar. |
skip | boolean | Always true — the user may skip connecting a calendar. |
mcp | SignupMcpConnect | How to connect an AI assistant to Bubbles over MCP + the connected tools (owner directive 5885769d). |
agent_context | string | Agent-facing guidance for this step: what happened, what to tell the user, and what to ask/call next. |
relay_to_user | string | Plain-language text for the agent to show the user. |
signup_session | string | The signup token (signup is complete). |
Errors
Every error uses the envelope { "error": { "type": …, "message": … } } with one of these closed
types:
| Type | HTTP status | Meaning |
|---|---|---|
invalid_request | 400 | The request body or parameters are malformed, or a constraint was violated (e.g. the active API key cap, returned with status 409). |
unauthorized | 401 | No credential was provided, the credential is malformed or unknown, or the credential kind is not accepted on this endpoint. |
token_revoked | 401 | The personal access token used has been revoked. |
account_deleted | 401 | The account that owns this credential has been deleted/anonymized or disabled; the credential is rejected fail-closed even though the token itself is otherwise valid. |
forbidden | 403 | The authenticated user may not perform this operation. |
pro_plan_required | 403 | The operation requires a workspace that can access paid features (e.g. creating API keys). |
not_found | 404 | The resource does not exist — or, for bubble reads, exists but is not readable by the caller (the response is identical to a missing resource so ids cannot be probed). |
rate_limited | 429 | The per-credential rate limit was exceeded; retry after the number of seconds in the Retry-After header. |
internal_error | 500 | An unexpected server error occurred; it has been reported. |
An unauthenticated request:
{ "error": { "type": "unauthorized", "message": "Missing bearer token" } }Rate limits
Requests are limited to 120 per minute per credential. Exceeding the limit returns
429 rate_limited; the Retry-After response header carries the number of seconds until the window
resets. Back off for that long before retrying.