# EchoWave — full agent reference

> EchoWave is an online video editor and media toolkit that is fully operable by AI agents. This document is the complete LLM-readable reference for the EchoWave API and MCP server.

## Endpoints & discovery

- Site: `https://echowave.io`
- Web app / editor: `https://echowave.io/app`
- REST API base: `https://echowave.io/api/v1`
- MCP server (Streamable HTTP): `https://echowave.io/mcp`
- OpenAPI 3.1: `https://echowave.io/openapi.json`
- API catalog (RFC 9727): `https://echowave.io/.well-known/api-catalog`
- MCP server card: `https://echowave.io/.well-known/mcp/server-card.json`
- Agent skills: `https://echowave.io/.well-known/agent-skills/index.json`
- OAuth 2.1 / OIDC: `https://echowave.io/.well-known/oauth-authorization-server`, `…/openid-configuration`, `…/oauth-protected-resource`, JWKS at `…/jwks.json`
- Health: `https://echowave.io/api/v1/health`

## Authentication

Three options, in order of preference:

1. **OAuth 2.1 (authorization code + PKCE)** — for agents acting on a user's behalf.
   - Discover endpoints at `/.well-known/oauth-authorization-server`.
   - Authorize: `GET https://echowave.io/api/auth/oauth2/authorize?response_type=code&client_id=<id>&redirect_uri=<uri>&scope=api:read%20api:write&code_challenge=<S256>&code_challenge_method=S256&state=<state>`
   - Token: `POST https://echowave.io/api/auth/oauth2/token` (`grant_type=authorization_code`, `code`, `redirect_uri`, `code_verifier`, `client_id`). Returns a JWT access token (alg EdDSA) + refresh token.
   - Send `Authorization: Bearer <access_token>`. Tokens are scoped to resource `https://echowave.io/api/v1` and the MCP server `https://echowave.io/mcp`.
2. **API key** — for server-to-server. Create one in account settings (`/app/settings`). Send `X-API-Key: <key>` (or as a bearer token).
3. **Firebase ID token** — first-party clients may send a Firebase ID token as `Authorization: Bearer <idToken>`.

Scopes: `api:read` (read), `api:write` (create/edit/render).

## Conventions

- All responses are JSON `{ "data": ... }`; list endpoints add `{ "data": [...], "pagination": { page, limit, total, totalPages } }`.
- Errors: `{ "error": "message", "code": "string", "details"?: any }` with standard HTTP status (400 validation, 401 unauthorized, 403 forbidden, 404 not found, 429 rate limited, 500 server, 503 service unavailable).
- Times are ISO‑8601; timeline positions (`startTime`, `endTime`) are milliseconds.

## REST endpoints

### Account
- `GET /account` — profile, plan, status, credits, counts, `planSource` (`"personal"`|`"team"`), `workspaces` (see Teams & workspaces).

### Projects
- `GET /projects?page&limit&workspaceId` — list. Project summaries include `workspaceId` (`null` for personal projects).
- `POST /projects` — create. Body: `{ title?, size?: { ratio: "16:9"|"9:16"|"1:1"|"4:5"|"custom", custom?: {width,height} }, fps?=30, backgroundColor?, workspaceId? }`.
- `GET /projects/{id}` — full project incl. `segments` map.
- `PATCH /projects/{id}` — update `{ title?, size?, fps?, backgroundColor?, duration? }`.
- `DELETE /projects/{id}`.

### Segments (the timeline)
A project's `segments` is a map keyed by segment id. Common fields: `type` (required), `startTime`, `endTime` (ms), `track`, `zIndex`, `opacity`, `rotation`, `position{x,y}`, `scale{x,y}`, `width`, `height`.
Types & extra fields:
- `text` — `text`, `font{family,files}`, `fontSize`, `bold`, `italic`, `color`, `align`, `strokeWidth`, `shadowBlur`.
- `video` / `audio` / `image` — `fileId` (an upload id), `cut`, `speed`, `volume`, `muted`.
- `subtitles` — `parentId`, `fileId`, `fontSize`, `color`, `align`, `verticalAlign`, `caption{presetId}`, `subtitles{segments:[{id,start,end,text}]}`.
- `wave` — `wave` (style id), `color`, `bars`, `corners`.
- `progress_bar` — `barType`, `color`.
- `shape` — `shapeType`, `color`.

Endpoints:
- `POST /projects/{id}/segments` — add (body = a Segment). Returns the created segment with its id.
- `PATCH /projects/{id}/segments/{segmentId}` — update.
- `DELETE /projects/{id}/segments/{segmentId}` — remove.

### Media
- `POST /media` — import by URL: `{ url, type: "video"|"audio"|"image", projectId?, workspaceId? }` → `{ uploadId, source, type }`.
- `POST /media/upload` — multipart `file` (and optional `projectId`, `workspaceId` form fields).

