# Wave API — integration docs for agents > The Wave API is the public REST surface for reading and writing a user's > Wave recordings — meetings, phone calls, lectures, podcasts, YouTube > imports. Token-authenticated, OAuth-scoped, OpenAPI-documented. Use this > when you're building a server-side integration. Use mcp.wave.co when > you're inside a conversational LLM client. Base URL: https://api.wave.co OpenAPI: https://api.wave.co/v1/openapi.json Interactive reference: https://api.wave.co/reference TypeScript SDK: bundled with @waveai/cli; matching client at sdk/index.ts Wave CLI: npm i -g @waveai/cli Related surfaces: app.wave.co (web app — mint tokens at /settings/integrations/api) mcp.wave.co (hosted MCP server for Claude/ChatGPT/Cursor) wave.co (product overview, see wave.co/llms.txt) Wave records and transcribes meetings, phone calls, lectures, podcasts, and imported audio/video. The Wave API lets external agents read that data (sessions, transcripts, summaries, folders, stats) and write back metadata (title, notes, tags, favorite), structured action items, and folder memberships. 200M+ minutes have been transcribed across the platform — every one of them is reachable through this API for the authenticated user who recorded it. --- ## Authentication All requests use Bearer tokens: `Authorization: Bearer wave_api_...` Tokens are created at https://app.wave.co/settings/integrations/api with per-scope permissions. Request only what you need. Scopes: - sessions:read — list/get session metadata, stats, bulk export (also accepted on folder reads) - sessions:search — semantic search - sessions:write — PATCH title/notes/tags/favorite/action_items - sessions:delete — DELETE a session - transcripts:read — GET transcript; also required for bulk `include_transcript: true` - media:read — signed audio/video URLs - account:read — GET /v1/account - folders:read — list/get folders (least-privilege grant for folder discovery) - folders:write — create/rename/recolour/delete folders; add/remove session memberships - events:read — pull and acknowledge the per-token event feed - webhooks:manage — register and manage webhooks Rate limits: 60 requests/minute, 10,000/day per token. 429 returned on breach. Error shape: ```json { "error": { "code": "not_found", "message": "Session not found" } } ``` --- ## Endpoints ### GET /v1/sessions — list sessions Query params: `limit` (≤100, default 20), `cursor` (ISO 8601, pass the previous response's `next_cursor`), `since` (ISO 8601), `type` (meeting, recording, phone, desktop, import, youtube, podcast), `folder` (folder id or case-insensitive name — see "Folders" below), `tag` (repeatable or comma-separated exact tags), and `tag_mode` (`any` or `all`). Only sessions with summaries are returned (i.e. finished processing). Example response: ```json { "sessions": [ { "id": "sess_01HX4R7MT8PQZ6VCN9ABYF2KJM", "title": "Weekly product sync", "timestamp": "2025-04-18T16:00:00.000Z", "duration_seconds": 1812, "type": "meeting", "platform": "zoom" } ], "next_cursor": "2025-04-17T20:30:00.000Z", "has_more": true } ``` ### GET /v1/sessions/{id} — session detail Returns a session's metadata including structured markdown summary, free-text notes, tags, and favorite flag. Transcript is NOT included here. Example response: ```json { "id": "sess_01HX4R7MT8PQZ6VCN9ABYF2KJM", "title": "Weekly product sync", "timestamp": "2025-04-18T16:00:00.000Z", "duration_seconds": 1812, "type": "meeting", "platform": "zoom", "language": "en", "summary": "## Overview\n…\n## Action items\n- [ ] JR: send legal the draft spec (Fri)", "notes": "Pre-read doc in Notion; follow up on legal timeline.", "tags": ["work", "roadmap"], "favorite": false } ``` The `summary` field is already structured markdown (Overview / Key points / Action items). Prefer passing it to downstream LLMs over paying inference on the raw transcript. Sessions of `type: "phone"` also carry a `phone` object (present on list, detail, search and bulk responses): ```json "phone": { "direction": "outbound", // "inbound" | "outbound" | null "to_number": "+15551234567", // dialed party (outbound) / your verified caller id (inbound) "from_number": "+15559876543", // your caller id (outbound) / null (inbound — bridge, unknown) "status": "completed", // ringing | in_progress | completed | missed | failed | null "ongoing": false, // true while the call is still live (ringing/in_progress) "provider": "telnyx", "contact_name": "Acme Co." } ``` Inbound calls are bridge connections, so the other party is unknown: `to_number` is your verified caller id and `from_number` is null. ### GET /v1/sessions/{id}/transcript — full transcript Returns BOTH a flat string and timed segments in one payload. Use `transcript` for LLM input; use `segments[]` for UIs that need timestamps. `start`/`end` are seconds as floats, not ISO timestamps. Example response: ```json { "id": "sess_01HX4R7MT8PQZ6VCN9ABYF2KJM", "transcript": "Josh: Alright, let's kick off…\nSam: Sure — I sent the draft last night.", "segments": [ { "speaker": "Josh", "start": 12.48, "end": 15.92, "text": "Alright, let's kick off…" }, { "speaker": "Sam", "start": 16.04, "end": 18.31, "text": "Sure — I sent the draft last night." } ] } ``` ### POST /v1/sessions/search — semantic search Body: `{ "query": "retail media strategy", "limit": 10, "tags": ["roadmap"], "tag_mode": "all" }` (limit ≤ 50). `tags` and `tag_mode` are optional. Each result includes a `snippet` — agents can triage without pulling the full session. Example response: ```json { "query": "retail media strategy", "results": [ { "id": "sess_01HX4R7MT8PQZ6VCN9ABYF2KJM", "title": "Weekly product sync", "timestamp": "2025-04-18T16:00:00.000Z", "type": "meeting", "similarity": 0.87, "snippet": "…we want to lead with retail media strategy in Q3…" } ], "total": 1 } ``` ### POST /v1/sessions/bulk — bulk export Body: `{ "session_ids": [...up to 50], "include_transcript": true, "include_summary": true }` Full transcripts come back in one request — ideal for backfilling a knowledge base. `include_transcript: true` requires the `transcripts:read` scope. ### GET /v1/sessions/stats — counts + totals Query params: `since`, `until` (ISO 8601; default last 30 days). Returns totals and breakdowns by type and platform. ### PATCH /v1/sessions/{id} — write-back metadata Body fields (any subset): `title` (≤500 chars), `notes` (≤50,000 chars), `tags` (array, ≤20 items, each ≤100 chars), `favorite` (boolean), or `action_items` (full replacement array, ≤200 items). Example request: ```json { "tags": ["work", "roadmap"], "favorite": true } ``` Fires `session.updated` webhook. For action-item edits, call `GET /v1/sessions/{id}/action-items` first. It returns `{ action_items, version, updated_at }`. Preserve existing item ids and pass `If-Match: ` on PATCH. A concurrent edit returns a version conflict instead of being overwritten. Action-item-only edits fire `session.action_items.updated`. Each action item: `text` (required, ≤2,000 chars), `id` (preserve for existing items), `completed`, `source` (agent | user), `assignee`, `due_date`. `due_date` is stored as ISO `YYYY-MM-DD`; send ISO or natural language such as "Friday", "next Tuesday" or "April 25" and it is normalized relative to today. `null` or `""` clears it; unparseable values are rejected with 400. Changing a known item's `text` or `due_date` stamps `user_edited_at` (epoch ms), which Wave's extraction pipeline respects when regenerating items. ### DELETE /v1/sessions/{id} — delete session Permanent. Storage files cleaned up asynchronously. Fires `session.deleted` webhook. ### GET /v1/sessions/{id}/media — signed media URLs Returns `{ audio_url, video_url, expires_at }`. URLs expire after 1 hour. ### GET /v1/account — whoami Returns `{ user_id, subscription_active, session_count }`. ### GET /v1/folders — list folders Returns the user's folders. Users organize sessions into folders in the Wave app; agents consume them as a scoping primitive. Pair with `GET /v1/sessions?folder=…`. Requires `folders:read` (`sessions:read` is also accepted). `created_at` / `updated_at` are ISO strings, present on folders created or modified since timestamps were introduced. The preferences `pinned` (boolean), `sort_order` (`newest` | `oldest` | `title`, the sort for sessions inside the folder), `order` (integer ≥ 0, manual position) and `last_opened_at` (ISO) are present only when set; absence means the default (not pinned, newest first, no manual position, never opened). Folders come back in display order: pinned first, then `order` ascending (folders with an `order` before those without), then name. Example response: ```json { "folders": [ { "id": "fld_01HX4R…", "name": "work", "color": "#6D28D9", "session_count": 82, "created_at": "2026-09-01T14:02:11.000Z", "updated_at": "2026-09-05T09:41:37.000Z", "pinned": true, "sort_order": "newest", "order": 0, "last_opened_at": "2026-09-06T08:15:02.000Z" }, { "id": "fld_01HX5S…", "name": "personal", "color": "#F59E0B", "session_count": 14 } ] } ``` ### GET /v1/folders/{id} — get one folder Same shape as a list entry. 404 if the folder does not exist. Requires `folders:read` (or `sessions:read`). ### POST /v1/folders — create a folder Body: `{ "name": "Customer research", "color": "#6D28D9" }`. `name` is trimmed, 1–100 chars; `color` is `#RRGGBB` or null. Requires `folders:write`. Matching names are resolved case-insensitively and return the existing folder, so creation is safe to retry. ### PATCH /v1/folders/{id} — rename / recolour / pin / reorder / sort Body: `{ "name"?: string, "color"?: "#RRGGBB" | null, "pinned"?: boolean, "sort_order"?: "newest" | "oldest" | "title", "order"?: integer 0..2^53-1 }` — at least one field. Omitted fields are unchanged; `updated_at` is bumped. Requires `folders:write`. 404 if missing. ### DELETE /v1/folders/{id} — delete a folder Returns 204. Only the folder is removed; sessions in it are never deleted. Requires `folders:write`. 404 if missing. ### POST /v1/sessions/{id}/folders/{folderId} — add membership Adds one non-exclusive folder membership. Idempotent. Bumps the folder's `updated_at`. ### DELETE /v1/sessions/{id}/folders/{folderId} — remove membership Removes only the folder relationship; the session and recording remain. Bumps the folder's `updated_at`. ### GET /v1/events — pull the per-token event feed Requires `events:read`. Accepts optional integer `cursor` and `limit` (default 50, max 200). Without an explicit cursor, Wave resumes from the server-tracked position for that token. Returns `events`, `next_cursor`, and `has_more`. ### POST /v1/events/ack — advance the event cursor Body: `{ "cursor": 148 }`. Cursor advancement is monotonic. Acknowledge only after downstream processing succeeds. --- ## Webhooks Register at `POST /v1/webhooks` with `{ url, events: [...] }`. The signing `secret` is returned once at creation. Max 5 webhooks per user. Events: - `session.completed` — fires once, after a session finishes processing and the summary, notes, and tags are queryable. This is the "session is ready to use" event. Subscribe to this one. - `session.updated` — session metadata was changed (title, summary, notes, tags, favorite). Fires for edits from any source: the API, mobile app, or web app. - `session.action_items.updated` — structured action items were written or edited; includes the new list and version. - `session.deleted` — session was deleted (soft-delete or hard-delete of a completed session). Delivery payload: ```json { "id": "evt_01HX4R7MT8PQZ6VCN9ABYF2KJM", "event": "session.completed", "created_at": "2025-04-21T17:14:08.120Z", "data": { "session": { "id": "sess_…", "title": "…", "summary": "…", "…": "…" } } } ``` Headers for verification: - `X-Wave-Webhook-Id` — unique event ID - `X-Wave-Webhook-Timestamp` — unix timestamp - `X-Wave-Webhook-Signature` — HMAC-SHA256 over `"${id}.${timestamp}.${body}"` using the webhook secret Retries: failed deliveries are retried with exponential backoff for up to 24 hours by an internal cron. --- ## Quick-start (Python stdlib) ```python import os, json, urllib.request TOKEN = os.environ["WAVE_API_KEY"] BASE = "https://api.wave.co/v1" def wave(path, method="GET", body=None): req = urllib.request.Request( f"{BASE}{path}", method=method, data=json.dumps(body).encode() if body else None, headers={ "Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json", }, ) with urllib.request.urlopen(req) as r: return json.loads(r.read()) # List the 20 most recent sessions sessions = wave("/sessions")["sessions"] # Pull transcript + summary for the first one sid = sessions[0]["id"] detail = wave(f"/sessions/{sid}") transcript = wave(f"/sessions/{sid}/transcript") # Semantic search hits = wave("/sessions/search", "POST", {"query": "retail media strategy"}) # Bulk backfill (transcripts in one request) bulk = wave("/sessions/bulk", "POST", { "session_ids": [s["id"] for s in sessions[:20]], "include_transcript": True, "include_summary": True, }) ``` --- ## Integration notes for multi-context accounts If a single Wave account mixes work and personal sessions, the recommended default-deny pattern is: 1. Ask the user to create a folder (in the Wave app) for the scope your agent should see — typically "work". 2. Ask them to drag qualifying sessions into that folder. (Folders already exist as a first-class concept in the Wave iOS, Android, macOS, and web clients.) 3. In the agent, always pass `?folder=work` to `GET /v1/sessions` and any listing flow. Any session not in the folder is out-of-scope by construction. 4. For `POST /v1/sessions/search` and `POST /v1/sessions/bulk` (which don't accept `folder`), first call `GET /v1/folders`, cache the target folder's membership (or cross-reference against the sessions you've already listed), and filter results client-side. This gives you cheap filtered listing in one round-trip and keeps work/personal separation auditable. Exact tag filters are also available on session listing and semantic search when a folder is not the right boundary. --- ## When to use the REST API vs the MCP server Use **api.wave.co** (this surface) when: - You're building a server-side integration, batch job, cron, or webhook consumer. - You need to **write back** to a session (PATCH title/notes/tags/favorite, DELETE). - You need **webhooks** for session.completed / session.updated / session.deleted. - You need bulk export of many transcripts in one request. - You need stable, versioned, OpenAPI-described endpoints. Use **mcp.wave.co** when: - You're inside Claude Desktop, ChatGPT, Cursor, or another MCP client and want the user's recordings ambient as tools. - You want OAuth + PKCE handled for you. - You want semantic search, transcripts, and session context exposed as native tools. - You want focused write tools for action items and folder organization. MCP writes are deliberately narrower than the REST surface: no session deletion and no recording or transcript edits. Both surfaces use the same underlying Wave account data. Pick by integration shape. --- Contact: https://wave.co · legal@wave.co Status: production Brand: see https://wave.co/llms.txt for product overview and positioning