REST API · v1
Actoria API
Generate photos and videos with your own consistent actors from any language or service. The API exposes the same pipeline as the Actoria studio and the MCP server: standalone photos, first frames, Seedance 2.5 / MiniMax H3 / Wan 3.0 videos, recast, script-to-video and audio — with your library of actors, props and locations, or with images you pass directly. Same account, same tokens, same limits.
Base URL https://actoria.ai/api/v1 · JSON over HTTPS · spec openapi.json (OpenAPI 3.1)
Quick start
- Subscribe — generation needs an active plan; reading your library works without one.
- Create an API key at Settings → API & MCP. Keys look like
act_live_…, are shown once and can be scoped (read,generate,upload) and revoked. Send it asAuthorization: Bearer act_live_…. - Generate a photo — by default the call waits for the image and returns its URL:
curl -X POST https://actoria.ai/api/v1/photos \
-H "Authorization: Bearer $ACTORIA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Mia on a rooftop at golden hour, cinematic portrait",
"aspect": "4:5",
"references": [{ "type": "actor", "id": "act_…" }],
"wait": true
}'The response is 201 with photoId, jobId, status, url, cost and your remaining credits. Start without waiting ("wait": false → 202) and poll:
# started with "wait": false → poll (long-poll up to 120 s per request)
curl "https://actoria.ai/api/v1/jobs/job_…/wait?timeoutS=60" -H "Authorization: Bearer $ACTORIA_API_KEY"Python
import os, requests
API = "https://actoria.ai/api/v1"
H = {"Authorization": f"Bearer {os.environ['ACTORIA_API_KEY']}"}
r = requests.post(f"{API}/photos", headers=H, json={
"prompt": "A red lighthouse on a cliff at golden hour",
"aspect": "16:9",
"wait": False,
}).json()
job = r["jobId"]
while True:
w = requests.get(f"{API}/jobs/{job}/wait", headers=H, params={"timeoutS": 60}).json()
if not w.get("timedOut"):
break
print(w["status"], w["result"]["url"] if w["result"] else w["job"]["error"])Node.js
const API = "https://actoria.ai/api/v1";
const H = { Authorization: `Bearer ${process.env.ACTORIA_API_KEY}`, "Content-Type": "application/json" };
const photo = await fetch(`${API}/photos`, {
method: "POST",
headers: H,
body: JSON.stringify({ prompt: "A quiet mountain lake at dawn", aspect: "16:9" }), // wait defaults to true
}).then((r) => r.json());
console.log(photo.status, photo.url); // "succeeded", "https://actoria.ai/f/img_….png"Authentication and scopes
- Every request carries
Authorization: Bearer <key>. Never put the key in the URL — such requests are rejected with400. - Scopes:
read(lists, status, models, prices),generate(anything that creates a job and spends tokens),upload(reference images). Missing scope →403 insufficient_scopewith the scope named inWWW-Authenticate. - OAuth access tokens issued to connected apps (ChatGPT, Claude) are accepted too, with the scopes the user granted.
- Missing or invalid key →
401with aWWW-Authenticate: Bearerchallenge.
Endpoints
GET endpoints take their arguments as query parameters (numbers, booleans and comma-separated lists are coerced); POST endpoints take a JSON body. Every field, type and default is documented in the API reference and in the OpenAPI spec; each operation is named after its MCP tool.
account
Who you are: plan, balance, limits, models and prices.
library
Actors, props and locations — the reusable references.
| GET | /actors | list actors | read |
| GET | /props | list props | read |
| GET | /locations | list locations | read |
| GET | /library/search | search references | read |
| POST | /actors | create actor | generate |
| POST | /actors/{actorId}/verification | verify actor | generate |
references
Direct images (URL, base64 or multipart) as temporary references.
| POST | /references | upload reference | upload |
| POST | /references/upload | Upload a reference image (multipart) | upload |
photos
Standalone photos — a prompt plus references, no project needed.
| POST | /photos | generate photo | generate |
| GET | /photos | list photos | read |
| GET | /photos/{photoId} | get photo | read |
generation
First frames and videos inside projects and scenes.
jobs
Status and long-polling of generation jobs.
| GET | /jobs/{jobId} | get job | read |
| GET | /jobs/{jobId}/wait | wait job | read |
projects
Projects, scenes, script-to-video and assembly.
| GET | /projects | list projects | read |
| GET | /projects/{projectId} | get project | read |
| GET | /scenes/{sceneId} | get scene | read |
| POST | /projects/from-script | create script video | generate |
| POST | /projects/{projectId}/finish | finish project | generate |
| POST | /projects/{projectId}/autorun/stop | stop autorun | generate |
audio
Speech, sound effects, captions and audio edits.
| GET | /audio/sources | list audio sources | read |
| POST | /audio/speech | generate speech | generate |
| POST | /audio/sound-effects | generate sound effect | generate |
| POST | /audio/transcriptions | transcribe audio | generate |
| GET | /audio/assets/{assetId} | get audio asset | read |
| POST | /audio/edits | apply audio edit | generate |
recast
Recast (video → video) with your actors.
| GET | /recast/sources | list recast sources | read |
| POST | /recast/uploads | upload recast video | upload |
| POST | /recasts | recast | generate |
References: saved or direct
Generation endpoints take a references array. Saved library items are referenced by id; direct images come as a public https URL or base64 and are moderated like website uploads (a frame must contain no people; real faces only through verified photo actors created with POST /actors). Frame-kind references are ignored for photos — the photo is the frame.
"references": [
{ "type": "actor", "id": "act_…" }, // saved actor (GET /actors)
{ "type": "prop", "id": "prp_…" }, // saved prop
{ "type": "location", "id": "loc_…" }, // saved location (one per scene)
{ "type": "asset", "id": "ast_…" }, // from POST /references or /references/upload
{ "type": "frame", "sceneId": "scn_…", "which": "last" }, // continue from your own frame (videos)
{ "type": "url", "url": "https://…/product.png", "kind": "prop", "name": "red sneaker" },
{ "type": "base64", "data": "…", "kind": "location" } // ≤ 3 MB decoded
]Jobs and waiting
- Photos and frames take 10–60 s, videos 1–15 min. Job-creating endpoints answer
202while the job is running and201when it finished within the wait (wait/waitTimeoutSin the body). GET /jobs/{jobId}returns status, ETA and the result;GET /jobs/{jobId}/wait?timeoutS=blocks up to 120 s and returnstimedOut: truewhen the job is still running — just call it again. Job ids:job_…for photos, frames, videos and portraits;v2v_…for recasts.- Failed jobs refund their tokens and carry a short
error. Result files live underhttps://actoria.ai/f/…— plain https URLs you can download or pass on. - Or don't poll at all: pass
callbackUrland get a webhook.
Webhooks
Add "callbackUrl": "https://…" to any job-creating request (POST /photos, /frames, /videos, /recasts, /actors). When the job reaches a terminal status we POST a JSON event to that URL. The URL must be public https (no private networks, no redirects); the request times out after 10 s and is retried after 1 min, 10 min and 1 h on any non-2xx answer. Recent deliveries and your signing secret are in Settings → Webhooks.
{
"id": "evt_…",
"type": "job.succeeded", // or "job.failed"
"createdAt": "2026-10-12T10:00:00.000Z",
"data": {
"job": { "id": "job_…", "kind": "frame", "status": "succeeded", "cost": 5, "resultUrl": "https://actoria.ai/f/…", "error": null, … },
"result": { "url": "https://actoria.ai/f/…", "posterUrl": null, "durationS": null },
"sceneId": "scn_…",
"projectId": "prj_…",
"photoId": "prj_…" // set for photos made with POST /photos
}
}Headers: X-Actoria-Event (job.succeeded | job.failed), X-Actoria-Delivery (unique id — use it to deduplicate retries) and X-Actoria-Signature: t=<unix seconds>,v1=<hex> where v1 = HMAC-SHA256(secret, "<t>.<raw body>"). Verify before trusting the payload:
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(secret, signatureHeader, rawBody) {
const { t, v1 } = Object.fromEntries(signatureHeader.split(",").map((kv) => kv.split("=")));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // replay window
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return expected.length === v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}import hmac, hashlib, time
def verify(secret: str, signature_header: str, raw_body: bytes) -> bool:
parts = dict(kv.split("=", 1) for kv in signature_header.split(","))
if abs(time.time() - int(parts["t"])) > 300:
return False
expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])Errors
Every error is JSON with a human-readable error and a stable code; validation errors add issues, payment errors add url, rate limits add retryAfterS and a Retry-After header. Responses are English only.
HTTP/1.1 402 Payment Required
{ "error": "Not enough tokens: need 5, have 2", "code": "paywall", "kind": "tokens", "required": 5, "available": 2, "url": "https://actoria.ai/billing" }| code | status | meaning |
|---|---|---|
| invalid_request | 422 | The request body or query doesn't match the schema (see `issues`). |
| invalid_json | 400 | The body is not valid JSON. |
| invalid_token | 401 | Missing, unknown, revoked or expired API key / token. |
| insufficient_scope | 403 | The key lacks the scope the endpoint needs (`scope` says which). |
| forbidden | 403 | The account is blocked. |
| verify_email | 403 | Confirm your e-mail on the website first. |
| paywall | 402 | No active subscription or not enough tokens (`kind`, `required`, `available`, `url`). |
| not_found | 404 | Unknown endpoint, or the object doesn't exist / isn't yours. |
| scene_busy | 409 | The scene already has a running job. |
| concurrent_limit | 409 | Too many videos rendering at once for your plan (`lane`, `limit`). |
| payload_too_large | 413 | The request body exceeds the cap. |
| prompt_unsafe | 422 | The prompt mentions minors or other prohibited content (`term`). |
| actor_unverified | 422 | A photo actor hasn't passed the face check (`url`). |
| moderation_rejected | 422 | The image failed moderation (people in a frame/prop/location reference, explicit content). |
| model_disabled | 422 | The requested model is switched off — see GET /models. |
| launch_rejected | 422 | The generation rules rejected the request (message explains). |
| rate_limited | 429 | Slow down and retry after `retryAfterS` seconds (`Retry-After` header). |
| provider_unavailable | 502 | The generation provider failed; tokens are refunded. |
| disabled | 503 | The API is temporarily switched off by the administrator. |
| internal | 500 | Unexpected error on our side. |
Limits and pricing
- 60 requests per minute per key (shared with the MCP server), 10 generations per minute per account, 120 requests per minute per IP.
- JSON bodies up to 8 MB (base64 images ≤ 3 MB decoded); multipart image uploads up to 12 MB; one long-poll up to 120 s.
- Prices are the studio's token prices — a photo or frame costs the frame price, videos are priced per second and model; see plans, per-second video prices or call
GET /estimatebefore spending. - Actors per photo or frame: 3 for 16:9, 2 for other aspects; reference caps per model are reported by
GET /accountandGET /models.
Security
- Keys are hashed at rest, work only on the API and MCP endpoints and can be revoked any time in Settings.
- Image and callback URLs are fetched with SSRF protection (public https hosts only); every image is moderated.
- Every call is logged (endpoint, outcome, job) for 30 days for support and abuse prevention.
FAQ
- Where do photos go?
- Under Photos on the Videos tab of your account — the same place as photos made on the website.
photoIdis the id of the wrapper project, so the photo can be opened in the studio or deleted from the website. - Can I use my own photos of people?
- Only as a photo actor:
POST /actorswithsource: "photo", consent, and the face check link it returns. Direct images with people are rejected. - Is there an SDK?
- Not yet — the OpenAPI document works with any client generator. For LLM agents use the MCP server: identical functions, same key.
Base URL: https://actoria.ai/api/v1 · API reference · openapi.json · MCP server · Support