# REST API reference # 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/PATCH/DELETE /v1/skills/{id}` | Image recipes, or codebase skills (`kind: "codebase"`) that name file paths and do not quote code. `GET /v1/skills?repoId=&kind=` filters. `skillIds` on images, characters, edits, scripts, explainers and brand kits. | | `GET /v1/skills/{id}/versions` · `POST /v1/skills/{id}/versions` | List versions and restore one. Restoring a codebase skill deleted within 30 days brings it back. | | `POST /v1/skills/{id}/regenerate` | Rewrite one codebase skill from the stored docs. 4 credits. A pinned, custom, or edited skill gets a proposal instead of an overwrite. | | `POST /v1/repos` · `GET /v1/repos` · `DELETE /v1/repos/{id}` | Connect a GitHub repository the app can already read. Does not start an index. Delete is for an admin, the member who connected it, or that team's manager. | | `POST /v1/repos/{id}/index` | Read the repo into docs and skills, or quote it with `estimate: true` (free, no model call). 12 credits plus 0.12 per selected file, rounded up, 3,000 file cap, 400 credit cap. A later commit is 6 plus 0.12 per changed file. A push does not start an index. | | `GET /v1/repos/{id}/indexes/{sha}` · `GET /v1/repos/{id}/docs` | Index status, and the docs (`kind`, `slug`). Reading is free. | | `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 |