# Second Studio Second Studio is a video production backend for AI agents. Give it a video whose editing you like and your own raw clips, and it returns a finished, captioned, beat-synced edit in that style — then posts it to your social accounts when you approve. Everything works through one API key, over a REST API or the Model Context Protocol (MCP), so Claude, Cursor or any agent can drive it. ## What it does | Capability | What you get | |---|---| | **Style analysis** | A reusable *style* from any reference video: shot lengths, pacing, where cuts land on the beat, each shot's job (hook, demo, payoff…), caption look, hook technique. | | **Style replication** | Your own clips cut to that style: matched to the reference's shots, cuts snapped to your music, captions in the reference's look, rendered and quality-checked. | | **Generation** | Character sheets, characters, locations, props, scenes, video shots and voiceover — each directed by the studio's production skills, so you write a brief, not a prompt. | | **Swaps and edits** | Put a stored character into an existing clip (object swap, motion transfer) or into a real photo (masked edit), keeping everything else as shot. | | **Explainer videos** | Save a brand kit (logo, colors, font, product screenshots), get an editable scene-by-scene script from a brief, render a branded explainer in 16:9, 9:16 or 1:1 — with captions, narration and music. See [Explainer videos](explainers.html). | | **Product recording** | Real recordings of a public web app, driven by simple steps (click, type, scroll), with a visible cursor. | | **Video fetching** | Bring a reference in from a social URL — on your own machine through the local MCP server, or on ours where enabled. | | **Team** | Several people on one account, each with their own key and role (admin, creator, reviewer); a shared library of brand kits, characters, styles and videos; per-person credit caps. See [Team](team.html). | | **Posting** | Publish or schedule finished videos to TikTok, Instagram, YouTube, LinkedIn, Facebook, X and more, through accounts you connect yourself. Drafts wait for human approval. | ## How it works ``` reference video ──► analyze_style ──► style (numbers + descriptions, no pixels) │ your raw clips + music ──────────────► create_edit ──► finished mp4 (QA-checked) │ create_post ──► approve ──► social accounts ``` - **Measured, then interpreted.** Cut points, shot lengths, motion, tempo and loudness are measured from the video itself. A vision model then labels what each shot is *for* and describes the caption style. A model never decides where a cut goes; it only describes cuts that were measured. - **Only your footage in your edit.** A style keeps numbers and descriptions from the reference — never its pixels or audio. The finished video contains only the clips and music you uploaded. - **Real interfaces only.** Recordings are of the real product. Second Studio never generates or fakes product UI. - **Every render is checked** for the wrong length, black or frozen stretches, and missing or clipped audio before it is returned. ## Start here 1. [Quickstart](quickstart.html) — get a key and make your first edit in five minutes. 2. [Connect an agent (MCP)](mcp.html) — one line for Claude Code, Cursor or any MCP client. 3. [REST API reference](api.html) — every endpoint, request and response. --- # Quickstart ## 1. Get your keys **[Request access](/#access)**. Every account is approved by a person. Once yours is, you receive two API keys and a starting credit balance: - **Agent key** — give this to your agents. It can do everything except release posts. - **Admin key** — keep this with the person who approves posts. Store both somewhere safe, like a password manager. Only a hash of each key is stored, so a lost key cannot be recovered — ask for a new one and the old one is revoked. ## 2. Connect your agent The fastest way is the remote MCP server — no install. In Claude Code: ```bash claude mcp add --transport http second-studio https://frugal-anteater-681.convex.site/mcp --header "Authorization: Bearer YOUR_AGENT_KEY" ``` Other clients: see [Connect an agent](mcp.html). Then just ask your agent, for example: > Analyze the editing style of reference.mp4, then cut my clips in ./footage to that style with > track.mp3 as the music, and show me the result. ## 3. Or do it yourself with curl Every step below is what the agent does for you. ```bash export SS=https://frugal-anteater-681.convex.site export KEY=ss_live_... # your agent key # Upload a file: get a one-time URL, send the bytes, register the result. upload() { url=$(curl -s -X POST "$SS/v1/assets/upload-url" -H "Authorization: Bearer $KEY" | jq -r .uploadUrl) sid=$(curl -s -X POST "$url" -H "Content-Type: $2" --data-binary @"$1" | jq -r .storageId) curl -s -X POST "$SS/v1/assets" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \ -d "{\"storageId\":\"$sid\",\"filename\":\"$(basename "$1")\"}" | jq -r .id } REF=$(upload reference.mp4 video/mp4) CLIP1=$(upload clip1.mp4 video/mp4) CLIP2=$(upload clip2.mov video/quicktime) MUSIC=$(upload track.mp3 audio/mpeg) # Analyze the reference's style (2 credits). Jobs are asynchronous: poll until done. JOB=$(curl -s -X POST "$SS/v1/styles" -H "Authorization: Bearer $KEY" \ -H "Content-Type: application/json" -d "{\"assetId\":\"$REF\"}" | jq -r .id) until [ "$(curl -s "$SS/v1/jobs/$JOB" -H "Authorization: Bearer $KEY" | jq -r .status)" = succeeded ]; do sleep 3; done STYLE=$(curl -s "$SS/v1/jobs/$JOB" -H "Authorization: Bearer $KEY" | jq -r .result.styleId) # Cut your clips to that style (5 credits). JOB=$(curl -s -X POST "$SS/v1/edits" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d "{ \"styleId\": \"$STYLE\", \"clipAssetIds\": [\"$CLIP1\", \"$CLIP2\"], \"musicAssetId\": \"$MUSIC\", \"captions\": [\"The hook goes first\", \"Then the proof\", \"Then the payoff\"] }" | jq -r .id) until [ "$(curl -s "$SS/v1/jobs/$JOB" -H "Authorization: Bearer $KEY" | jq -r .status)" = succeeded ]; do sleep 5; done curl -s "$SS/v1/jobs/$JOB" -H "Authorization: Bearer $KEY" | jq -r '.outputs[] | select(.role=="edit") | .url' ``` A failed job reports `status: "failed"` with an `error`, and its credits are refunded automatically. ## Next - [Styles and edits](styles.html) — what a style contains and every edit option. - [Posting](social.html) — connect social accounts and publish with approval. - [Credits](billing.html) — what things cost. --- # Connect an agent (MCP) Second Studio speaks the [Model Context Protocol](https://modelcontextprotocol.io), so any MCP-capable agent can use it with your API key. There are two servers; most people want the remote one. | | Remote server | Local server | |---|---|---| | Install | Nothing — a URL and a header | `npx second-studio-mcp` (Node 20+) | | Uploads | Agent sends files to a one-time upload URL | Agent passes a local file path | | Fetching social videos | On our servers, where enabled | On your machine with `yt-dlp`, if installed | | Waiting for jobs | Agent polls `get_job` | Tools wait for completion by default | Use the **agent key** in agent configuration. It cannot approve posts, so an agent can never release its own drafts. ## Remote server URL: `https://frugal-anteater-681.convex.site/mcp` · Transport: Streamable HTTP · Auth: `Authorization: Bearer ` **Claude Code** ```bash claude mcp add --transport http second-studio https://frugal-anteater-681.convex.site/mcp --header "Authorization: Bearer YOUR_AGENT_KEY" ``` **Cursor, Windsurf and other clients with a JSON config** ```json { "mcpServers": { "second-studio": { "type": "http", "url": "https://frugal-anteater-681.convex.site/mcp", "headers": { "Authorization": "Bearer YOUR_AGENT_KEY" } } } } ``` **Claude Desktop** (via the local bridge, since it reads `claude_desktop_config.json`): ```json { "mcpServers": { "second-studio": { "command": "npx", "args": ["-y", "second-studio-mcp"], "env": { "SECOND_STUDIO_API_KEY": "YOUR_AGENT_KEY", "SECOND_STUDIO_URL": "https://frugal-anteater-681.convex.site" } } } } ``` ## Local server Runs on your machine and talks to the same API. Use it when the agent works with files on your disk. ```bash SECOND_STUDIO_API_KEY=YOUR_AGENT_KEY SECOND_STUDIO_URL=https://frugal-anteater-681.convex.site npx -y second-studio-mcp ``` ```bash claude mcp add second-studio --env SECOND_STUDIO_API_KEY=YOUR_AGENT_KEY \ --env SECOND_STUDIO_URL=https://frugal-anteater-681.convex.site -- npx -y second-studio-mcp ``` Optional: `SECOND_STUDIO_MAX_WAIT` — seconds a tool waits for a job before returning it as still running (default 900). Install [yt-dlp](https://github.com/yt-dlp/yt-dlp) to let `fetch_video` download references on your own machine. ## Tools | Tool | What it does | Credits | |---|---|---| | `whoami` | Account, key scope and settings | — | | `get_credits` | Balance, prices and packs | — | | `upload_file` *(local)* | Upload a file from disk | — | | `create_upload_url` + `register_asset` *(remote)* | Upload in two steps | — | | `get_asset`, `list_assets`, `delete_asset` | Manage files | — | | `fetch_video` | A reference from a social URL | 1 if fetched on our servers | | `analyze_style` | Reference video → reusable style | 2 | | `get_style`, `list_styles` | Read styles, including the full spec | — | | `create_edit` | Your clips cut to a style | 5 | | `record_url` | Record a public web app | 3 | | `create_character`, `get_character`, `list_characters`, `delete_character` | Stored identities you can reuse by id | like one image | | `create_brand`, `get_brand`, `list_brands`, `delete_brand` | Brand kits for explainer videos | — | | `write_script` | An editable scene-by-scene explainer script from a brief | 2 | | `create_explainer` | A rendered, branded explainer video from a brief or script | 12 | | `generate_image` | Character sheets, characters, locations, props, scenes, products | per image | | `generate_video` | Video shots, from text, an image or references (+ a voice track) | quoted per request | | `generate_voice` | Performed voiceover | 1 per 500 chars | | `lipsync` | Re-time a mouth to an audio track | 15 | | `get_job`, `list_jobs` | Job status and outputs | — | | `list_social_accounts` | Connected accounts | — | | `connect_social_account` | A link to connect a new account | — | | `create_post`, `list_posts` | Draft and track posts | 1 per post | | `list_models` | Image, video and swap models and what each supports | — | | `create_skill`, `list_skills`, `get_skill`, `update_skill`, `delete_skill` | Your own versioned image recipes | — | | `edit_image` | Masked edit of a real photo, rest untouched | per image | | `swap_character`, `motion_transfer` | Keep the footage, change the subject | quoted | | `finalize_video`, `cancel_job`, `upscale_video` | Drafts → keeper, cancel with refund, 2160p resample | quoted / — / 3 | | `library` | Everything the team can reuse by id, with who made it | — | | `list_members`, `invite_member`, `update_member`, `remove_member`, `create_member_key`, `list_keys`, `revoke_key` | Team members, roles, keys, credit and dollar caps, claim links (admin) | — | | `list_providers`, `connect_provider`, `disconnect_provider` | Your own Higgsfield / ElevenLabs keys (admin, where enabled) | — | | `list_voices`, `clone_voice`, `delete_voice` | Your workspace's voices and clones from your samples | like voice | | `analyze_brand` | A suggested brand kit from your boards | 1 | | `report_issue`, `list_reports` | File a bug, feature request or question; follow the response | — | Approving posts is deliberately not a tool. A person approves with the admin key — see [Posting](social.html). ## Example prompts - *"Analyze ./refs/viral.mp4 and tell me what makes its editing work."* - *"Make three 15-second edits of ./footage in the style of that reference, with different hooks."* - *"Record our pricing page at https://example.com/pricing on mobile, then cut it to my saved style."* - *"Draft the best one to our TikTok for tomorrow 6pm Eastern."* --- # REST API reference Base URL: `https://frugal-anteater-681.convex.site` · All bodies are JSON · Authenticate every `/v1` request with ``` Authorization: Bearer ss_live_... ``` ## Conventions - **Jobs are asynchronous.** Endpoints that do work (`/v1/styles`, `/v1/edits`, `/v1/recordings`, `/v1/fetch`) return `202` with a [job](#jobs) immediately. Poll `GET /v1/jobs/{id}` every few seconds. - **Credits** are charged when a job is accepted and refunded automatically if it fails. See [Credits](billing.html). - **Getting a key:** [request access](/#access). Keys work only after a person approves the account; an unapproved key gets `403 key_pending_approval`. - **Errors** have one shape: ```json { "error": { "code": "insufficient_credits", "message": "this costs 5 credit(s); balance is 2. …" } } ``` | Status | Codes | |---|---| | 400 | `missing_field`, `invalid_field`, `invalid_json`, `invalid_id`, `invalid_url`, `invalid_pack`, `unknown_account`, `platform_mismatch` | | 401 | `unauthorized` — missing, invalid or revoked key | | 402 | `insufficient_credits` | | 403 | `key_pending_approval`, `forbidden` (needs an admin key), `fetch_disabled` | | 404 | `not_found` | | 409 | `conflict`, `not_pending` | | 429 | `rate_limited`, `too_many_jobs` | | 502 / 503 | `zernio_error`, `not_configured` | ## Account ### `GET /v1/me` ```json { "orgId": "js7…", "keyScope": "agent", "name": "Acme", "credits": 93, "autoPublish": false, "allowHostedFetch": false, "maxConcurrentJobs": 4, "apiVersion": "2026-10-08" } `apiVersion` changes whenever the endpoint or MCP tool surface changes; MCP clients that cache the tool list should reconnect when it does. ``` ## Assets An asset is any file in your account: an uploaded clip or song, a reference, a finished edit, a recording. Uploading takes three calls, so large files go straight to storage instead of through the API. ### `POST /v1/assets/upload-url` Returns a one-time URL: `{ "uploadUrl": "https://…" }`. Send the raw bytes to it: ```bash curl -X POST "$uploadUrl" -H "Content-Type: video/mp4" --data-binary @clip.mp4 # → { "storageId": "kg2…" } ``` ### `POST /v1/assets` Register the upload: `{ "storageId": "kg2…", "filename": "clip.mp4" }` → `201` with the asset: ```json { "id": "j97…", "kind": "video", "role": "upload", "filename": "clip.mp4", "contentType": "video/mp4", "size": 991017, "url": "https://…", "source": null, "jobId": null, "createdAt": 1791328110667 } ``` `kind` is `video`, `audio`, `image`, `json` or `other`, from the uploaded Content-Type. ### `GET /v1/assets` · `GET /v1/assets/{id}` · `DELETE /v1/assets/{id}` List the 100 most recent, read one (with a download `url`), or delete one and its file permanently. ## Styles ### `POST /v1/styles` — analyze a reference (2 credits) ```json { "assetId": "j97…", "semantic": true } ``` `semantic` (default `true`) adds vision-model shot roles, descriptions and caption style on top of the measured analysis. When the job succeeds, `result.styleId` is the new style and `result` summarizes it: ```json { "styleId": "jn7…", "aspect": "9:16", "duration": 17.8, "shot_count": 7, "bpm": 99.4, "cuts_on_beat_ratio": 0.71, "semantic": true, "summary": "Fast result-first hook, …", "pacing": { "shot_count": 7, "mean_shot": 2.54, "median_shot": 0.73, "cuts_per_minute": 20.3, … } } ``` ### `GET /v1/styles` · `GET /v1/styles/{id}` `GET /v1/styles/{id}` includes the full `spec` — see [Styles and edits](styles.html#the-style-spec). ## Edits ### `POST /v1/edits` — cut your clips to a style (5 credits) | Field | Type | | |---|---|---| | `styleId` | string | **Required.** | | `clipAssetIds` | string[] | **Required.** 1–30 of your video assets. | | `musicAssetId` | string | An audio asset. Cuts snap to its beat when the style is beat-driven. | | `musicStart` | number | Seconds into the track to start (default 0). | | `captions` | string[] | Caption lines in order (up to 40, 200 chars each). The first is the hook. | | `brief` | string | What the video is about. Used to write captions when `captions` is omitted. | | `aspect` | string | `9:16`, `4:5`, `1:1`, `4:3` or `16:9`. Defaults to the reference's. | | `duration` | number | Target length, 3–180 s. Defaults to the reference's. | | `keepClipAudio` | boolean | Mix your clips' own sound. Default: on without music, off (ducked) with music. | | `semantic` | boolean | Use the vision model to pick the best moments (default `true`). | The finished job's `outputs` contain the `edit` (mp4) and the `plan` (JSON: which clip and in-point filled each shot). `result.qa` is the automatic quality report; `result.notes` explains decisions such as beat snapping and layout. ## Generation See [Generating images, characters and video](generation.html) for every option. | Endpoint | | |---|---| | `POST /v1/characters` | `{ name, description, referenceAssetIds?, aspect?, resolution? }` → a stored character whose sheet is generated; poll until `ready` | | `GET /v1/characters` · `GET /v1/characters/{id}` · `DELETE /v1/characters/{id}` | List, read (with `sheetUrl`), delete | | `POST /v1/images` | `{ kind, brief, characterIds?, references?, aspect?, resolution?, n?, estimate? }` → job; outputs are images | | `POST /v1/videos` | `{ brief, characterIds?, duration?, resolution?, aspect?, imageAssetId?, referenceAssetIds?, referenceVideoAssetIds?, referenceAudioAssetIds?, estimate? }` → job, or a price with `estimate: true` | | `POST /v1/voice` | `{ text, voice?, direction?, stability?, perform? }` → job; output is audio | | `POST /v1/lipsync` | `{ videoAssetId, audioAssetId, pro? }` → job | Generation results include `notes` on choices made. The production craft behind them stays server-side. ## Models, skills, swaps, edits | | | |---|---| | `GET /v1/models` | The image, video and swap models you can pick, with what each supports. | | `POST /v1/skills` · `GET /v1/skills` · `GET/POST/DELETE /v1/skills/{id}` | Your own versioned image recipes; `skillIds` on images, characters, edits and brand kits. | | `POST /v1/images/edit` | Masked edit of a real photo: only the mask is painted, the rest is untouched. | | `POST /v1/swaps` | `object_swap` / `motion_transfer`: keep the footage, change the subject. | | `POST /v1/videos/{id}/finalize` · `draft: true` | Cheap 480p drafts, then the keeper at 720p/1080p. | | `POST /v1/jobs/{id}/cancel` | Cancel with a full refund. | | `POST /v1/upscales` | Lanczos resample to 2160p. | | `POST /v1/dialogue` · `GET /v1/voices` · `POST /v1/voices/clone` | Multi-speaker tracks with per-line timings; your own ElevenLabs voices and clones when your key is connected. | | `GET /v1/providers` · `POST/DELETE /v1/providers/{higgsfield\|elevenlabs}` | Connect your own provider keys (admin, where enabled). | | `POST /v1/brands/analyze` | A suggested brand kit from your boards. | See [Generation](generation.html). ## Explainer videos Brand kits, scripts and renders have their own page: [Explainer videos](explainers.html). | | | |---|---| | `POST /v1/brands` · `GET /v1/brands` · `GET /v1/brands/{id}` · `POST /v1/brands/{id}` · `DELETE /v1/brands/{id}` | Brand kits: name, description, tone, colors, font, logo, product images with descriptions, music. Free. | | `POST /v1/scripts` | `{ brandId, brief, duration?, aspect?, language? }` → job; `result.script` is an editable scene list. 2 credits. | | `POST /v1/explainers` | `{ brandId, brief | script, aspect?, captions?, voiceover?, musicAssetId? }` → job; the video plus the rendered script. 12 credits. | ## Recordings ### `POST /v1/recordings` — record a public web app (3 credits) ```json { "url": "https://example.com", "viewport": "mobile", "steps": [ { "type": "hold", "seconds": 2 }, { "type": "click", "selector": "text=Pricing" }, { "type": "scroll", "pixels": 900 }, { "type": "hold", "seconds": 2 } ] } ``` See [Recording](recording.html) for every step type. Output: a `recording` asset (mp4) and `result.marks`, the timestamp of every step for cutting. ## Fetching ### `POST /v1/fetch` — a reference from a social URL (1 credit) `{ "url": "https://www.tiktok.com/@…/video/…", "start": 3, "end": 18 }`. `start`/`end` (seconds, optional) keep only that section — handy for a 4–30 s reference clip for `POST /v1/videos`. Hosted fetching is a per-account setting: if it is off you get `403 fetch_disabled` — ask us to enable it (`POST /v1/reports` works) and confirm you have the rights to the clips, or fetch on your own machine with the local MCP server and upload instead. Output: a `fetched` asset with `source` attribution (and the `trim` applied). ## Jobs ### `GET /v1/jobs/{id}` · `GET /v1/jobs` ```json { "id": "jd7…", "kind": "replicate", "status": "succeeded", "progress": "done", "error": null, "result": { … }, "creditsCharged": 5, "refunded": false, "attempts": 1, "outputs": [ { "assetId": "j95…", "role": "edit", "kind": "video", "filename": "edit.mp4", "url": "https://…" } ], "createdAt": 1791469255706, "startedAt": 1791469256012, "finishedAt": 1791469281420 } ``` `status`: `queued` → `running` → `succeeded` | `failed`. A job with no word from the worker for three hours is marked failed and refunded. ## Social accounts and posts See [Posting](social.html) for the approval model. | Endpoint | | |---|---| | `GET /v1/social/accounts` | Connected accounts: `{ id, platform, username, displayName, status }` | | `POST /v1/social/connect` | `{ "platform": "tiktok", "redirectUrl": "https://…" }` → `{ "authUrl": "…" }` | | `POST /v1/posts` | Draft a post (1 credit). See below. | | `GET /v1/posts` | Posts and their status | | `POST /v1/posts/{id}/approve` | **Admin key.** Publish or schedule a pending post. | | `POST /v1/posts/{id}/reject` | **Admin key.** Reject it; its credit is refunded. | `POST /v1/posts`: ```json { "assetId": "j95…", "caption": "Dinner in one search #recipes", "targets": [ { "platform": "tiktok", "accountId": "6a62…" } ], "scheduledFor": "2026-10-12T18:00:00-04:00" } ``` Give `scheduledFor` (ISO 8601 with an offset, or with `timezone`) or `"publishNow": true`. Post statuses: `pending_approval` → `approved` → `scheduled` | `published` | `failed`, or `rejected`. ## Team Members, roles, keys, per-person credit caps and the shared library have their own page: [Team](team.html). | | | |---|---| | `GET /v1/members` · `POST /v1/members` · `GET/POST/DELETE /v1/members/{id}` · `POST /v1/members/{id}/keys` | People on the account, each with their own key and role. | | `GET /v1/keys` · `DELETE /v1/keys/{id}` | Every live key and whose it is. | | `GET /v1/library` | Brand kits, characters, styles, finished videos and uploads the team can reuse, with `createdBy`. | ## Reporting problems Found a bug, want a feature, or have a question? File it through the API — agents can do this on their own when something goes wrong. Reports are free. ### `POST /v1/reports` | Field | Type | | |---|---|---| | `title` | string | **Required.** Up to 200 characters. | | `description` | string | **Required.** What happened, what you expected, steps to reproduce. Up to 10,000 characters. | | `kind` | string | `bug` (default), `feature` or `question`. | | `severity` | string | `low`, `normal` (default), `high` or `blocking`. | | `jobId` | string | The job involved. Its status, error, inputs and QA report are attached automatically. | ```json { "kind": "bug", "severity": "high", "jobId": "jd7…", "title": "Captions cut off on 4:5 edits", "description": "The second caption line is clipped at the top. Expected it inside the frame." } ``` Returns `201` with the report. `GET /v1/reports` and `GET /v1/reports/{id}` show each report's `status` — `open`, `acknowledged`, `fixed`, `wont_fix` or `closed` — and the team's `response`. ## Credits | Endpoint | | |---|---| | `GET /v1/billing` | `{ credits, costs, packs }` | | `GET /v1/billing/ledger` | Every charge, refund and purchase, newest first | --- # Styles and edits ## The idea Viral and on-brand short-form video is mostly *structure*: how soon the first cut lands, how long each shot holds, whether cuts hit the beat, what job each shot does, how captions look and move. A **style** captures that structure from a reference video. An **edit** rebuilds it from your own footage. ## What gets measured, and what gets interpreted | Measured from the file (exact) | Interpreted by a vision model (descriptive) | |---|---| | Every hard cut, to the frame | Each shot's role: `hook`, `context`, `demo`, `detail`, `reaction`, `payoff`, `cta`, `broll` | | Shot lengths and pacing (cuts per minute, first cut) | What is on screen, camera movement, speed ramps | | Motion level per shot | Burned-in text, and how captions look: position, case, weight, colour, highlight, words at a time | | Tempo, beat grid, share of cuts that land on a beat | Transition into each shot: cut, dissolve, whip, zoom, flash, slide | | Loudness (LUFS) | Hook duration and technique; colour grade; a written summary | Vision models sample about one frame per second, so they cannot place a cut precisely — they are never asked to. They only describe shots whose boundaries were already measured. With `semantic: false`, or if the model is unavailable, you still get the full measured analysis. ## The style spec `GET /v1/styles/{id}` returns the full spec. Abridged: ```json { "aspect": "9:16", "duration": 17.77, "shots": [ { "index": 0, "start": 0.0, "end": 0.37, "duration": 0.37, "role": "hook", "motion": 1.0, "transition_in": "cut", "speed": 1.0, "camera": "screen-recording", "description": "Finished dish shown first", "on_screen_text": "Dinner in 10 minutes" } ], "pacing": { "shot_count": 7, "median_shot": 0.73, "cuts_per_minute": 20.3, "first_cut": 0.37 }, "music": { "present": true, "bpm": 99.4, "cuts_on_beat_ratio": 0.71, "beats": [0.46, 1.06, …] }, "loudness_lufs": -13.6, "captions": { "present": true, "position": "upper", "case": "sentence", "words_per_chunk": 3, "weight": "black", "color": "#FFFFFF", "highlight_color": "#FFD60A", "outline": true }, "hook": { "duration": 0.37, "technique": "result first" }, "summary": "…", "semantic": true, "model": "google/gemini-3.8-flash" } ``` A style keeps numbers and descriptions only — never the reference's pixels or audio — so you can delete the reference afterwards and keep using the style. ## How an edit is built 1. **Slots.** Each shot in the style becomes a slot with the same length, role, speed and transition, scaled if you ask for a different `duration`. 2. **Beat snap.** If you supply music and the reference cuts on the beat, every cut moves to the nearest beat of *your* track (never more than 0.6 s), keeping the reference's rhythm. 3. **Footage.** Each slot gets a window of one of your clips. With the vision model, it picks the moment that fits the slot's role — the strongest moment for the hook, payoff moments for payoff slots — and avoids repeating footage. Without it, a motion-and-detail heuristic matches each slot's energy. 4. **Layout.** Footage that matches the output shape fills the frame. Footage that doesn't (a phone recording, a landscape clip in a vertical edit) is never cropped — it sits over a blurred copy of itself. When there are captions, it moves below a clear caption band so text never covers the content. 5. **Captions** follow the style's look, timed in chunks of its words-per-screen. With a highlight colour, the active word lights up in turn. Lines come from `captions`, or are written from your `brief` in the reference's voice. 6. **Audio.** Your music (trimmed, faded out) with your clips' sound ducked underneath, or your clips' own sound alone, normalized to −14 LUFS for social platforms. 7. **QA.** The exact file you receive is checked for length, size, black frames, frozen picture, and silent or badly levelled audio. A failing render fails the job and refunds it. ## Steering an edit - **Captions** — pass them explicitly for full control; the first line is the hook and should make a promise. - **Length** — `duration` rescales every slot proportionally. - **Shape** — `aspect` renders 9:16, 4:5, 1:1, 4:3 or 16:9 from the same style. - **Several variants** — run several edits from one style with different captions, music or clips; analysis is paid once. - **The plan** — every edit returns `plan.json`: the clip, in-point, length and transition of each slot. ## Good inputs - References: 10–90 seconds; the clearer the structure, the better the style. - Clips: more raw footage than the edit needs (2–3× its length), in focus, with clear subjects. - Formats: anything common — mp4, mov, webm, mkv; mp3, wav, m4a for music. --- # Generating images, characters and video Second Studio can make the material around your real footage: a recurring character, the places it lives, the props it holds, cinematic b-roll, and the voice it speaks with. ## How generation works Every request runs through a **prompt director** before it reaches a model. The director applies the studio's production craft — face locks, character sheets, location plates, timed video beats and performed voiceover, including what failed before — and turns your plain-language `brief` into a finished prompt. The craft and the prompts it writes stay on our side; each result returns `notes` on any choice made or part of the brief that could not be honoured. You don't write prompts. Describe what you want; the skills do the rest. (If you do want to send your own prompt verbatim, pass `raw: true` with `prompt`.) **Rules applied to every generation:** people are fine, including photorealistic people. A specific real person's likeness is allowed only as a [character](#characters) built from photos you confirm you have permission to use; named celebrities and public figures are never rendered. No software interfaces or legible screen text (real product UI is recorded, not generated — see [Recording](recording.html)), no third-party logos, nothing harmful. Requests the model provider's moderation rejects fail and are refunded. ## Characters A **character** is an identity the studio stores for you — a mascot, an invented character, or a real person such as your founder: a name, a description, optional photos, and a generated reference sheet. Once it is `ready`, pass its id to any image or video and the studio attaches the sheet (and photos) as identity references automatically — no asset ids to manage. `POST /v1/characters` (MCP: `create_character`) — priced like one image. ```json { "name": "Chip", "description": "A friendly cartoon chili pepper mascot: glossy red body, curly green stem cap, big warm eyes, tiny white chef's apron. 3D animated-film look." } ``` | Field | | |---|---| | `name` | **Required.** Up to 80 characters. | | `description` | **Required.** Looks, outfit, personality, art style. | | `referenceAssetIds` | Up to 6 photos of the person or character, uploaded as assets. The sheet is made to match them. | | `consent` | **Required with photos.** `true` confirms you have this person's permission to use their likeness (or that it is not a real person). | | `aspect`, `resolution` | For the sheet; default `16:9` at `2K`. | The response is the character with `status: "creating"`. Poll `GET /v1/characters/{id}` until it is `ready` (about a minute), then use it: ```json { "kind": "scene", "characterIds": [""], "aspect": "9:16", "brief": "Chip stirs a pot on a sunlit stove, steam rising, wearing his apron." } ``` `characterIds` works on `POST /v1/images` and `POST /v1/videos` (up to 4 characters). `GET /v1/characters` lists yours with sheet URLs; `DELETE /v1/characters/{id}` removes one and its sheet. **Restate wardrobe in each brief**: the character locks who they are, not what they wear in a given scene. ## Images `POST /v1/images` (MCP: `generate_image`) — GPT Image 2.5. Priced per image by resolution; see [Credits](billing.html). | `kind` | Use it for | |---|---| | `character_sheet` | Locking a character's identity: turnarounds, face close-up, details | | `character` | A single character image | | `location` | An empty, reusable set your character can be placed into | | `prop` | Objects a character holds or uses, as a reference sheet | | `scene` | A locked character inside a locked location | | `product` | Product shots | | `freeform` | Anything else | | Field | | |---|---| | `brief` | **Required.** What you want, in plain words. | | `kind` | Default `freeform`. | | `references` | Up to 16 `{ "assetId": "…", "role": "character identity" }`. Use your character sheet here for consistency. | | `aspect` | `1:1`, `16:9`, `9:16`, `4:3`, `3:4`, `3:2`, `2:3`, `4:5`, `5:4`, `21:9` | | `resolution` | `1K`, `2K` (default) or `4K` | | `n` | 1–4 images | | `model` | `gpt-image-2.5` (default), `soul-2`, `grok-imagine-2`, `qwen-image-3`, `ideogram-4`, `nano-banana-pro`. `GET /v1/models` lists what each supports (reference count, resolutions, batch). No Midjourney: it has no API and automating a personal account breaks its terms. | | `seed` | Where the model supports it, for repeatable variations. | | `skillIds` | Up to 4 of your own [skills](#your-own-skills). | ```json { "kind": "character_sheet", "aspect": "16:9", "brief": "Chip, a friendly cartoon chili pepper mascot: glossy red body, curly green stem cap, big warm eyes, tiny chef's apron." } ``` ### Keeping a character consistent Use a stored [character](#characters) and pass `characterIds`. If you'd rather manage it yourself, make a `character_sheet` image and pass it in `references` with a role like `"character identity"`. ## Video `POST /v1/videos` (MCP: `generate_video`) — Seedance 2.5, directed so every beat is timed to the clip's length. | Mode | Send | |---|---| | Text to video | `brief` only | | Animate an image | `imageAssetId`, optional `endImageAssetId` for the last frame | | From references | `referenceAssetIds` (up to 9: character sheet, location…), `characterIds` | | Following a motion reference | `referenceVideoAssetIds` (up to 3 clips whose choreography, camera and timing the shot follows) | | A character speaking | `referenceAudioAssetIds` (a voice track, up to 3), with image or video references | | Field | | |---|---| | `brief` | **Required.** The shot. | | `duration` | 4–30 seconds (default 5) | | `resolution` | `480p`, `720p` (default), `1080p` | | `aspect` | `21:9`, `16:9`, `4:3`, `1:1`, `3:4`, `9:16` (ignored when animating an image) | | `audio` | Generate sound with the video (default `true`) | | `estimate` | `true` returns the price in credits without generating. Accounts on their own provider key also see `providerUsd`, the cost that key will incur. | **Video is priced per request** from the model provider's own quote for those exact settings, so the price follows length and resolution. Always available up front: ```bash curl -X POST https://frugal-anteater-681.convex.site/v1/videos -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \ -d '{"brief":"slow push-in on Chip stirring a pot, steam rising","duration":6,"estimate":true}' # → { "estimate": true, "credits": 14 } ``` For a character who talks on camera, generating the shot *with* the voice track (reference audio) gives far better results than lip-syncing an existing clip afterwards — the motion is built around the speech. To put a stored character into a trend: upload the trend clip as an asset (you must have the right to use it), pass it in `referenceVideoAssetIds` with the character in `characterIds`, and describe the scene in the brief. The studio follows the clip's motion and camera; it does not copy the clip's footage. ## Voice `POST /v1/voice` (MCP: `generate_voice`) — ElevenLabs v3, 1 credit per 500 characters. By default your script is directed into a *performed* one — same words, with v3 audio tags and punctuation for rhythm and emotion, because flat reads are almost always a rhythm problem rather than a voice problem. `perform: false` reads the text exactly as written. | Field | | |---|---| | `text` | **Required.** Up to 5,000 characters. | | `voice` | A voice name (default `Rachel`). | | `direction` | How it should be performed: "warm, slightly amused, conspiratorial". | | `stability` | 0–1. Lower is more expressive. | | `perform` | Default `true`. | ## Lip sync `POST /v1/lipsync` (MCP: `lipsync`) — 15 credits per clip: re-times a video's mouth to an audio track. Use it for footage you already have; for new speaking shots, prefer video with reference audio. ## Availability Image generation and the skills are live. Video, voice and lip sync return `503 not_configured` on a deployment where their provider has not been connected yet. ## Your own skills The studio applies its own craft inside every `generate_*` call. If your team has recipes of its own — an unretouched-portrait look, a brand duotone, how to add someone to a group photo so the grain matches — keep them in the product instead of in a doc: `POST /v1/skills` `{ "name", "instructions", "defaults": { "aspect", "resolution", "n" }, "references": [{ "assetId", "role" }] }`. Skills are shared with the team and versioned: an update makes a new version, and a job records the version it ran with. Apply them with `skillIds` on images, characters and edits, or set `skillIds` on a brand kit so every image made for that brand gets them. The studio's skills still run first; yours are appended to the director's context and its references join the request's references. `GET /v1/skills`, `GET/POST/DELETE /v1/skills/{id}`. Free. ## Edit a photo in place — `POST /v1/images/edit` Put a stored character or a referenced subject into a masked area of a real photo and keep every pixel outside it exactly as shot. The model only sees a window around the mask; its output is composited back inside the mask with a feathered edge, so the rest of the photo is the original bytes, not a re-render. ```json { "sourceAssetId": "…", "mask": { "rect": [0.62, 0.12, 0.3, 0.8] }, "brief": "standing behind the couch, hands in pockets, lit from the left like the man next to him", "characterIds": ["…"] } ``` | field | | |---|---| | `mask` | `{ "rect": [x, y, w, h] }` or `{ "polygon": [[x, y], …] }` in pixels or 0–1 fractions — **include the whole subject** (head to feet), with a little room. | | `maskAssetId` | Instead of `mask`: an image the size of the photo, white or opaque where to paint. | | `characterIds`, `references`, `model`, `skillIds` | As for images. | Output: the edited image at the photo's resolution (`outputs.image`) and the mask used (`outputs.mask`). Priced like one image. For several people, run one edit per mask. ## Swap a character into a video — `POST /v1/swaps` Keeps the source footage — camera, motion, everyone else, timing — and changes only the subject. This is the trend-video tool: `POST /v1/fetch` with `start`/`end` for a 4–30 s clip, then: ```json { "mode": "object_swap", "sourceVideoAssetId": "…", "characterIds": ["…"], "resolution": "720p", "estimate": true } ``` `mode: "motion_transfer"` is the other direction: your character driven by the source's choreography and camera. 1–8 reference images in total (`characterIds` attach a character's sheet and photos; `references` adds more). Output keeps the source length (sources over 30 s are trimmed). Quoted per request; `estimate: true` first. Own-key accounts pay the platform fee plus the provider. ### Several people, one photo `edits: [{ mask, brief, characterIds }, …]` (up to 6) applies the slots in order on the same photo in one job; each slot sees the previous one's result, so inserted people see each other. ## Voices of your own With your ElevenLabs key connected ([Team](team.html#your-own-provider-keys)): - `GET /v1/voices` — your workspace's voices and the clones made here. - `POST /v1/voices/clone` `{ name, sampleAssetIds (1–5 clean single-speaker clips), consent: true }` — an instant clone on your workspace; returns a `voiceId` for `POST /v1/voice`, `POST /v1/dialogue` and explainers. Priced like any voice (1 credit per 500 characters). `DELETE /v1/voices/{id}`. - `POST /v1/dialogue` `{ lines: [{ speaker, voiceId, text }] }` — one track, per-line `start`/`end`. ## Drafts, finalize, cancel - `draft: true` on `POST /v1/videos` renders at 480p for a cheap likeness/choreography check. - `POST /v1/videos/{jobId}/finalize` `{ "resolution": "1080p" }` re-renders a finished job with the same directed prompt and inputs; `estimate: true` shows the price. - `POST /v1/jobs/{id}/cancel` cancels a queued or running job and refunds its credits in full. - Generated clips start moving on the first frame; pass `holds: true` if you want the opening and closing frames held for a title. ## Upscale — `POST /v1/upscales` `{ "assetId": "…", "factor": 2 }` resamples a video up to 2160p with Lanczos and light sharpening. This is honest resampling, not a generative upscaler; the result says `method: "lanczos"`. 3 credits. --- # Explainer videos An explainer is a 30–120 second video that says what a product does, for whom, and why it matters: the kind of thing that sits on a landing page, opens a sales call, or launches a feature. Second Studio makes them from three inputs you control: 1. **A brand kit** — name, what the company does, tone, colors, font, logo, product screenshots. 2. **A script** — written by the studio from your brief, or by you. It's plain JSON: a list of scenes. 3. **A render** — the scenes dressed in the brand, with motion, optional captions, narration and music. Rendering is deterministic. The same script and brand always produce the same video, so you can edit one line of a scene and re-render without anything else changing. The model writes words, never layout. ## 0. Start from your boards — `POST /v1/brands/analyze` `{ "imageAssetIds": ["…"], "notes": "editorial, light canvas" }` (1–6 brand boards, screenshots or deck pages) → a job whose `result.suggested` is a ready kit: colors, the closest font, a `style` block and tone, with a short rationale. Pass it to `POST /v1/brands`. 1 credit. ## 1. Save a brand kit — `POST /v1/brands` Upload the logo and any product images first ([assets](api.html#assets)), then: ```json { "name": "Acme Analytics", "description": "Product analytics for B2B SaaS teams: connect your app, ask questions in plain language, get answers without SQL.", "tone": "clear, confident, a little dry", "tagline": "See what your customers do next.", "colors": { "primary": "#F5F3FF", "secondary": "#B8B2D6", "accent": "#8B5CF6", "background": "#0F0B1F", "text": "#ECE9F7" }, "font": "Inter", "logoAssetId": "…", "images": [ { "assetId": "…", "description": "Dashboard showing a retention cohort chart" }, { "assetId": "…", "description": "The natural-language question box with an answer" } ], "musicAssetId": "…" } ``` | field | notes | |---|---| | `description` | The script writer's main source. Say what it does, for whom, and what's different. | | `colors` | Five hex values: `primary` (headlines), `secondary` (supporting text), `accent` (bars, numbers, buttons), `background`, `text`. Any you omit get a dark default. | | `font` | One of `Inter`, `Montserrat`, `Poppins`, `DM Sans`, `Space Grotesk`, `Playfair Display`, `Manrope`, `Sora`, `Karla`. Headlines always use it; set `style.bodyFont` for a different body face. | | `images` | Up to 12. The description tells the writer what each shows so it can place them in `image` scenes. Only these images can appear in a video. | | `musicAssetId` | A bed track, looped and ducked under narration. Optional; can also be given per render. | | `videos` | Up to 12 clips with descriptions: product recordings, b-roll, founder clips. The script can place them as `video` scenes. | | `style` | How the brand looks beyond colors and font — see below. | | `logoDarkAssetId` | A logo for light canvases. | | `backgroundAssetId` | A full-bleed photo or video behind every scene (dimmed so type stays readable). | | `skillIds` | Your own image skills, applied to every image made for this brand. | `GET /v1/brands`, `GET /v1/brands/{id}`, `POST /v1/brands/{id}` (update any field, including adding and removing library assets), `DELETE /v1/brands/{id}`. Free. ### Style Every field is optional; the renderer has defaults (dark canvas, soft accent glow, bold headings, check-mark lists, pill button). | `style.` | values | |---|---| | `canvas` | `dark` (brand background), `light` (off-white canvas, ink type), `custom` (your colors exactly, no glow) | | `gradient` | `true`/`false` — the soft accent glow behind scenes | | `headingWeight` | `light`, `regular`, `semibold`, `bold` | | `captionStyle` | `box` (translucent pill) or `plain` (text with a shadow) | | `listMarker` | `check`, `number`, `dash`, `dot` | | `buttonStyle` | `pill`, `underline`, `text` | | `radius` | 0–4: corner radius in % of the short side (0 = square) | | `logoSize` | 3–12: corner logo size | | `lockup` | `true` puts a large centered wordmark on the title and CTA cards | | `backgroundDim`, `backgroundTint` | 0–1: darken the background; tint it toward the brand colors (duotone feel) | | `bodyFont` | A second family (same list as `font`) for body text and captions, e.g. Montserrat headings over Karla body | A flat, editorial brand: `{ "canvas": "light", "gradient": false, "headingWeight": "light", "listMarker": "dash", "buttonStyle": "underline", "captionStyle": "plain", "radius": 0.2, "lockup": true }`. A photographic one: `backgroundAssetId` + `{ "backgroundDim": 0.5, "backgroundTint": 0.6, "lockup": true }`. ## 2. Get a script — `POST /v1/scripts` (2 credits) ```json { "brandId": "…", "brief": "60-second explainer for our homepage. Audience: heads of product at 50–500 person SaaS companies. Pain: they wait days for an analyst to answer simple questions. Key points: set up in an afternoon, plain-language questions, no SQL. Real numbers you can use: customers report 42% faster roadmap decisions. CTA: start a free trial at acme.example.", "duration": 60, "aspect": "16:9" } ``` The job's `result.script` is the scene list: ```json { "title": "Acme Analytics — homepage explainer", "scenes": [ { "id": "s1", "type": "title", "durationSec": 4, "headline": "Stop guessing what your users want.", "subline": "Acme turns raw product events into answers.", "narration": "You have the data. Getting an answer out of it still takes a week." }, { "id": "s2", "type": "steps", "durationSec": 8, "headline": "How it works", "steps": ["Connect your product", "Ask a question in plain language", "Share the answer"], "narration": "…" }, { "id": "s3", "type": "image", "durationSec": 6, "imageAssetId": "…", "headline": "Ask it like you'd ask a colleague", "fit": "contain", "narration": "…" }, { "id": "s4", "type": "stat", "durationSec": 4, "value": "42%", "label": "faster roadmap decisions", "narration": "…" }, { "id": "s5", "type": "cta", "durationSec": 5, "headline": "Try Acme free", "action": "acme.example", "narration": "…" } ] } ``` Scene types: `title`, `bullets`, `steps`, `stat`, `quote`, `image`, `video` (one of the brand's clips: `videoAssetId`, `start`, `fit`, `sound`), `compare` (left vs right), `cta`. Any scene may carry `backgroundAssetId` for a full-bleed photo or video behind it. Every scene has `id`, `durationSec`, `narration` (what a voice says) and an optional short `caption`. Edit anything — wording, order, durations, drop a scene — and send it to the render step. The studio only uses real numbers and claims from your brief; if you give it none, it won't invent any. Options: `duration` 15–180 s (default 60), `aspect` `16:9` | `9:16` | `1:1`, `language`. ## 3. Render — `POST /v1/explainers` (12 credits) Either a brief (the studio writes the script and renders in one go) or a script: ```json { "brandId": "…", "script": { "title": "…", "scenes": [ … ] }, "aspect": "16:9", "captions": true } ``` | option | | |---|---| | `brief` / `script` | One of the two. A script from step 2, edited as you like, or your own in the same shape. | | `aspect` | `16:9` (1920×1080), `9:16` (1080×1920), `1:1` (1080×1080). Layouts adapt. | | `captions` | Show each scene's caption or narration as an on-screen line. Good when there's no voice. | | `voiceover` | Narrate each scene with a generated voice — the platform's, or your own ElevenLabs voice (pass its `voiceId` as `voice`) when your key is connected. Scenes stretch to fit the read. | | `voiceoverAssetId` | Your own recorded or cloned narration track, played across the whole video. Give this *or* `voiceover`. | | `voiceoverJobId` | A finished dialogue or voice job: its track becomes the voiceover and its line timings set the scene cuts, in order — scene 1 runs for line 1, and so on. | | `estimate` | `true` returns the price and total seconds without rendering. | | `musicAssetId` | Bed track for this render; overrides the brand's. | | `duration` | With a brief: the target length. With a script: the scenes are rescaled to it; leave it out to keep the scenes' own timing. | | `language` | Used when you give a brief. | The result has the MP4 (`outputs.video`), the final `script` that was rendered, and `duration`. A render is silent unless you pass `musicAssetId`, `voiceover: true` or `voiceoverAssetId`; `captions: true` puts the narration on screen. Renders take a few minutes; poll the job or use the MCP tool, which waits. You can also pass an inline `brand` object instead of `brandId` for a one-off (same fields as the kit). ## Through MCP `create_brand` → `write_script` → `create_explainer`, plus `list_brands`, `get_brand`, `delete_brand`. The local MCP server accepts file paths for the logo, images and music and uploads them for you. ## What it's good for - Landing-page and App Store explainers, in every aspect ratio from one script. - Feature launches: new brief, same brand kit, done in minutes. - Localised versions: re-run the script step with `language`, keep the brand. - Sales enablement: a `compare` scene for "before / after" or "them / us", a `stat` for proof. Product screenshots in a video come only from the images you put in the brand kit; the studio never generates or fakes an interface. --- # Recording product UI `POST /v1/recordings` (MCP: `record_url`) opens a public web page in a real browser and films it while following your steps. You describe *what* to do; Second Studio makes it watchable: a visible cursor that travels to each target, typing at human speed, scrolling in smooth flicks rather than jumps, and a pause after every action. It never ends on an action — the last frame holds. ## Steps | Step | Fields | Does | |---|---|---| | `goto` | `url` | Navigate to another public URL | | `click` | `selector`, `timeout`? | Move the cursor to the element and click it | | `hover` | `selector` | Move the cursor onto the element | | `type` | `selector`, `text`, `delay_ms`? (default 85) | Click the field and type, key by key | | `press` | `key` (e.g. `Enter`, `Tab`) | Press a key | | `scroll` | `pixels` (negative scrolls up) | Scroll in smooth flicks | | `wait_for` | `selector`, `timeout`? (s) | Wait until the element is visible | | `hold` | `seconds` | Hold still so viewers can read | Every step also accepts `after` (seconds to pause afterwards, default 0.4) and `mark` (a name for this moment in `result.marks`). Selectors use [Playwright syntax](https://playwright.dev/docs/selectors): CSS (`#buy`, `[data-testid=save]`) or text (`text=Get started`). Without `steps`, the recording settles on the page, scrolls through it and returns to the top. ```json { "url": "https://example.com", "viewport": "mobile", "steps": [ { "type": "hold", "seconds": 2, "mark": "landing" }, { "type": "click", "selector": "text=Search" }, { "type": "type", "selector": "input[name=q]", "text": "chicken dinner" }, { "type": "press", "key": "Enter" }, { "type": "wait_for", "selector": ".results" }, { "type": "hold", "seconds": 3, "mark": "results" } ] } ``` ## Options | field | | |---|---| | `viewport` | `desktop` 1280×800 (default), `laptop` 1440×900, `hd` 1920×1080, `mobile` 390×844 | | `scale` | `2` records at 2× device pixels (a 3840×2160 file for `hd`): crisp text when the edit zooms in | | `typingDelayMs` | Per-character typing delay, default 85 (set `delay_ms` on a step to override) | Steps gained: `goto` takes `wait_for` (a selector to wait for on the new page, so a transition is never a frozen or black frame — otherwise it waits for network idle); `network_idle` waits for the page to settle; `click`, `hover` and `type` take `glide` (seconds the cursor travels, default 0.5) and `settle` (pause before the click, default 0.3). The cursor shows a click ripple. Every mark for a `click`, `hover` or `type` carries `box`: the element's bounding box as fractions of the frame (`x`, `y`, `w`, `h`), so an editor can zoom and draw callouts on exact coordinates. ## Viewports | `viewport` | Size | |---|---| | `desktop` (default) | 1280 × 800 | | `mobile` | 390 × 844 at 2× pixel density, touch enabled | ## Output A `recording` asset (mp4, 30 fps) and `result.marks`: `[{ "name": "landing", "t": 2.41 }, …]` — the time of every step, for cutting on the action rather than in the middle of it. Use the recording as a clip in an edit like any other footage. ## Limits and rules - Public `http(s)` URLs only; private and internal network addresses are refused. - Up to 60 steps and 300 seconds of recording. - Pages that need a login, or that sit on `localhost`, are not reachable yet. Record those yourself and upload the file as a clip. - Only record sites you own or have permission to film. --- # Posting to social accounts Second Studio publishes and schedules through accounts **you** connect. Supported platforms include TikTok, Instagram, YouTube, LinkedIn, Facebook, X, Threads, Pinterest and Bluesky. ## Connect an account ```bash curl -X POST https://frugal-anteater-681.convex.site/v1/social/connect -H "Authorization: Bearer $KEY" \ -H "Content-Type: application/json" -d '{"platform":"tiktok","redirectUrl":"https://your-site.com/connected"}' # → { "authUrl": "https://…" } ``` Open `authUrl` in a browser and approve on the platform's own consent screen. You land back on `redirectUrl` when it is done. Facebook Pages and LinkedIn organisations ask you to pick which page. Then `GET /v1/social/accounts` lists it: ```json { "data": [ { "id": "6a62538d…", "platform": "tiktok", "username": "yourbrand", "displayName": "Your Brand", "status": null } ] } ``` Your accounts are isolated in their own profile. Nobody else's agent can see or post to them, and you cannot target an account that is not yours. ## The approval model Posting is public and hard to undo, so it is split across the two keys: | | Agent key | Admin key | |---|---|---| | Draft a post | ✓ | ✓ | | Approve or reject a post | — | ✓ | 1. An agent drafts: `POST /v1/posts` → status `pending_approval`. 2. A person reviews it (`GET /v1/posts`) and approves with the admin key: `POST /v1/posts/{id}/approve` → `scheduled` or `published`. Or rejects it, and its credit is refunded. An account can opt into **auto-publish** (email peter@hellofaro.io), which skips approval. Even then, every post must name its accounts explicitly. ## Rules enforced on every post - **Name the account.** Each target needs the account's exact `id` and `platform` from `GET /v1/social/accounts`. Brands often own two accounts on one platform (consumer and business), and display names can be misleading. Unknown or mismatched accounts are refused, with the list of your real accounts in the error. - **No accidental "now".** A `scheduledFor` time in the past is refused rather than published immediately. To post now, say `"publishNow": true`. - **Explicit time zones.** `scheduledFor` must carry an offset (`2026-10-12T18:00:00-04:00`) or come with a `timezone` (`America/New_York`). - Instagram videos post as Reels and YouTube videos as Shorts. ## Status `pending_approval` → `approved` → `scheduled` → `published`, or `failed` (with `error`; the credit is refunded) or `rejected`. Statuses update as the platform reports back. ## Before you post - Captions are burned into the video for muted viewing; the post `caption` is the text under it. - Make sure you have the rights to everything in the video and the claims in the caption. - Never make earnings or income promises. --- # Team One account, many people. Each member has their own API key (so their agent or MCP client shows who they are), a role, and an optional monthly credit cap. Everything anyone makes — brand kits, stored characters, styles, uploads, finished videos, posts — belongs to the account and is shared with the team, with the member who made it recorded on it. ## Roles | role | can | |---|---| | `admin` | Everything: invite and remove members, mint and revoke keys, approve posts, change account settings, plus all of creator. | | `creator` | Make things — analyze, edit, record, generate, explainers, brand kits, characters — and draft posts. | | `reviewer` | Read everything, approve or reject posts, file reports. Cannot create or spend. | Account-level keys from before teams existed keep working: an admin key acts as `admin`, an agent key as `creator`. ## Invite someone — `POST /v1/members` (admin) ```json { "name": "Urav", "email": "urav@example.com", "role": "creator", "monthlyCreditCap": 500 } ``` Returns the member, their API key **once**, and a `claimUrl`. Send the person the claim link: it shows them the key one time and expires in 72 hours, so the key never passes through chat. They put it in their own MCP client (`SECOND_STUDIO_API_KEY`) and are up — `GET /v1/me` shows their name, role and spend. Keys made by an admin are active immediately. **First member:** an account that has no members yet may create its first one from any account-level key — it becomes the account's admin. That is how a new customer bootstraps their team on day one. Member fields: `role`, `email`, `monthlyCreditCap`, `monthlyUsdCap`, `perJobUsdCap`, `allowedSocialAccounts` (accountIds this person may post to), `webhookUrl` (POSTed a `job.finished` event when their jobs end). - `GET /v1/members` — everyone, with role, live keys, cap and `spentThisMonth`. - `POST /v1/members/{id}` — change `role`, `name`, `email`, `monthlyCreditCap` (`null` removes it). Changing the role re-scopes the member's keys at once. - `DELETE /v1/members/{id}` — their keys stop working; what they made stays. - `POST /v1/members/{id}/keys` — another key for the same person (e.g. a second agent). - `GET /v1/keys` · `DELETE /v1/keys/{id}` — every live key on the account and whose it is. ## Spend control Three caps, all checked **before** a job starts, from its estimate, with a `402 member_cap` that says which cap, what's spent and what the job would cost: - `monthlyCreditCap` — credits per month. - `monthlyUsdCap` — estimated **provider dollars** per month. This is the one that matters on accounts that run on their own provider keys, where a job costs 1–4 credits of platform fee but real money at the provider (about $0.45 for a character sheet, about $34 for a 30 s 1080p video). - `perJobUsdCap` — no single job over this estimate. `GET /v1/members` shows `spentThisMonth: { credits, usd }` per person; refunds for failed and canceled jobs net out for the same person. ## Who may change what Everything is visible to every member. Changing or deleting a brand kit, skill, character or voice is for admins and the member who created it. Removing a member keeps what they made. ## Shared library — `GET /v1/library` One call that lists what the team can reuse by id: brand kits, stored characters (with sheet URLs), analyzed styles, finished videos and uploads — each with `createdBy`. Agents should look here before creating something that may already exist: Urav's character sheet, the brand kit, last week's launch style. ## Attribution `memberId` (who made it) is on jobs, assets, brand kits, characters, posts and reports; posts also carry `decidedBy` and `decidedAt` for the approval. `GET /v1/reports` shows who filed each one. ## Your own provider keys Approved accounts connect their own Higgsfield and ElevenLabs keys themselves: `POST /v1/providers/higgsfield` `{ keyId, secret }`, `POST /v1/providers/elevenlabs` `{ apiKey }` (admin, over TLS; verified with the provider, stored encrypted, never returned; `DELETE` to disconnect; `GET /v1/providers` shows what is connected). With your own key, jobs cost the platform fee in credits and the provider bills you. Ask us to enable it on your account. ## Through MCP `library`, `list_members`, `invite_member`, `update_member`, `remove_member`, `create_member_key`, `list_keys`, `revoke_key`, `list_providers`, `connect_provider`, `disconnect_provider`. Reviewer keys see every tool but can only run the read-only ones plus `approve_post`, `reject_post` and `report_issue`. --- # Credits and billing Second Studio is prepaid. You buy credits; each operation spends some. There is no subscription and credits do not expire. ## Prices | Operation | Credits | |---|---| | Analyze a style (`POST /v1/styles`) | 2 | | Make an edit (`POST /v1/edits`) | 5 | | Record a web app (`POST /v1/recordings`) | 3 | | Fetch a video on our servers (`POST /v1/fetch`) | 1 | | Draft a post (`POST /v1/posts`) | 1 | | Generate an image (`POST /v1/images`) | Priced per image by resolution — check with `estimate: true` | | Create a character (`POST /v1/characters`) | Same as one image | | Generate video (`POST /v1/videos`) | Quoted per request from the model provider's price — check with `estimate: true` | | Generate voice (`POST /v1/voice`) | 1 per 500 characters | | Lip sync (`POST /v1/lipsync`) | 15 | | Write an explainer script (`POST /v1/scripts`) | 2 | | Render an explainer video (`POST /v1/explainers`) | 12 | | Brand kits (`POST /v1/brands`) | Free | | Everything else — uploads, reading jobs and styles, listing accounts, bug reports | free | ## Packs | Pack | Credits | Price | |---|---|---| | `starter` | 100 | $10 | | `growth` | 550 | $50 | | `scale` | 1,200 | $100 | `GET /v1/billing` always returns the current prices and packs. ## Buying credits Email [peter@hellofaro.io](mailto:peter@hellofaro.io) with the pack you want and the account it is for. Credits are added as soon as payment is confirmed, and show up immediately in `GET /v1/billing`. ## Refunds are automatic Credits are taken when a job or post is accepted. They are returned automatically when: - a job fails, including a render that fails its quality check; - a job gets no response from a worker for three hours; - a post is rejected, or fails on the platform. `GET /v1/billing/ledger` lists every purchase, charge and refund with the running balance, and each job shows `creditsCharged` and `refunded`. ## Running out A request you cannot afford returns `402 insufficient_credits` and is not started — nothing is half-done or partly charged. --- # Limits, security and data ## Limits | | Limit | |---|---| | Jobs queued or running at once | 4 per account | | New jobs | 120 per hour (bursts of 30) | | New posts | 60 per hour (bursts of 10) | | Bug reports | 60 per hour (bursts of 12) | | Clips per edit | 1–30 | | Edit length | 3–180 seconds | | Caption lines | 40, up to 200 characters each | | Input file size | 2 GB | | Fetched videos | 15 minutes long | | Recording | 60 steps, 300 seconds | | Job running time | 1 hour | Hitting a limit returns `429` with `rate_limited` or `too_many_jobs`. Wait and retry; the message says how long. Need more? Email [peter@hellofaro.io](mailto:peter@hellofaro.io). ## API keys - Keys look like `ss_live_…`. Only a hash is stored, so a key can never be shown again, by us or anyone. - Every key is approved by a person before it works. Until then it returns `403 key_pending_approval`. - **Agent keys** do everything except approve posts. **Admin keys** can also approve. Put only agent keys in agent and MCP configuration. - Treat keys like passwords: keep them out of code repositories, front-end code and screenshots. - Lost or leaked a key? Email [peter@hellofaro.io](mailto:peter@hellofaro.io) to revoke it and get a new one. ## Your footage - Files you upload, and everything Second Studio makes for you, stay in your account until you delete them with `DELETE /v1/assets/{id}`. Download links are long, unguessable URLs; anyone who has a link can open that file, so share them deliberately. - Each job works in its own temporary directory on an isolated worker, deleted when the job ends. - A **style** stores numbers and descriptions, never the reference's pictures or sound. Deleting the reference does not affect the style. - With semantic analysis on, a reduced-resolution copy of a video is sent, for that request only, to a vision model provider through OpenRouter. Turn it off per request with `semantic: false`. - Posts go through our social publishing provider to the platforms you connect. ## Responsible use - Analyze and fetch only videos you have the right to use as a reference. Platform terms restrict downloading; uploading a reference you already have is always preferred. - Record only sites you own or have permission to film. - Edits use only your footage and music. Do not upload material you do not have the rights to publish.