# 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 our servers where enabled, or download it yourself and upload it. | | **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 **[Get started](/signin/)**. 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://second.studio.hellofaro.io/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://second.studio.hellofaro.io 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 for the first minute). 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 plus 1 per started minute of footage). 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. --- # Meet the crew Second Studio's capabilities come as a crew of robots. Each robot is a character with skills: it bundles the studio skills, MCP tools and REST endpoints for one job. Ask your agent to "use Sketch" or "have Boom voice this" and it knows which tools to call. Robots add no cost: you pay only for the jobs their tools run. | Robot | Role | Tools | |---|---|---| | **Sketch** (`sketch`) | Character Sheet bot | `create_character`, `get_character`, `list_characters`, `generate_image`, `edit_image` | | **Reel** (`reel`) | Director | `generate_video`, `generate_image`, `upscale_video`, `get_job`, `list_jobs` | | **Boom** (`boom`) | Voice and sound | `generate_voice`, `clone_voice`, `list_voices`, `delete_voice` | | **Clap** (`clap`) | Editor | `create_edit`, `finalize_video`, `fetch_video`, `download_asset` | | **Spool** (`spool`) | Style and templates keeper | `analyze_style`, `get_style`, `list_styles`, `save_template`, `list_templates`, `get_template`, `import_brand_from_url`, `list_brands` | | **Script** (`script`) | Writer | `write_script`, `create_explainer` | | **Grip** (`grip`) | Sets and props | `generate_image`, `edit_image`, `upload_file`, `library` | | **Rig** (`rig`) | Product and screen capture | `record_url`, `record_repo`, `create_edit` | Sketch is the Character Sheet bot: it designs a character and locks the look with a three panel reference sheet. ## For agents - MCP: `list_crew` returns every robot; `get_robot` with `robot_id` returns one, with its skills, tools, endpoints and a starter prompt. - REST: `GET /v1/crew` and `GET /v1/crew/{id}`. Free and read only. ```bash curl https://second.studio.hellofaro.io/v1/crew/sketch -H "Authorization: Bearer $SECOND_STUDIO_API_KEY" ``` Coming next: build your own robot (colors and moves) and give it your own skills. --- # 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. The server is hosted, so there is nothing to install: a URL and a header. Use the **agent key** in agent configuration. It cannot approve posts, so an agent can never release its own drafts. Keep it in the `SECOND_STUDIO_API_KEY` environment variable, never in chat or a repo: ```bash export SECOND_STUDIO_API_KEY="ss_live_..." ``` The fastest setup is the one prompt at [/setup.txt](/setup.txt): paste it into any agent. ## Hosted server URL: `https://second.studio.hellofaro.io/mcp` · Transport: Streamable HTTP · Auth: `Authorization: Bearer $SECOND_STUDIO_API_KEY` **Claude Code** ```bash claude mcp add --transport http second-studio https://second.studio.hellofaro.io/mcp --header "Authorization: Bearer $SECOND_STUDIO_API_KEY" ``` **Cursor** (`.cursor/mcp.json`), Windsurf and other clients with a JSON config ```json { "mcpServers": { "second-studio": { "type": "http", "url": "https://second.studio.hellofaro.io/mcp", "headers": { "Authorization": "Bearer ${env:SECOND_STUDIO_API_KEY}" } } } } ``` **Claude Desktop** (`claude_desktop_config.json`, through the `mcp-remote` bridge; put your key in place of MY_KEY, the file stays on your computer): ```json { "mcpServers": { "second-studio": { "command": "npx", "args": ["-y", "mcp-remote", "https://second.studio.hellofaro.io/mcp", "--header", "Authorization: Bearer ${SECOND_STUDIO_API_KEY}"], "env": { "SECOND_STUDIO_API_KEY": "MY_KEY" } } } } ``` **ChatGPT and other apps with connectors**: add a custom MCP connector with the URL above and the same Authorization header. **No MCP support**: call the REST API at `https://second.studio.hellofaro.io/v1` with the same header. `GET /v1/me` checks the key. Uploads go to a one-time upload URL from `upload_asset`, and agents poll `get_job` until a job finishes. ## Tools | Tool | What it does | Credits | |---|---|---| | `whoami` | Account, key scope and settings | n/a | | `get_credits` | Balance, prices and packs | n/a | | `upload_file` *(local)* | Upload a file from disk | n/a | | `create_upload_url` + `register_asset` *(remote)* | Upload in two steps | n/a | | `get_asset`, `list_assets`, `delete_asset` | Manage files | n/a | | `fetch_video` | A reference from a social URL | 1 if fetched on our servers | | `analyze_style` | Reference video to a reusable style | 2 for the first minute, then 1 per extra minute (10 min max) | | `get_style`, `list_styles` | Read styles, including the full spec | n/a | | `list_templates`, `get_template`, `save_template`, `delete_template` | Social videos saved to replicate | free (3 with analyze) | | `create_edit` | Your clips cut to a style | 5 | | `record_url` | Record a public web app | 3 | | `record_repo` | Build a private GitHub repository and record its key flows | 20+ | | `connect_github` | Admin: link to install the GitHub App on chosen repositories | free | | `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 | n/a | | `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 first minute, +7 per extra minute | | `generate_image` | Character sheets, characters, locations, props, scenes, products | GPT Image 6 / 16 / 48 at 1K / 2K / 4K. A character sheet is twice that, and half comes back if the first render passes | | `generate_video` | Video shots, from text, an image or references (+ a voice track) | quoted per request | | `generate_voice` | Performed voiceover | 2 minimum, then 1 per 250 characters | | `lipsync` | Re-time a mouth to an audio track | 15 min, 1.25 per second | | `get_job`, `list_jobs` | Job status and outputs | n/a | | `list_social_accounts` | Connected accounts | n/a | | `connect_social_account` | A link to connect a new account | n/a | | `create_post`, `list_posts` | Draft and track posts | 1 per post | | `list_crew` | The crew: preset robots and the tools each bundles | free | | `get_robot` | One robot's skills, tools, endpoints and starter prompt | free | | `list_models` | Image, video and swap models and what each supports | n/a | | `create_skill`, `list_skills`, `get_skill`, `update_skill`, `delete_skill` | Your own versioned image recipes | n/a | | `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 / n/a / 3 | | `library` | Everything the team can reuse by id, with who made it | n/a | | `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) | n/a | | `list_invites`, `invite_by_email`, `create_invite_link`, `revoke_invite` | Invite by email (joins on Google sign-in) or by expiring link (admins; managers into their team) | none | | `my_memberships`, `list_teams`, `create_team`, `update_team`, `delete_team`, `set_member_team` | Teams and managers (governance accounts; admin to change) | none | | `list_budgets`, `allocate_budget`, `revoke_budget`, `set_key_credit_cap`, `list_audit` | Member and team credit budgets, per-key caps, audit log (admins; managers inside their team) | none | | `list_providers`, `connect_provider`, `disconnect_provider` | Your own Higgsfield / ElevenLabs keys (admin, where enabled) | n/a | | `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 | | `import_brand_from_url` | Import a brand from its guidelines page or brand book PDF: logo, colors, fonts, voice and tone, taglines, do's and don'ts, imagery notes. Fills a kit you pick or creates one, and every field stays editable. Also on the Brand tab in the dashboard. | 1 | | `brand_from_website` | Brand guidelines from your public website (colors, font, tone, audience, products, voice rules), saved to a brand kit without overwriting your edits. New accounts with a work email get one free scan of their domain at signup. | 1 | | `report_issue`, `list_reports` | File a bug, feature request or question; follow the response | n/a | | `feedback`, `/feedback` prompt | Send product feedback to the team (10 per hour per key) | 0 | The `/feedback` prompt (MCP prompt `feedback`) asks what you want to share, then sends it with the `feedback` tool. It is free. 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://second.studio.hellofaro.io` · 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:** [get started](/signin/). 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 | `bad_request` (fallback), `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. ``` ### `DELETE /v1/account` Deletes the signed-in person's own account. This is the endpoint the mobile app's Delete account button calls. Authenticate with either: - an API key (`Authorization: Bearer ss_live_...`) owned by a member who is linked to a signed-up person. The account deleted is that person's. Account-level keys (no member) get `403 no_user_for_key` and nothing changes. - the person's Clerk session token (`Authorization: Bearer `). ```bash curl -X DELETE https://second.studio.hellofaro.io/v1/account \ -H "Authorization: Bearer $SECOND_STUDIO_API_KEY" -H "Content-Type: application/json" \ -d '{"confirm":"DELETE"}' ``` ```json { "deleted": true, "clerkDeleted": true, "memberships": 1, "orgsClosed": 1, "keysRevoked": 2 } ``` It removes the person from every account they belong to, revokes every API key they own (including the one used), writes `account.deleted` to each account's audit log, anonymizes their profile and deletes their sign-in. From a Convex client the same thing is `api.account.deleteMe({ confirm: "DELETE" })`. | Status | Code | Meaning | |---|---|---| | 400 | `confirm_required` | Body must be `{"confirm":"DELETE"}`. | | 401 | `unauthorized` | No valid signed-in session, or the API key is invalid or revoked. | | 403 | `no_user_for_key` | The API key is not tied to a person (account-level key), so it cannot delete an account. | | 404 | `not_found` | Already deleted. | | 409 | `sole_owner` | You are the only admin of an account with other members. Make someone else an admin first. | If you are the only member of an account, that account is closed and all of its keys are revoked. Unused credits are not refunded. ## 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 for the first minute, then 1 per extra minute, 10 minutes max) ```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 plus 1 per started minute of footage) | Field | Type | | |---|---|---| | `styleId` | string | **Required.** | | `clipAssetIds` | string[] | **Required.** 1 to 10 of your video assets, 30 minutes of footage in total at most. | | `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/crew` · `GET /v1/crew/{id}` | The crew: preset robots and the skills, tools and endpoints each bundles ([Meet the crew](crew)). Free. | | `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. | | `POST /v1/brands/import` | `{ "url": "https://example.com/brand", "brandId": "optional" }`. Imports a brand guidelines page or brand book PDF: logo (saved to the kit), colors and named palette, fonts, voice and tone, taglines, do's and don'ts, imagery and logo usage notes. Fills `brandId` when given, otherwise creates a new kit; every field stays editable. Follows same-site guide pages and up to 2 linked PDFs. Public http(s) only, 10 imports per hour per team. 1 credit. | | `POST /v1/brands/from-website` | `{ "url": "https://example.com" }`. Reads the homepage and a few key pages and writes brand guidelines to `result.guidelines`, saved to a brand kit (`result.brand`). Public http(s) sites only. 1 credit. | 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 for the first minute, 7 per extra started minute. | ## 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. ### `POST /v1/repo-recordings`: record a private GitHub repository (20 credits, up to 3 flows) `{ "repo": "acme/web", "ref": "main", "maxFlows": 3 }`. Builds the app in a sandbox and records its key flows; needs GitHub connected (`POST /v1/github/install`). Optional `envSetId` (`/v1/env-sets`), `loginId` (`/v1/logins`), `storageState` or `storageStateAssetId`, `flows`, `mode`. Using an env set, login or session needs an admin key; `repo` must belong to the installation's GitHub account. Returns `503 github_not_configured` until GitHub is switched on. Price: 4 credits per flow past 3, 3 more with a saved login or storage_state, and 5 credits per started 10 sandbox minutes past 25, charged at completion (hard stop at 45 minutes). See [Recording](recording.html#recording-a-private-repository). ## Fetching ### `POST /v1/fetch`: a reference from a social URL (1 credit) `{ "url": "https://www.tiktok.com/@…/video/…", "start": 3, "end": 18 }`. `url` must be a public http or https address. Private hosts, link-local and metadata addresses (including IPv4-mapped IPv6), and other schemes are rejected with `400 invalid_url`. `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 download it on your own machine and upload it instead. Output: a `fetched` asset with `source` attribution (and the `trim` applied). Optional `"then": "analyze_style" | "remix" | "none"` starts the next step on the server as soon as the fetch succeeds, so the caller does not have to stay connected. Both `analyze_style` and `remix` start a style analysis of the fetched clip, charged like `POST /v1/styles` (2 credits for the first minute) when it starts; for `remix`, use the resulting `styleId` with your own clips or character. The fetch job's `result.followUp` holds `{ "then", "jobId" }` (or `{ "then", "error" }`, e.g. `insufficient_credits`, if it could not start), and the follow-up job's `input.fromJobId` points back at the fetch. ## Templates Social videos saved to replicate later, from the mobile app's share sheet or by your agent. ### `POST /v1/templates`: save a template (free) `{ "url": "https://www.tiktok.com/@…/video/…", "title": "Hook style", "note": "Copy the fast cuts", "analyze": true }`. Only `url` is required, and it has the same public http(s) rules as `POST /v1/fetch`. `analyze: true` also fetches the video and learns its style, charged like `POST /v1/fetch` with `then: "analyze_style"`; if hosted fetching is off the template is still saved and the response includes `analyzeError`. ### `GET /v1/templates` · `GET /v1/templates/{id}` · `DELETE /v1/templates/{id}` Each template has `id`, `url`, `platform`, `title`, `note`, `status` (`saved`, `analyzing`, `ready`, `failed`) and `styleId` once ready. To replicate one: use `styleId` with `POST /v1/edits`, or, if there is none yet, `POST /v1/fetch` its `url` with `then: "analyze_style"`. ## 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 per account; 7 per X account when the caption has a link). 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 (`admin`, `manager`, `creator`, `reviewer`), keys, per-person credit caps and the shared library have their own page: [Team](team.html). Teams (with invites) and budgets (with key caps) are separate switches, on for accounts created from now on; older accounts keep their behavior until we enable them, and until then those endpoints answer `403 governance_disabled`. | | | |---|---| | `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/teams` · `POST /v1/teams` · `POST/DELETE /v1/teams/{id}` · `POST /v1/members/{id}/team` | Teams and their managers (governance accounts). | | `GET /v1/allocations` · `POST /v1/allocations` · `DELETE /v1/allocations/{id}` | Member and team credit budgets, enforced when a job or post is accepted (`402 budget_exceeded`). | | `GET /v1/invites` · `POST /v1/invites` · `POST /v1/invites/links` · `DELETE /v1/invites/{id}` | Email invites that activate on Google sign-in, and expiring, limited-use invite links. | | `POST /v1/keys/{id}/cap` | Monthly credit cap on one key. | | `GET /v1/audit` · `GET /v1/me/memberships` | Audit log; the accounts you belong to. | | `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`. ### `POST /v1/feedback` Send product feedback: what to fix, add or keep. Free (0 credits), limited to 10 per hour per key. The MCP `feedback` tool and the `/feedback` prompt use this route. | Field | Type | | |---|---|---| | `message` | string | **Required.** Up to 4,000 characters. | | `category` | string | `bug`, `feature`, `praise` or `other`. | | `contact` | string | Optional email or handle for follow-up. | ```json { "message": "Let me pick the caption font per edit.", "category": "feature" } ``` Returns `201` with `{ "id": "...", "message": "Thank you for the feedback. ..." }`. ## 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: up to 10 minutes. 10 to 90 seconds is the sweet spot; 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://second.studio.hellofaro.io/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, 2 credits minimum, then 1 credit per 250 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`) re-times a video's mouth to an audio track. Priced per second of output: 1.25 credits per second (pro: about 2.1), 15 minimum. Pass `seconds` at least as long as the clip (default 10); a longer clip is refused and refunded. 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 (2 credits minimum, then 1 per 250 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 for the first minute, 7 per extra minute) 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`. Upload the logo, images and music first with `upload_asset`, then pass the asset ids. ## 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 with `POST /v1/recordings`. Use a repository recording (below) for an app you can share from GitHub, or record it yourself and upload the file as a clip. - Only record sites you own or have permission to film. ## Recording a private repository `POST /v1/repo-recordings` (MCP: `record_repo`) builds your app from a GitHub repository in an isolated, throwaway sandbox, starts it with a local database, and records its key flows. Nothing from the build leaves the sandbox except the videos and a redacted build log. ### 1. Connect GitHub (once, admin) In the portal open **GitHub and logins** and press **Connect GitHub**, or call `POST /v1/github/install` (MCP: `connect_github`) and open the `url` it returns. Pick the repositories to share. The app asks for read-only access to code and metadata. Each recording gets its own key, scoped to one repository, that expires within an hour. `GET /v1/github` lists linked installations; `DELETE /v1/github/installations/{id}` unlinks one (uninstall the app on GitHub to remove our access entirely). If GitHub is not switched on for the service yet, these calls return `503 github_not_configured`. ### 2. Optional: env sets and logins (admin) - **Env sets**: `POST /v1/env-sets` `{ "name": "staging", "env": { "NEXT_PUBLIC_API": "..." } }`, or paste a `.env` file in the portal. Use test or development values only. - **Logins**: `POST /v1/logins` `{ "name": "demo", "host": "localhost", "username": "demo@acme.com", "password": "...", "totpSecret": "optional base32 key" }`. The recorder signs in off camera, before filming, and only on that host. - **Signed-in session**: pass a Playwright `storage_state` JSON inline as `storageState`, or upload it (`POST /v1/assets`) and pass its id as `storageStateAssetId`. Either way it is encrypted like a login and an uploaded file is deleted once copied, so it is never served by URL. Use this for single sign-on or approvals that can't be scripted. Recordings that use an env set, a login or a session need an admin key. Values are encrypted at rest (versioned keys) and never returned: lists show names, variable names and a masked user name. They are decrypted only for the worker, once per run, and scrubbed from build logs, progress and errors. Deleting an env set or login, or disconnecting GitHub, cancels and refunds queued recordings that use it. Adding, changing, deleting and using them is written to the audit log on governed accounts. ### 3. Record ```json { "repo": "acme/web", "ref": "main", "envSetId": "...", "loginId": "...", "maxFlows": 3, "flows": [ { "title": "Create a project", "steps": [ { "type": "goto", "url": "/projects" }, { "type": "cap", "text": "Start a project in one click" }, { "type": "click", "selector": "text=New project" } ] } ] } ``` | Field | | |---|---| | `repo` | `owner/name`, required | | `ref` | branch, tag or commit; default branch if omitted | | `subdir` | the app's directory in a monorepo, like `apps/web` (relative, no `..`); repo root if omitted | | `installationId` | only when the account has more than one GitHub installation | | `mode` | `auto` (default) or `real`; both build and run your real app today. `mock` (a faked backend) is not supported yet and is rejected with `400 invalid_field` | | `flows` | up to 5 `{title, steps}`; steps as above plus `cap {text}` for a caption. `goto` takes a path like `/settings` | | `maxFlows` | 1 to 5 (default 3). Without `flows`, we plan this many from the code and README | | `viewport`, `scale` | as for `POST /v1/recordings` | Price: 20 credits for up to 3 flows, 4 credits per extra flow (max 5), and 3 more when the run signs in with a saved login, an OTP or a storage_state. That is quoted up front (`input.quote`). The first 25 sandbox minutes (install, build and recording) are included; after that it is 5 credits per started 10 minutes, charged at completion (`result.overtimeCredits`). The sandbox stops at 45 minutes, so overtime adds at most 10 credits. The result has `sandboxSeconds` (what overtime is based on), `flows: [{ flow, storageId, marks }]`, `buildLog` (secrets and tokens removed) and `modeUsed`. If the build, start-up or recording fails on our side (`build_failed`, `start_failed`, `flow_failed`), the credits are refunded. `bad_repo` (nothing we can build or run, for example not a supported web app) is not refunded. Supported first: Node frontends (Next.js, Vite) with Postgres or SQLite through Prisma or Drizzle, signing in with a seeded user, a saved login, or a signed-in session. --- # 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://second.studio.hellofaro.io/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. `redirectUrl` must be a public http or https URL; other schemes and private addresses are rejected. 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 | n/a | ✓ | 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 support@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. | | `manager` | Everything a creator can, plus run one team: give teammates credit budgets, cap their keys, read the team's audit log. Needs governance (below). | | `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 per character-sheet render, 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. ## Teams, budgets and audit (governance) Teams and credit budgets are two separate switches. **Accounts created from now on have both on.** Accounts created earlier keep exactly their current behavior until we enable one or both for them (ask us); until then the endpoints for that feature answer `403 governance_disabled`. With either on, people can also sign in (through Clerk, with Google, GitHub, Apple or email) to manage the account; agents keep using API keys. A sign-in links to the member with the same verified email. Apps use Clerk with Convex's `ConvexProviderWithClerk`: ```tsx ``` **Teams.** Group members and give each team a manager. Admins create, rename and delete teams; naming a manager makes that member a `manager` of the team. Deleting a team keeps its members on the account, without a team, and revokes the team's budgets. - `GET /v1/teams`: teams with their manager and members (any member). - `POST /v1/teams` `{ "name": "Video", "managerMemberId": "…" }` (admin). - `POST /v1/teams/{id}` `{ "name": "…", "managerMemberId": "…" | null }` (admin; `null` removes the manager, who becomes a creator). - `DELETE /v1/teams/{id}` (admin). - `POST /v1/members/{id}/team` `{ "teamId": "…" | null }` (admin). **Invites** (teams on). Admins invite anyone, into any team; a manager invites creators and reviewers into their own team. Every invite is in the audit log. - `POST /v1/invites` `{ "email": "zed@example.com", "role": "creator", "teamId": "…", "expiresInDays": 14 }`: a pending membership. It becomes active the first time that person signs in with the same verified email. We do not send the email yet (the response says `"emailSent": false`), so tell the person yourself. - `POST /v1/invites/links` `{ "role": "creator", "teamId": "…", "expiresInHours": 168, "maxUses": 1 }`: a shareable link. The token is shown once and only its hash is stored. Whoever opens it signs in and joins with that role and team, until it expires, runs out of uses or is revoked. - `GET /v1/invites`: pending email invites and live links (managers see their team's). - `DELETE /v1/invites/{id}`: revoke a link or cancel a pending email invite. **Credit budgets.** A budget is a spend limit on the account balance, not extra credits. Give one to a member or a whole team, per month (`"period": "month"`, resets on the 1st, UTC) or in total (`"period": "total"`). Admins can budget anyone and any team; a manager can budget members of their own team, but not themselves and not the team as a whole. Budgets are checked on the server when a job or post is accepted, in the same step as the charge; going over answers `402 budget_exceeded` with the scope (`member`, `team` or `key`), budget, amount spent and cost. Refunds for failed jobs give the budget back. - `GET /v1/allocations`: live budgets (all for admins, the team's for managers, your own otherwise). - `POST /v1/allocations` `{ "memberId": "…", "credits": 500, "period": "month", "note": "…" }`, or `teamId` instead of `memberId`. - `DELETE /v1/allocations/{id}`: revoke one. **Key caps.** `POST /v1/keys/{id}/cap` `{ "creditCap": 200 }` sets a monthly credit cap on one API key (`null` removes it), handy for an agent that should never spend more than a set amount. Admins, or a manager for keys of their team. **Audit log.** `GET /v1/audit` lists who changed what, newest first: governance switched on, team changes, member roles and teams, budgets granted and revoked, key caps, Google sign-in links. Admins see everything; managers see entries about their team. **Your accounts.** `GET /v1/me/memberships` lists the accounts the person behind this key belongs to (through their Google sign-in), with role and team. ## 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`. Governance tools (teams or budgets on): `list_invites`, `invite_by_email`, `create_invite_link`, `revoke_invite`, `my_memberships`, `list_teams`, `create_team`, `update_team`, `delete_team`, `set_member_team`, `list_budgets`, `allocate_budget`, `revoke_budget`, `set_key_credit_cap`, `list_audit`. They run through the same permission checks as the REST endpoints. --- # Credits, plans and billing Second Studio runs on credits. Each operation spends some; failed jobs are refunded automatically. You can buy one-time credit packs, or save with a monthly plan. Credits do not expire. ## Prices | Operation | Credits | |---|---| | Analyze a style (`POST /v1/styles`) | 2 for the first minute, then 1 per extra started minute, up to 10 minutes | | Make an edit (`POST /v1/edits`) | 5 plus 1 per started minute of footage, up to 10 clips and 30 minutes | | Record a web app (`POST /v1/recordings`) | 3 | | Fetch a video on our servers (`POST /v1/fetch`) | 1, or 2 for Instagram and other proxied hosts, plus 1 per extra 25 MB on a proxied download | | Draft a post (`POST /v1/posts`) | 1 per account, or 7 per X account when the caption has a link | | Generate an image (`POST /v1/images`) | GPT Image: 6 at 1K, 16 at 2K, 48 at 4K. Nano Banana: 6 at 1K or 2K, 9 at 4K. Own key: 1. Check with `estimate: true` | | Create a character (`POST /v1/characters`) | Twice the image price (32 at GPT Image 2K). Half is refunded when the first sheet passes | | Generate video (`POST /v1/videos`) | Quoted per request from the model provider's price, plus 2 credits (own key: 2, with no extra 2). Check with `estimate: true` | | Swap or motion transfer | Quoted per request (own key: 2) | | Generate voice (`POST /v1/voice`), including cloned voices | 2 credits minimum, then 1 per 250 characters | | Lip sync (`POST /v1/lipsync`) | 15 minimum; 94 per minute of output (pro: 157 per minute) | | Write an explainer script (`POST /v1/scripts`) | 2 | | Render an explainer video (`POST /v1/explainers`) | 12 for the first minute, 7 per extra started minute, narration included | | Upscale a video | 3 | | Brand kits (`POST /v1/brands`) | Free | | Everything else: uploads, reading jobs and styles, listing accounts, bug reports | Free | `GET /v1/billing` always returns the current prices, packs and plans. ## Credit packs (one-time top-ups) | Pack | Credits | Price | Credits per $ | |---|---|---|---| | `starter` | 100 | $10 | 10.0 | | `growth` | 520 | $50 | 10.4 | | `scale` | 1,050 | $100 | 10.5 | ## Monthly plans | Plan | Price | Credits every month | Credits per $ | Jobs at once | |---|---|---|---|---| | `studio` | $100/mo | 1,150 | 11.5 | 8 | | `studio_max` | $200/mo | 2,375 | 11.9 | 16 | Plans give 15% to 19% more credits per dollar than packs: **$20 of top-ups buys 200 credits; $20 of Studio buys 230, and $20 of Studio Max about 238.** Plan accounts also run 8 or 16 jobs at once (instead of 4). Monthly credits are added each time the invoice is paid and stay in your balance, even after you cancel. You can top up with packs at any time while on a plan. ## Annual billing Annual is 5% off twelve times the monthly price, paid once, with the year's credits granted up front. | Plan | Price | Credits for the year | Credits per month | |---|---|---|---| | `studio` | $1,140/year | 13,728 | 1,144 | | `studio_max` | $2,280/year | 27,468 | 2,289 | That is about 12 credits per dollar, a bit more than the monthly plans. Subscribe with `POST /v1/billing/subscribe` `{ "plan": "studio", "interval": "year" }`. If you already have an annual subscription, you keep your current price and your current credit grant until you change plans. A renewal does not move you onto this price by itself. ## Buying credits or a plan | What | REST | MCP | |---|---|---| | Top up with a pack | `POST /v1/billing/checkout` `{ "pack": "growth" }` | `buy_credits` with `pack` | | Start a plan | `POST /v1/billing/subscribe` `{ "plan": "studio" }` | `buy_credits` with `plan` | | Change plan | `POST /v1/billing/plan` `{ "plan": "studio_max" }` | `change_plan` | | Cancel | `POST /v1/billing/plan` `{ "plan": "none" }` | `change_plan` with `none` | Each returns a Stripe Checkout link (`url`) that a person opens to pay. Credits are added as soon as Stripe confirms payment, through our webhook, even if the browser is closed before the success page loads. A new customer can also buy a first pack at `/signup?pack=starter`, which creates the account. Plan changes take effect right away for concurrency, with no proration; the new monthly credit grant starts at the next renewal. Cancelling ends the plan at the end of the paid period. If a renewal payment fails the plan shows `past_due` while Stripe retries; if Stripe gives up, the plan ends and the account goes back to packs. Credits already granted are never taken back. ### Who can buy On accounts with teams (all new accounts): - **Admins** can buy packs and start, change or cancel plans. - **Managers** can buy packs (top-ups). The credits go to the account balance, not to their team's budget, so buying cannot raise anyone's spending limit. Managers cannot change the plan. - **Creators and reviewers** cannot buy; their 402 errors tell them to ask an admin or manager. Older accounts without teams keep the previous rule: any key except a reviewer's can get a pack link, and plans need an admin key. Every purchase is attributed to the account. The ledger (`GET /v1/billing/ledger`) shows each `purchase` and `subscription` grant, and on accounts with teams the audit log records who created each Checkout link, each purchase, every plan change and any failed payment. Member and team budgets are never changed by a purchase. ## 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, grant, 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. The error message says how to top up (`buy_credits`, or `POST /v1/billing/checkout`) and points to this page. ## How prices are set 1 credit is $0.10 at the Starter price. Every operation is priced so that at least 2.5 times its provider cost is covered by the net dollars we actually receive per credit on every monthly pack and plan, after bonus credits and Stripe's fees (among those, the lowest is Studio Max, about $0.081 per credit). A test in the codebase checks this for every monthly pack, plan and operation. --- # 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 to 10, and 30 minutes of footage in total | | 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 [support@hellofaro.io](mailto:support@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 [support@hellofaro.io](mailto:support@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.