### AI voice-over (TTS) & voices
- `GET /voices` — `{ presets:[...], clones:[...] }`.
- `POST /tts` — `{ projectId, text (≤5000), voiceId, speed?=1, instruct?, model? }` → `{ uploadId, source, url, durationMs, cost, voiceId }`. Then `add_segment` type=audio with that `uploadId`.
- `POST /voices/clones` — `{ name, storagePath, sampleScript?, language? }`.
- `DELETE /voices/clones/{id}`.

### Subtitles
- `POST /subtitles/transcribe` — `{ uploadId }` → `{ jobId }`. Cues land on the upload asynchronously.
- `POST /subtitles/translate` — `{ uploadId, targetLanguage }` → `{ jobId, alreadyExists }`.

### Renders
- `POST /renders` — `{ projectId }` → `202 { renderId, projectId, status: "queued" }`.
- `GET /renders?page&limit&projectId&workspaceId` — list.
- `GET /renders/{id}` — `{ renderId, projectId, status: "queued"|"rendering"|"completed"|"failed", progress, outputUrl, thumbnailUrl, watermark, durationMs, errorMessage, createdAt, completedAt }`.
- `POST /renders/{id}/download` — `{ downloadUrl, filename }`.

Render lifecycle: `queued → rendering → completed | failed`. Poll `GET /renders/{id}` (or supply a webhook). `outputUrl` is present when `completed`. Renders on free accounts carry a watermark.

### AI Clips
Turn one long video (podcast, interview, webinar, meeting, sermon, lecture, stream, vlog, talk) into ranked short clips: transcribes, watches (faces, active speaker, scene cuts, screen share), scores each moment with a Viral Score (0–100; sub-scores hook/clarity/emotion/value/trend + reasons), frames each clip with a speaker-aware camera (follow, stack/split, grid, presenter+slides, picture-in-picture, fill, letterbox-over-blur), adds captions, an AI title/hook, hashtags and a description. Free accounts get 30 source-minutes of analysis per month; higher tiers get more (402 `CLIP_LIMIT_REACHED` once over budget).
- `POST /clips` — `{ uploadId | projectId, title?, workspaceId?, settings?: { genre?, count? (1-30 or "auto"), length?: { preset, minMs, maxMs }, ratio? ("9:16"|"1:1"|"4:5"|"16:9"), layoutMode?, prefs?, captions?, prompt?, keywords?, autoMaterialize?, autoRender? } }` → `202 { jobId, renderNodeJobId }`. Starts a background job.
- `GET /clips?page&limit&status&workspaceId` — list the caller's clip jobs, newest first.
- `GET /clips/{id}` — `{ id, title, status: "queued"|"transcribing"|"analyzing"|"finding"|"framing"|"ready"|"error"|"cancelled", progress, settings, summary, candidates: [{ id, start, end, durationMs, title, hook, hashtags, score, scores:{hook,clarity,emotion,value,trend}, excerpt, layoutMode, status }], error?, createdAt, updatedAt }`. Poll until `status` is `"ready"` or `"error"`.
- `POST /clips/{id}/projects` — `{ candidateIds: [...], render?, overrides? }` → `202 { projects: [{ candidateId, projectId, renderId? }] }`. Turns candidates into real EchoWave projects; `render:true` also queues a render for each (poll `GET /renders/{id}`). Materializing is free; a render from a free account still completes, just watermarked (same rule as `POST /renders`) — this endpoint never blocks on entitlement.
- `POST /clips/{id}/search` — `{ prompt }` → `{ candidates: [...] }`. Re-searches the job's stored transcript for moments matching `prompt`, without re-running full analysis.
- `POST /clips/{id}/cancel` — requests cancellation; `DELETE /clips/{id}` — deletes the job and its analysis artifacts (materialized projects are untouched).

### Templates
Templates are reusable projects with fillable placeholders — ideal for automated, personalised video. Placeholder `type` is `text`, `image`, `video` or `audio`: text placeholders take plain strings; image/video/audio placeholders take public https URLs. ALL placeholders are optional at render time — omitted keys keep the template's default content.
- `GET /templates` (optional `?workspaceId=`) — list templates, personal plus every team workspace the caller belongs to → `{ templates: [{ id, title, preview, placeholderCount, placeholders, workspaceId, workspaceName, owned, updatedAt }] }`.
- `GET /templates/{id}/placeholders` — `{ templateId, title, placeholders: [{ key, label, hint, type, segmentId, required, defaultText? }] }`.
- `POST /templates/{id}/renders` — `{ placeholders?: { <key>: <value> }, title?, callbackUrl? }` → `202 { renderId, watermark, skippedSegments?, status: "queued" }`. Poll `GET /renders/{id}`.
- `POST /templates/{id}/batches` — `{ rows: [{ placeholders, externalId?, callbackUrl? }], title? }` (1–100 rows; `externalId` is your row id, echoed back in statuses) → `202 { batchId, total, renders: [{ index, externalId, renderId }], skipped?, watermark }`.
- `GET /batches/{id}` — `{ batchId, templateId, total, createdCount, skippedCount, counts: { queued, inProgress, done, error, expired }, renders: [...], createdAt }`. Batches are kept 30 days.

