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) return202with a job immediately. PollGET /v1/jobs/{id}every few seconds. - Credits are charged when a job is accepted and refunded automatically if it fails. See Credits.
- Getting a key: get started. Keys work only after a person approves the account; an
unapproved key gets
403 key_pending_approval. - Errors have one shape:
{ "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
{ "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) get403 no_user_for_keyand nothing changes. - the person's Clerk session token (
Authorization: Bearer <Clerk session JWT>).
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"}'
{ "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:
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:
{ "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)
{ "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:
{ "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.
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 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). 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.
Explainer videos
Brand kits, scripts and renders have their own page: Explainer videos.
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)
{ "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 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.
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
{ "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 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:
{ "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. 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. |
{ "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. |
{ "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 |
Agents: this page is also available as Markdown; the whole reference is in llms-full.txt.