VectorClone API
The VectorClone API gives you programmatic access to our full studio — face swap, talking avatars, lip sync, real-time full-body swap, and generative image editing — over a single, clean REST interface. Send an input, get back a rendered result — clean and watermark-free. No GPUs, no models to host, no infrastructure to run.
Every request is authenticated with an API key and billed from your prepaid balance. There are no subscriptions and no minimums — you pay only for what you generate, at the per-unit prices shown in Pricing.
Quickstart
Three steps to your first render — upload an asset, submit a job, poll for the result.
- 1Create an API key in the developer console and fund your balance.
- 2Upload each input asset to get a file_key.
- 3Submit a capability call, then poll the returned status URL until it succeeds.
# 1) Upload a portrait
FILE_KEY=$(curl -s https://api.vectorclone.com/api/v1/uploads \
-H "Authorization: Bearer $VECTORCLONE_API_KEY" \
-F "file=@portrait.png" | jq -r .file_key)
# 2) Submit an Avatar Studio job
JOB=$(curl -s https://api.vectorclone.com/api/v1/avatar \
-H "Authorization: Bearer $VECTORCLONE_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"image_key\":\"$FILE_KEY\",\"script\":\"Hello from VectorClone!\"}")
JOB_ID=$(echo "$JOB" | jq -r .id)
# 3) Poll until it is done
curl -s https://api.vectorclone.com/api/v1/jobs/$JOB_ID \
-H "Authorization: Bearer $VECTORCLONE_API_KEY"Authentication
Authenticate every request with your secret API key. Send it either as a Bearer token or in the X-API-Key header — both are accepted.
# Bearer token (recommended)
curl https://api.vectorclone.com/api/v1/pricing \
-H "Authorization: Bearer sk_live_..."
# or the X-API-Key header
curl https://api.vectorclone.com/api/v1/pricing \
-H "X-API-Key: sk_live_..."401 unauthorized.Create a separate key per project or environment so usage stays auditable and you can revoke one without disrupting the others.
Base URL & versioning
All endpoints live under a single versioned base URL:
https://api.vectorclone.com/api/v1The current version is v1. We only make additive changes within a version — new fields and new capabilities may appear, but existing fields and behavior stay stable. Breaking changes ship under a new version prefix.
All responses are JSON. Successful submits return 202 with a job you poll; errors return a JSON body with a stable code (see Errors).
Uploading assets
Capability calls reference inputs by file_key, not by URL. Upload each image, audio, or video file first and use the returned key. We host the upload on our own storage — there is no CORS or presign dance, and no external URLs are fetched.
/uploadsmultipart/form-data| Field | Type | Required | Description |
|---|---|---|---|
file | file | Yes | The asset to upload — an image, audio, or video file. The type is detected from the content type. |
curl https://api.vectorclone.com/api/v1/uploads \
-H "Authorization: Bearer $VECTORCLONE_API_KEY" \
-F "file=@face.png"{
"file_key": "api/images/9f8c...-a1.png",
"url": "https://cdn.vectorclone.com/api/images/9f8c...-a1.png"
}See Limits & formats for the maximum size of each asset type.
Async jobs & polling
Rendering is asynchronous. A capability call returns immediately with a queued job; you then poll its status URL (or receive a webhook) until it reaches a terminal state.
/jobs/{job_id}A job moves through these statuses:
| Status | Terminal | Meaning |
|---|---|---|
| queued | No | Accepted and waiting for a render slot. |
| processing | No | Actively rendering. |
| succeeded | Yes | Done — output_url is populated. |
| failed | Yes | Could not be completed; error holds a stable code. Held funds are refunded. |
{
"id": "b2f1c0de-...-77",
"status": "succeeded",
"output_url": "https://cdn.vectorclone.com/...signed...",
"credits_charged": 0.42,
"error": null
}output_url is a signed link that stays valid for a window set by us (7 days by default). Fetch the result and store it on your own infrastructure within that window — do not treat the URL as permanent. You are billed (credits_charged) only when a job succeeds; failed jobs are automatically refunded.Poll on a reasonable interval (e.g. every 2–5 seconds). Only your own jobs are visible to your keys.
Webhooks
Instead of polling, pass a callback_url on any capability call. When the job reaches a terminal state we POST the same payload the status endpoint returns to your URL.
| Header | Value |
|---|---|
| X-VectorClone-Event | job.completed |
| X-VectorClone-Signature | HMAC-SHA256 (hex) of the raw request body |
Verify authenticity by recomputing the signature over the raw body with your webhook secret and comparing it to X-VectorClone-Signature.
import crypto from "crypto";
app.post("/vc-webhook", express.raw({ type: "application/json" }), (req, res) => {
const expected = crypto
.createHmac("sha256", process.env.VC_WEBHOOK_SECRET)
.update(req.body) // the RAW bytes, not parsed JSON
.digest("hex");
if (expected !== req.get("X-VectorClone-Signature")) {
return res.status(400).end();
}
const event = JSON.parse(req.body.toString());
// event = { id, status, output_url, credits_charged, error }
res.status(200).end();
});Idempotency
Network retries should never double-charge you or render twice. Send an Idempotency-Key header (any unique string, e.g. a UUID) on a submit. A retry carrying the same key returns the original job — no second charge, no duplicate work.
curl https://api.vectorclone.com/api/v1/face-swap-image \
-H "Authorization: Bearer $VECTORCLONE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6b1a9e2c-order-4821" \
-d '{"image_key":"...","face_key":"..."}'Rate limits
Requests are rate limited per key. Exceeding the limit returns 429 with code rate_limited — back off and retry. Separately, the platform enforces a global concurrency ceiling so heavy API traffic can never starve capacity; if it is momentarily saturated you get at_capacity, which is safe to retry shortly.
| Tier | Requests / minute |
|---|---|
| default | 60 |
| pro | 600 |
Need a higher tier? Contact us from the console.
Errors
Errors return a JSON body with a stable, machine-readable code and a human-readable message. Branch on the code, not the message text.
{
"error": {
"code": "insufficient_credits",
"message": "Your API balance is too low for this call. Top up to continue."
}
}| HTTP | Code | Meaning |
|---|---|---|
| 401 | unauthorized | Missing, invalid, or revoked API key. |
| 402 | insufficient_credits | Balance too low — top up to continue. |
| 400 | invalid_input | A field failed validation. |
| 400 | unsupported_format | The uploaded file type isn't supported. |
| 400 | input_too_large | Asset exceeds the size/duration limit. |
| 400 | capability_unavailable | This capability is not currently enabled. |
| 404 | not_found | The job doesn't exist or isn't yours. |
| 429 | rate_limited | Too many requests — slow down and retry. |
| 429 | at_capacity | Platform momentarily saturated — retry shortly. |
Failed jobs (returned by the status endpoint) use these result codes:
| Code | Meaning |
|---|---|
| face_not_detected | No usable face was found in an input. |
| content_rejected | The input violated our content policy. |
| processing_failed | The render failed for another reason (auto-refunded). |
Avatar Studio
Turn a single portrait plus a script into a talking avatar video.
/avatar| Field | Type | Required | Description |
|---|---|---|---|
image_key | string | Yes | file_key of the source portrait (from /uploads). |
script | string | Yes | What the avatar says. Up to 5,000 characters. |
voice_id | string | No | A specific voice to speak the script. |
language | string | No | Language hint for the voice. |
output_resolution_p | int | No | 480, 720, or 1080. |
callback_url | string | No | Webhook POSTed on the terminal status. |
curl https://api.vectorclone.com/api/v1/avatar \
-H "Authorization: Bearer $VECTORCLONE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image_key": "api/images/portrait.png",
"script": "Welcome to the future of video.",
"output_resolution_p": 1080
}'{
"id": "b2f1c0de-...-77",
"status": "queued",
"status_url": "/v1/jobs/b2f1c0de-...-77"
}Face Swap — video
Swap a face into a source video. Billed per second of output.
/face-swap| Field | Type | Required | Description |
|---|---|---|---|
video_key | string | Yes | file_key of the source video. |
face_key | string | Yes | file_key of the face image to swap in. |
max_duration_sec | int | No | Cap the processed duration. |
output_resolution_p | int | No | 480, 720, or 1080. |
callback_url | string | No | Webhook POSTed on the terminal status. |
curl https://api.vectorclone.com/api/v1/face-swap \
-H "Authorization: Bearer $VECTORCLONE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"video_key": "api/videos/clip.mp4",
"face_key": "api/images/face.png"
}'{
"id": "b2f1c0de-...-77",
"status": "queued",
"status_url": "/v1/jobs/b2f1c0de-...-77"
}Face Swap — image
Swap a face into a single still image.
/face-swap-image| Field | Type | Required | Description |
|---|---|---|---|
image_key | string | Yes | file_key of the base image. |
face_key | string | Yes | file_key of the face image to swap in. |
callback_url | string | No | Webhook POSTed on the terminal status. |
curl https://api.vectorclone.com/api/v1/face-swap-image \
-H "Authorization: Bearer $VECTORCLONE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image_key": "api/images/scene.png",
"face_key": "api/images/face.png"
}'{
"id": "b2f1c0de-...-77",
"status": "queued",
"status_url": "/v1/jobs/b2f1c0de-...-77"
}Lip Sync
Re-sync a video's mouth movements to a new audio track.
/lip-sync| Field | Type | Required | Description |
|---|---|---|---|
video_key | string | Yes | file_key of the source video. |
audio_key | string | Yes | file_key of the audio to sync to. |
output_resolution_p | int | No | 480, 720, or 1080. |
callback_url | string | No | Webhook POSTed on the terminal status. |
curl https://api.vectorclone.com/api/v1/lip-sync \
-H "Authorization: Bearer $VECTORCLONE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"video_key": "api/videos/clip.mp4",
"audio_key": "api/audio/voice.wav"
}'{
"id": "b2f1c0de-...-77",
"status": "queued",
"status_url": "/v1/jobs/b2f1c0de-...-77"
}Reimagine
Edit or restyle up to four images from a text prompt.
/reimagine-image| Field | Type | Required | Description |
|---|---|---|---|
image_keys | string[] | Yes | 1–4 file_keys of the source images. |
prompt | string | Yes | The edit instruction. Up to 2,000 characters. |
callback_url | string | No | Webhook POSTed on the terminal status. |
curl https://api.vectorclone.com/api/v1/reimagine-image \
-H "Authorization: Bearer $VECTORCLONE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image_keys": ["api/images/room.png"],
"prompt": "Make it a cozy cabin at golden hour"
}'{
"id": "b2f1c0de-...-77",
"status": "queued",
"status_url": "/v1/jobs/b2f1c0de-...-77"
}Real-Time Swap (Full body)
Swap a full-body performance in real time on a live video stream — for calls, avatars, and interactive apps. Unlike the async capabilities, this is a live session: you create a session, connect a stream, and send/receive video with sub-second latency.
/real-time-swap/session| Field | Type | Required | Description |
|---|---|---|---|
duration_minutes | int | Yes | Maximum length of this session (1–240). You are billed PER SECOND actually streamed, up to this cap — the stream hard-stops here so you never overrun. You must have the max cost available to start. |
prompt | string | No | Optional swap instruction. A sensible default is used if omitted. |
curl https://api.vectorclone.com/api/v1/real-time-swap/session \
-H "Authorization: Bearer $VECTORCLONE_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "duration_minutes": 10 }'{
"session_id": "e7c1...-9a",
"stream_url": "wss://api.vectorclone.com/realtime",
"model": "vectorclone-rt-1",
"session_token": "rt_...",
"prompt": "Replace the person's face ...",
"max_duration_sec": 600,
"expires_at": "2026-07-12T18:40:00Z",
"cost_usd": 24.00
}Connecting to the live stream
After creating a session, open a WebSocket to the returned stream_url with your session_token— the token is all that's needed. You'll get back the details to join a standard LiveKit media room, where you publish your camera and subscribe to the swapped video track. Any standard LiveKit client works — no special SDK.
- 1Open a WebSocket to stream_url?session_token=… (the token is all that's needed).
- 2On open, send {"type":"session_join","passthrough":false}, then a set_image (to swap to a reference face) or a prompt message.
- 3Receive a room_info message with a livekit_url + token.
- 4Join that LiveKit room with any LiveKit client; publish your camera and subscribe to the transformed track (register the listener BEFORE connecting — the swapped track can arrive immediately).
const ws = new WebSocket(
streamUrl + "?session_token=" + sessionToken // streamUrl from the session response
);
ws.onopen = () => {
ws.send(JSON.stringify({ type: "session_join", passthrough: false }));
// Swap to a reference face (base64, no data: prefix)…
ws.send(JSON.stringify({
type: "set_image",
image_data: faceBase64,
prompt: "Keep lighting and background unchanged",
enhance_prompt: true,
}));
// …or drive the swap from a text prompt only:
// ws.send(JSON.stringify({ type: "prompt", prompt: "…", enhance_prompt: true }));
};import { Room, RoomEvent } from "livekit-client";
ws.onmessage = async (e) => {
const msg = JSON.parse(e.data);
if (msg.type !== "room_info") return;
const room = new Room({ adaptiveStream: false, dynacast: false });
// Register BEFORE connect — the swapped track can arrive immediately.
room.on(RoomEvent.TrackSubscribed, (track) => {
if (track.kind === "video") track.attach(outputVideoEl); // the swapped stream
});
await room.connect(msg.livekit_url, msg.token);
// Publish your camera; the swapped video comes back as the remote track.
const cam = await navigator.mediaDevices.getUserMedia({
video: { width: 512, height: 512, frameRate: 25 },
});
await room.localParticipant.publishTrack(cam.getVideoTracks()[0]);
};cost_usd in the response is the maximum (the full block); you must have that much available to start, but you only pay for the seconds you use. The stream hard-stops at max_duration_sec, so you never overrun. Start another session to continue. The livekit_url points at standard LiveKit media infrastructure; your client connects to it directly for low-latency video.Balance
Check your prepaid balance and lifetime spend programmatically — handy for dashboards, low-balance alerts, or gating your own usage.
/balancecurl https://api.vectorclone.com/api/v1/balance \
-H "Authorization: Bearer $VECTORCLONE_API_KEY"{
"balance_usd": 142.50,
"total_spent_usd": 57.50,
"total_topped_up_usd": 200.00,
"spent_this_week_usd": 8.20,
"spent_this_month_usd": 31.40,
"spent_this_year_usd": 57.50
}spent_this_week_usd, spent_this_month_usd and spent_this_year_usd are convenience windows (calendar week starting Monday, month, and year, all UTC). total_spent_usd is lifetime. For any custom range, use GET /usage/summary below.
Usage
Your recent API calls and what each was charged, newest first.
/usage| Query | Type | Description |
|---|---|---|
| limit | int | How many rows to return (default 100, max 500). |
| from | ISO 8601 | Optional. Only calls at or after this time (e.g. 2026-07-01T00:00:00Z). |
| to | ISO 8601 | Optional. Only calls before this time. |
curl "https://api.vectorclone.com/api/v1/usage?limit=50" \
-H "Authorization: Bearer $VECTORCLONE_API_KEY"[
{
"id": "b2f1c0de-...-77",
"endpoint": "avatar",
"feature": "avatar",
"status": "succeeded",
"charged_usd": 0.42,
"created_at": "2026-07-12T17:55:03Z"
}
]Usage summary
Accurate call counts and spend for your whole account — or a date range — computed from your full history, not a sum of the recent /usagelog. Totals stay correct no matter how many calls you've made. Use this for spend dashboards and budgets.
/usage/summary| Query | Type | Description |
|---|---|---|
| from | ISO 8601 | Optional. Start of the range (inclusive). |
| to | ISO 8601 | Optional. End of the range (exclusive). Omit both for all-time. |
curl "https://api.vectorclone.com/api/v1/usage/summary?from=2026-07-01T00:00:00Z" \
-H "Authorization: Bearer $VECTORCLONE_API_KEY"{
"total_calls": 945,
"succeeded": 916,
"failed": 29,
"total_spent_usd": 1362.38,
"per_feature": [
{ "feature": "real_time_swap_full", "calls": 200, "spend_usd": 327.04 },
{ "feature": "avatar", "calls": 512, "spend_usd": 803.11 }
]
}Changelog
What's changed in the API & docs. Last updated 2026-07-25.
- New GET /v1/usage/summary — accurate total calls, succeeded/failed, total spend, and a per-capability breakdown for any date range (?from=&to=). Use this instead of summing the capped /usage log, which only returns the most recent rows.
- GET /v1/balance now also returns spent_this_week_usd, spent_this_month_usd and spent_this_year_usd (alongside the lifetime total_spent_usd) so you can monitor spend per period in one call.
- GET /v1/usage now accepts ?from= and ?to= to scope the returned call log to a date range.
Pricing
Prices are pay-as-you-go and charged from your prepaid balance. The table below is live — it reads directly from GET /v1/pricing (public, no auth), so it always reflects current pricing. A per-unit price and a per-call minimum apply.
| Capability | Unit | Price / unit | Min charge |
|---|---|---|---|
| Loading live pricing… | |||
per_second capabilities bill by output duration; per_generation bill a flat rate per successful call. You are only ever charged when a job succeeds.
Limits & formats
| Input | Limit |
|---|---|
| Image upload | Up to 45 MB |
| Audio upload | Up to 45 MB |
| Video upload | Up to 500 MB |
| Output resolution | 480p, 720p, or 1080p |
| Avatar script | Up to 5,000 characters |
| Reimagine prompt | Up to 2,000 characters |
| Reimagine images | 1–4 per call |
Ready to build?
Create a key, fund your balance, and ship your first render today.