### Media tools (one-shot)
- `POST /tools/{tool}` — `tool` ∈ `trim|crop|compress|convert|rotate|flip|speed|reverse|loop|mute|split`. Body `{ src (url), name?, options }` (options vary by tool, e.g. convert: `{ targetFormat, videoCodec?, audioCodec? }`; trim: `{ segments:[{start,end}] }`; compress: `{ quality }`). Returns `202 { renderId }`; poll `GET /renders/{id}`.

### Stock
- `GET /stock/photos?query&perPage` — search the web for images (direct image URLs usable as media; results may be copyrighted). Single page per query.

### Status
- `GET /api/v1/health` — `{ status, timestamp, checks }`. No auth.

## Teams & workspaces

Content is workspace-scoped; workspace ids come from `GET /account`.

- `GET /account` includes `planSource` (`"personal"` | `"team"`) and `workspaces: [{ id, name, role, plan, entitled }]` — the caller's team workspaces.
- `GET /projects`, `GET /renders` and `GET /templates` accept `?workspaceId=` to list a team workspace's content (teammates' included). Membership is required — otherwise `403 WORKSPACE_FORBIDDEN`.
- `POST /projects` and `POST /media` accept an optional `workspaceId` body field (multipart `/media/upload`: a `workspaceId` form field). Editor role required; content created in a workspace is visible to the whole team.
- Membership lift: a paid team seat activates paid features for that member everywhere — `plan`/`status` in `GET /account` reflect the team seat (`planSource: "team"`) and renders are watermark-free.

## MCP server

Connect an MCP client to `https://echowave.io/mcp` (Streamable HTTP). Authenticate via OAuth (the server advertises `WWW-Authenticate` → `/.well-known/oauth-protected-resource`) or an API key. Tools mirror the REST surface: `get_account`, `list_projects`, `create_project`, `get_project`, `update_project`, `delete_project`, `add_segment`, `update_segment`, `delete_segment`, `import_media`, `list_voices`, `generate_speech`, `create_voice_clone`, `transcribe_subtitles`, `translate_subtitles`, `queue_render`, `get_render`, `list_renders`, `get_render_download`, `generate_clips`, `get_clip_job`, `list_clip_jobs`, `materialize_clips`, `search_clip_moments`, `list_templates`, `get_template_placeholders`, `render_from_template`, `render_batch_from_template`, `get_batch_status`, `search_stock_photos`, `run_tool`.

## Example: caption a video end-to-end

1. `create_project` → `{ id }`
2. `import_media { url, type:"video" }` → `{ uploadId }`
3. `add_segment { type:"video", fileId:uploadId, startTime:0, endTime:<ms> }`
4. `transcribe_subtitles { uploadId }`
5. `add_segment { type:"subtitles", parentId:<videoSegId>, fileId:uploadId, ... }`
6. `queue_render { projectId:id }` → `{ renderId }`
7. poll `get_render { renderId }` until `status:"completed"`, then `get_render_download`.

## Example: personalised videos from a template (batch)

1. `list_templates` → pick a template `{ id }`
2. `get_template_placeholders { templateId:id }` → e.g. `[{ key:"headline", type:"text" }, { key:"hero_image", type:"image" }]`
3. `render_batch_from_template { templateId:id, rows:[ { placeholders:{ headline:"Hi Ana!", hero_image:"https://…/ana.jpg" }, externalId:"cust-001" }, … ] }` → `{ batchId }` (up to 100 rows)
4. poll `get_batch_status { batchId }` until `counts.done == total`, then collect each render's output (`get_render` / `get_render_download`).

## Example: make clips end-to-end

1. `import_media { url:"https://…/episode-42.mp4", type:"video" }` → `{ uploadId }` (or reuse an existing `uploadId`/`projectId`).
2. `generate_clips { uploadId, count:8, ratio:"9:16", genre:"podcast", prompt:"moments about pricing" }` → `202 { jobId }`. Analysis runs in the background.
3. poll `get_clip_job { id:jobId }` every few seconds until `status:"ready"` (or `"error"`) — then read `candidates`, each with a `score` (Viral Score 0–100), `title`, `hook` and `start`/`end`.
4. pick the candidates worth keeping (e.g. `score >= 75`) and call `materialize_clips { clipJobId:jobId, candidateIds:[...], render:true }` → `202 { projects:[{ candidateId, projectId, renderId }] }`. This both opens each clip as a real EchoWave project AND queues a render.
5. poll `get_render { id:renderId }` for each until `status:"completed"`, then `get_render_download` for the MP4. (Skip `render:true` in step 4 and call `queue_render { projectId }` later if you'd rather review the projects in the editor first.)

Optional: instead of (or in addition to) the AI's own picks, call `search_clip_moments { clipJobId:jobId, prompt:"moments about our refund policy" }` to re-search the same transcript for a different angle — it returns fresh `candidates` without re-running full analysis.
