Lensora Studio is a two-call orchestrator that wraps four PixelAPI primitives behind a single guided flow:
turntable, dolly, or cinematic.You make two HTTP calls. The first returns object proposals in < 2 s. The second runs the full pipeline in the background and you poll for the result.
| Endpoint | Credits | USD | Refund on failure |
|---|---|---|---|
/v1/studio/init | 5 | $0.005 | n/a (charged on success only) |
/v1/studio/transform | 75 | $0.075 | Yes — auto-refunded if any stage fails or times out |
| End-to-end | 80 | $0.08 |
No subscription required. Pay-per-use. Median end-to-end runtime is ~3-4 minutes (object detection ≈ 1 s, the rest is parallel background-replace + 3D + render).
POST /v1/studio/init — upload + detectUpload a photo. Get back object proposals plus a session_id you'll use for the transform call.
| Field | Type | Required | Description |
|---|---|---|---|
image | file | required | JPEG / PNG / WebP. Max 20 MB. |
max_objects | int | optional | Cap the number of returned proposals. Range 2-8, default 6. |
curl -X POST https://api.pixelapi.dev/v1/studio/init \
-H "Authorization: Bearer $PIXELAPI_KEY" \
-F "[email protected]" \
-F "max_objects=6"
{
"session_id": "2a91884c-1bb5-44e3-a6e0-7db0a5a56668",
"source_url": "https://api.pixelapi.dev/uploads/3f27a1...jpg",
"objects": [
{
"label": "vintage twin-lens reflex camera",
"category": "product",
"bbox": [0.18, 0.12, 0.79, 0.93]
},
{
"label": "entire image (no crop)",
"category": "full_frame",
"bbox": [0.0, 0.0, 1.0, 1.0]
}
],
"suggested_actions": [],
"credits_used": 5,
"vlm_elapsed_ms": 842
}
bbox is normalized [x1, y1, x2, y2] in 0-1 range, top-left origin.suggested_actions will recommend the /v1/video-tryon workflow.POST /v1/studio/transform — run the pipelinePick the object, choose a background, choose a camera. Returns a job_id immediately; the job runs in the background and you poll for status.
| Field | Type | Required | Description |
|---|---|---|---|
session_id | string | required | From /init. Sessions live 24h. |
object_index | int | optional | Index into the objects array. Default 0. |
background | object | required | See background modes below. |
camera_preset | string | optional | turntable · dolly · cinematic. Default turntable. |
background.type | Other fields | What you get |
|---|---|---|
"remove" | none | Transparent background. The MP4 renders the subject on a clean white plate. |
"image" | url (public) | Your own backplate dropped in as the scene. |
"prompt" | prompt (string) | We generate a subject-free scene from your description and use it as the backplate. |
curl -X POST https://api.pixelapi.dev/v1/studio/transform \
-H "Authorization: Bearer $PIXELAPI_KEY" \
-H "Content-Type: application/json" \
-d '{
"session_id": "2a91884c-1bb5-44e3-a6e0-7db0a5a56668",
"object_index": 0,
"background": {
"type": "prompt",
"prompt": "on a marble countertop with soft natural light"
},
"camera_preset": "turntable"
}'
{
"job_id": "e76d4dfc-8e0a-4e1b-a5a2-4f9c8a14e2b3",
"status": "queued",
"credits_used": 75,
"estimated_seconds": 240,
"poll_url": "/v1/studio/result/e76d4dfc-8e0a-4e1b-a5a2-4f9c8a14e2b3"
}
GET /v1/studio/result/{job_id} — poll statusPoll every 3-5 seconds until status is completed or failed. Credits are refunded automatically on failure.
{
"job_id": "e76d4dfc-8e0a-4e1b-a5a2-4f9c8a14e2b3",
"status": "processing",
"step": "generating-3d",
"progress": 60,
"scene_url": "https://api.pixelapi.dev/outputs/files/edits/.../scene.jpg"
}
The step field walks through these stages: starting → cropping → removing-bg → generating-bg → compositing → generating-3d → rendering-video → done.
{
"job_id": "e76d4dfc-8e0a-4e1b-a5a2-4f9c8a14e2b3",
"status": "completed",
"step": "done",
"progress": 100,
"output": {
"mp4_url": "https://api.pixelapi.dev/dl/3d/.../turntable.mp4",
"glb_url": "https://api.pixelapi.dev/dl/3d/.../model.glb",
"scene_url": "https://api.pixelapi.dev/outputs/files/edits/.../scene.jpg",
"removed_bg_url": "https://api.pixelapi.dev/outputs/files/edits/.../cutout.png"
}
}
Four downloadable artifacts every job produces:
| Field | What it is | Use case |
|---|---|---|
mp4_url | The hero deliverable. 24 fps, 4 s, 768×768. | Drop into product pages, ads, social. |
glb_url | The reconstructed 3D mesh with production-ready textures. | AR previews, Blender/Unity/Unreal/Three.js. |
scene_url | The composited still — subject + new background. | Static product photo, thumbnails. |
removed_bg_url | Subject cutout with transparent alpha. | Re-use in your own compositing. |
{
"job_id": "e76d4dfc-...",
"status": "failed",
"error": "3D generation timeout",
"progress": 0
}
When status == "failed", the 75 transform credits are automatically refunded to your account before the response is returned. The 5 /init credits are not refunded because object detection always succeeds on its own.
Each preset is a 4-second 24 fps render. Click play to compare:
Auto-orient is on by default. Before rendering, the model is auto-rotated so its largest face points square to the camera at angle 0. That means dolly never goes edge-on, and turntable never opens on a thin sliver, regardless of how the source photo was framed.
import os, time, requests
API = "https://api.pixelapi.dev"
KEY = os.environ["PIXELAPI_KEY"]
H = {"Authorization": f"Bearer {KEY}"}
# 1. INIT
with open("rolleiflex.jpg", "rb") as f:
init = requests.post(f"{API}/v1/studio/init",
headers=H, files={"image": f}).json()
print("session:", init["session_id"])
print("objects:")
for i, o in enumerate(init["objects"]):
print(f" [{i}] {o['label']} ({o['category']})")
# 2. TRANSFORM
job = requests.post(f"{API}/v1/studio/transform",
headers={**H, "Content-Type": "application/json"},
json={
"session_id": init["session_id"],
"object_index": 0,
"background": {
"type": "prompt",
"prompt": "on a marble countertop with soft natural light",
},
"camera_preset": "cinematic",
}).json()
print("job:", job["job_id"], "ETA:", job["estimated_seconds"], "s")
# 3. POLL
while True:
s = requests.get(f"{API}/v1/studio/result/{job['job_id']}",
headers=H).json()
print(f" {s['status']:10} step={s.get('step','-'):20} pct={s.get('progress',0)}%")
if s["status"] == "completed":
out = s["output"]
print("MP4:", out["mp4_url"])
print("GLB:", out["glb_url"])
# Download the MP4
with open("output.mp4", "wb") as f:
f.write(requests.get(out["mp4_url"]).content)
break
if s["status"] == "failed":
print("FAILED:", s.get("error"))
break
time.sleep(4)
import fs from "fs";
import FormData from "form-data";
import axios from "axios";
const API = "https://api.pixelapi.dev";
const H = { Authorization: `Bearer ${process.env.PIXELAPI_KEY}` };
// 1. INIT
const form = new FormData();
form.append("image", fs.createReadStream("rolleiflex.jpg"));
form.append("max_objects", "6");
const init = (await axios.post(`${API}/v1/studio/init`, form, {
headers: { ...H, ...form.getHeaders() },
})).data;
// 2. TRANSFORM
const job = (await axios.post(`${API}/v1/studio/transform`, {
session_id: init.session_id,
object_index: 0,
background: { type: "prompt", prompt: "on a marble countertop with soft natural light" },
camera_preset: "cinematic",
}, { headers: { ...H, "Content-Type": "application/json" } })).data;
// 3. POLL
while (true) {
const s = (await axios.get(`${API}/v1/studio/result/${job.job_id}`, { headers: H })).data;
console.log(`${s.status} step=${s.step} pct=${s.progress}`);
if (s.status === "completed") { console.log("MP4:", s.output.mp4_url); break; }
if (s.status === "failed") { console.log("FAILED:", s.error); break; }
await new Promise(r => setTimeout(r, 4000));
}
| HTTP | Meaning | What to do |
|---|---|---|
| 400 | Invalid image, bad background.type, object_index out of range, or bbox crop too small. | Re-check inputs. |
| 401 | Missing / invalid API key. | Pass Authorization: Bearer <key>. |
| 402 | Insufficient credits (need 5 for init, 75 for transform). | Top up at pixelapi.dev/billing. |
| 404 | Session expired (24h TTL) or job not found. | Restart with a fresh /init call. |
| 503 | Object detection temporarily unavailable. | Retry after a few seconds — no credits charged. |