# PlayVeo API — complete reference > Generated from https://playveo.online/docs on 2026-09-30. Base URL: https://playveo-api.aboessa101.workers.dev. Authenticate every request with `Authorization: Bearer pv_...` (create a key at https://playveo.online/dashboard/api-keys). Generation is asynchronous: create a job, then poll it until `status` is `completed` (or `failed`). ## Contents - Getting Started: Overview, Quickstart, Authentication, Jobs & Polling, Credits & Pricing, Rate Limits - Video: Text to Video (POST), Image to Video (POST), Frames to Video (POST), Components to Video (POST), Get Video (GET), List Videos (GET) - Images: Text to Image (POST), Image to Image (POST), Remove Background (POST), Bulk Images (POST), Get Image (GET), List Images (GET), Delete Image (DELETE) - Characters: Create Character (POST), List Characters (GET), Get Character (GET), Delete Character (DELETE) - Reference: Models & Ratios, Image Optimization, Webhooks, Errors, Recipes --- # Getting Started # PlayVeo API Generate AI videos and images programmatically using Google Veo 3.1 and Imagen 4 through a simple REST API. - 8 sec Video length - ~3 min Generation time - 1080p Resolution ### Base URL https://playveo-api.aboessa101.workers.dev ### Quick steps - 01 Create account Sign up at playveo.online. - 02 Get API key Dashboard → API Keys. - 03 Make a request Pass the key in Authorization header. - 04 Poll for result GET the job ID until status = completed. **Examples** **Generate a video** ```bash curl -X POST https://playveo-api.aboessa101.workers.dev/v1/videos \ -H "Authorization: Bearer pv_your_key" \ -H "Content-Type: application/json" \ -d '{ "prompt": "A cat playing piano in a jazz club", "aspect_ratio": "16:9" }' ``` **Generate images** ```bash curl -X POST https://playveo-api.aboessa101.workers.dev/v1/images/text-to-image \ -H "Authorization: Bearer pv_your_key" \ -H "Content-Type: application/json" \ -d '{ "prompt": "A futuristic city at night", "aspect_ratio": "16:9", "count": 2 }' ``` **Poll for result** ```bash curl https://playveo-api.aboessa101.workers.dev/v1/videos/vid_abc123 \ -H "Authorization: Bearer pv_your_key" # → { "video": { "status": "completed", "videoUrl": "..." } } ``` # Quickstart From zero to a finished video. Every generation endpoint works the same way: you **submit** a job, get an `id` back immediately, then **poll** that id until it is done. ### 1. Get a key [Dashboard → API Keys](https://playveo.online/dashboard/api-keys) → Create key. It looks like `pv_…` and is shown once. Copy the whole string. > A **truncated key** is the most common cause of `401 Unauthorized`. A valid key is **35 characters**: the `pv_` prefix plus 32 hex characters. Check the length before assuming the key is invalid. ### 2. Submit a job The response arrives in well under a second with `status: "pending"`. Nothing has been generated yet — credits are reserved at this point. ### 3. Poll until finished Poll every **5–10 seconds**. Images typically finish in **30–60s**, videos in **1.5–5 minutes**. Stop when `status` becomes `completed` or `failed`. > Prefer not to poll at all? Pass `webhook_url` and we POST the finished job to you. See **Webhooks**. ### 4. Download the result `videoUrl` / `resultUrls` are plain HTTPS URLs you can stream or download with no auth header. They are kept for **10 days** — copy anything you want to keep to your own storage. **Examples** **Full example: submit + poll + download** _Node.js_ ```javascript const KEY = process.env.PLAYVEO_API_KEY; const BASE = 'https://playveo-api.aboessa101.workers.dev'; async function generateVideo(prompt) { // 1. Submit const res = await fetch(BASE + '/v1/videos', { method: 'POST', headers: { 'Authorization': 'Bearer ' + KEY, 'Content-Type': 'application/json', }, body: JSON.stringify({ prompt, aspect_ratio: '16:9', model: 'veo3_lite', duration: 8, }), }); if (!res.ok) throw new Error(await res.text()); const { id } = await res.json(); console.log('job', id); // 2. Poll (every 10s, give up after 10 min) const deadline = Date.now() + 10 * 60 * 1000; while (Date.now() < deadline) { await new Promise(r => setTimeout(r, 10_000)); const p = await fetch(BASE + '/v1/videos/' + id, { headers: { 'Authorization': 'Bearer ' + KEY }, }); const { video } = await p.json(); console.log(video.status); if (video.status === 'completed') return video.videoUrl; if (video.status === 'failed') throw new Error(video.error); } throw new Error('timed out'); } const url = await generateVideo('a red bicycle on a quiet street'); console.log(url); ``` _Python_ ```python import os, time, requests KEY = os.environ["PLAYVEO_API_KEY"] BASE = "https://playveo-api.aboessa101.workers.dev" H = {"Authorization": f"Bearer {KEY}"} def generate_video(prompt: str) -> str: # 1. Submit r = requests.post( f"{BASE}/v1/videos", headers={**H, "Content-Type": "application/json"}, json={ "prompt": prompt, "aspect_ratio": "16:9", "model": "veo3_lite", "duration": 8, }, timeout=30, ) r.raise_for_status() job_id = r.json()["id"] print("job", job_id) # 2. Poll (every 10s, give up after 10 min) deadline = time.time() + 600 while time.time() < deadline: time.sleep(10) v = requests.get(f"{BASE}/v1/videos/{job_id}", headers=H, timeout=30).json()["video"] print(v["status"]) if v["status"] == "completed": return v["videoUrl"] if v["status"] == "failed": raise RuntimeError(v.get("error")) raise TimeoutError("timed out") url = generate_video("a red bicycle on a quiet street") print(url) # 3. Download open("out.mp4", "wb").write(requests.get(url, timeout=300).content) ``` _cURL_ ```bash # 1. Submit — returns the job id curl -sX POST https://playveo-api.aboessa101.workers.dev/v1/videos \ -H "Authorization: Bearer $PLAYVEO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "prompt": "a red bicycle on a quiet street", "aspect_ratio": "16:9", "model": "veo3_lite", "duration": 8 }' # → {"id":"c0d77498-...","status":"pending","creditCost":0.4} # 2. Poll until status is completed curl -s https://playveo-api.aboessa101.workers.dev/v1/videos/c0d77498-... \ -H "Authorization: Bearer $PLAYVEO_API_KEY" # → {"video":{"status":"processing","videoUrl":null}} # → {"video":{"status":"completed","videoUrl":"https://..."}} # 3. Download (no auth header needed) curl -o out.mp4 "https://.../worker/serve/c0d77498-....mp4" ``` # Authentication All requests require a bearer token in the `Authorization` header. Format `Authorization: Bearer pv_ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` > **Never expose your API key** in client-side code, public repos, or logs. Use environment variables. > Rotate keys at any time from [Dashboard → API Keys](https://playveo.online/dashboard/api-keys). **Examples** **Request header** ```http Authorization: Bearer pv_your_api_key_here ``` **cURL example** ```bash curl https://playveo-api.aboessa101.workers.dev/v1/videos \ -H "Authorization: Bearer pv_your_key" ``` **401 unauthorized** ```json { "error": "Invalid API key" } ``` # Jobs & Polling Generation is asynchronous. One endpoint is the exception: **Remove Background** returns the finished image in the same response. ### Statuses ### How long to expect - Type - Typical - Poll every > Do not poll faster than once per second, and always set an overall timeout. If a job is still `processing` after **15 minutes**, treat it as stuck and contact support with the job id. ### Result retention Generated videos and images are stored for **10 days**, then deleted. Result URLs are public and unauthenticated — treat them as unguessable, not secret — and mirror anything you need long-term into your own bucket. **Examples** **Polling with backoff** _Node.js_ ```javascript async function waitForJob(kind, id, key, { intervalMs = 5000, maxMs = 15 * 60 * 1000, } = {}) { const field = kind === 'videos' ? 'video' : 'image'; const deadline = Date.now() + maxMs; let wait = intervalMs; while (Date.now() < deadline) { await new Promise(r => setTimeout(r, wait)); const res = await fetch( `https://playveo-api.aboessa101.workers.dev/v1/${kind}/${id}`, { headers: { Authorization: 'Bearer ' + key } }, ); // Transient 5xx: keep waiting instead of failing the job. if (res.status >= 500) { wait = Math.min(wait * 2, 30_000); continue; } if (!res.ok) throw new Error(await res.text()); const job = (await res.json())[field]; if (job.status === 'completed') return job; if (job.status === 'failed') throw new Error(job.error || 'failed'); wait = intervalMs; // healthy response: back to base interval } throw new Error('job ' + id + ' timed out'); } ``` _Python_ ```python import time, requests def wait_for_job(kind, job_id, key, interval=5, max_seconds=900): field = "video" if kind == "videos" else "image" headers = {"Authorization": f"Bearer {key}"} deadline = time.time() + max_seconds wait = interval while time.time() < deadline: time.sleep(wait) r = requests.get(f"https://playveo-api.aboessa101.workers.dev/v1/{kind}/{job_id}", headers=headers, timeout=30) # Transient 5xx: keep waiting instead of failing the job. if r.status_code >= 500: wait = min(wait * 2, 30) continue r.raise_for_status() job = r.json()[field] if job["status"] == "completed": return job if job["status"] == "failed": raise RuntimeError(job.get("error") or "failed") wait = interval raise TimeoutError(f"job {job_id} timed out") ``` **Completed video** ```json { "video": { "id": "c0d77498-9efc-4d79-9217-01d9d232ddd2", "prompt": "a red bicycle on a quiet street", "aspectRatio": "16:9", "status": "completed", "videoUrl": "https://.../worker/serve/c0d77498.mp4", "error": null, "resolution": "720p", "createdAt": "2026-08-10T23:16:40.020Z", "completedAt": "2026-08-10T23:18:04.017Z" } } ``` # Credits & Pricing Every request that generates something costs credits. The exact charge for a request is always returned as `creditCost` in the submit response, so you never have to guess. ### Cost per request - Operation - Credits - Notes ### When you are charged Credits are deducted when the job is **accepted**, not when it finishes. Reusing a character or attaching a reference image costs nothing extra. > **Failed jobs are refunded automatically.** When a job ends up `failed` — or finishes with no result — the full `creditCost` is credited straight back. Applies to videos, images and characters. > The refund is triggered by the failure being reported. A job that is stuck in `processing` has not failed yet, so it has not been refunded yet — send us the job id if it has been over 15 minutes. > Not enough balance returns `402` with the exact shortfall, and the job is never created — so a 402 never costs you anything. ### Duration and price Video price scales linearly with `duration`, so the length you choose is the main thing driving cost. An unsupported value is rejected with `400` before anything is charged. - Endpoint - Allowed duration > **Why only 8s for the image-based modes?** Those are generated through a pipeline that produces a fixed 8 second clip, so a shorter value could not be honored. Rather than accept it, ignore it, and still bill you per requested second, the API rejects it with a `400` explaining the constraint. Omit `duration` and you get the correct default. **Examples** **creditCost in the response** ```json { "id": "c0d77498-9efc-4d79-9217-01d9d232ddd2", "status": "pending", "creditCost": 0.4, "estimated_time": 180 } ``` **Out of credits (402)** ```json { "error": "Not enough credits. Need 0.4, have 0.15" } ``` **Check the cost without committing** ```bash # duration drives the price: 4s is half of 8s curl -sX POST https://playveo-api.aboessa101.workers.dev/v1/videos \ -H "Authorization: Bearer $PLAYVEO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"prompt":"...","model":"veo3_lite","duration":4}' # → {"creditCost":0.2} ``` # Rate Limits Hourly caps apply to generation from the **web dashboard**, counted per account per rolling hour and set by your plan. Exceeding one returns `429`. > **API key traffic is not hourly-capped today.** Requests authenticated with a `pv_` key skip the hourly counter entirely — your real limits are your credit balance and the concurrency of our generation fleet. Build in retry-on-429 anyway, so a future change cannot break your integration. ### What actually throttles you The `429` body always states the limit that was hit and `retryAfter` in seconds, so read it rather than hardcoding numbers. **Examples** **429 response** ```json { "error": "Rate limit exceeded. You can create 20 videos per hour. Try again later.", "retryAfter": 3600 } ``` **Handling it** _Node.js_ ```javascript async function submit(body, key, attempt = 0) { const res = await fetch('https://playveo-api.aboessa101.workers.dev/v1/videos', { method: 'POST', headers: { Authorization: 'Bearer ' + key, 'Content-Type': 'application/json', }, body: JSON.stringify(body), }); if (res.status === 429 && attempt < 3) { const { retryAfter } = await res.json().catch(() => ({})); const waitMs = (retryAfter ?? 60) * 1000; await new Promise(r => setTimeout(r, waitMs)); return submit(body, key, attempt + 1); } if (!res.ok) throw new Error(await res.text()); return res.json(); } ``` _Python_ ```python import time, requests def submit(body, key, attempts=3): url = "https://playveo-api.aboessa101.workers.dev/v1/videos" headers = {"Authorization": f"Bearer {key}", "Content-Type": "application/json"} for attempt in range(attempts): r = requests.post(url, headers=headers, json=body, timeout=30) if r.status_code == 429: wait = r.json().get("retryAfter", 60) time.sleep(wait) continue r.raise_for_status() return r.json() raise RuntimeError("rate limited after retries") ``` --- # Video # Text to Video Generate an 8-second AI video from a text description using Google Veo 3.1. `POST /v1/videos` **Parameters** | Name | Type | Required | Default | Allowed | Description | |---|---|---|---|---|---| | `prompt` | string | yes | | | Text description of the video to generate. | | `model` | string | no | `omni_flash` | `omni_flash`, `veo3_fast`, `veo3_quality`, `veo3_lite` | Video model. See Models reference. | | `duration_seconds` | number | no | `8` | | Clip length. Omni Flash: 4/6/8/10s. Veo models: 4/6/8s. | | `aspect_ratio` | string | no | `16:9` | `16:9`, `9:16` | Video aspect ratio. | | `resolution` | string | no | `720p` | `720p`, `1080p` | Output resolution. 1080p delivers a 1920×1080 (or 1080×1920) upscale of the rendered clip and takes about a minute longer. Same credit cost. Any other value is rejected with 400. | | `quality` | string | no | `fast` | | Legacy preset, used only if model is omitted. | | `characters` | string[] | no | | | Names of your characters to include (up to 7). Each must already be ready. See Characters. | | `webhook_url` | string | no | | | URL to POST when job completes. | > Poll `GET /v1/videos/:id` every 10 s until `status: "completed"`. **Examples** _cURL_ ```bash curl -X POST https://playveo-api.aboessa101.workers.dev/v1/videos \ -H "Authorization: Bearer $PLAYVEO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "prompt": "A golden retriever running on a beach at sunset, cinematic slow motion", "aspect_ratio": "16:9", "model": "omni_flash", "duration": 8, "resolution": "1080p" }' ``` _Node.js_ ```javascript const res = await fetch('https://playveo-api.aboessa101.workers.dev/v1/videos', { method: 'POST', headers: { Authorization: 'Bearer ' + process.env.PLAYVEO_API_KEY, 'Content-Type': 'application/json', }, body: JSON.stringify({ prompt: 'A golden retriever running on a beach at sunset, cinematic slow motion', aspect_ratio: '16:9', model: 'omni_flash', duration: 8, // 4 | 6 | 8 (omni_flash also allows 10) resolution: '1080p', // '720p' (default) | '1080p' }), }); if (!res.ok) throw new Error(await res.text()); const { id, creditCost } = await res.json(); console.log(id, creditCost); // then poll GET /v1/videos/:id ``` _Python_ ```python import os, requests r = requests.post( "https://playveo-api.aboessa101.workers.dev/v1/videos", headers={ "Authorization": "Bearer " + os.environ["PLAYVEO_API_KEY"], "Content-Type": "application/json", }, json={ "prompt": "A golden retriever running on a beach at sunset, cinematic slow motion", "aspect_ratio": "16:9", "model": "omni_flash", "duration": 8, # 4 | 6 | 8 (omni_flash also allows 10) "resolution": "1080p", # "720p" (default) | "1080p" }, timeout=30, ) r.raise_for_status() job = r.json() print(job["id"], job["creditCost"]) # then poll GET /v1/videos/:id ``` _Response_ ```json { "id": "c0d77498-9efc-4d79-9217-01d9d232ddd2", "status": "pending", "resolution": "1080p", "creditCost": 0.4, "estimated_time": 180 } ``` # Image to Video Animate a reference image into an 8-second video. `POST /v1/videos/image-to-video` **Parameters** | Name | Type | Required | Default | Allowed | Description | |---|---|---|---|---|---| | `image` | string | yes | | | Base64 data URL (data:image/png;base64,...). | | `optimize_reference` | boolean | no | `true` | | Downscale/re-encode your image before it is handed to the generator. Keep it on unless you specifically need the original pixels — see Image Optimization. | | `prompt` | string | yes | | | How to animate the image. | | `model` | string | no | `omni_flash` | `omni_flash`, `veo3_fast`, `veo3_quality`, `veo3_lite` | Video model. | | `duration_seconds` | number | no | `8` | `8` | This mode always produces 8 second clips. Any other value is rejected with 400 rather than silently ignored — see Credits & Pricing. | | `aspect_ratio` | string | no | `16:9` | `16:9`, `9:16` | Video aspect ratio. | | `resolution` | string | no | `720p` | `720p`, `1080p` | Output resolution. 1080p delivers a 1920×1080 (or 1080×1920) upscale of the rendered clip and takes about a minute longer. Same credit cost. Any other value is rejected with 400. | | `quality` | string | no | `fast` | | Legacy preset, used only if model is omitted. | | `characters` | string[] | no | | | Names of your characters to include (up to 7). Each must already be ready. See Characters. | | `webhook_url` | string | no | | | Completion callback URL. | **Examples** _Node.js_ ```javascript import { readFile } from 'node:fs/promises'; // The image must be sent as a base64 data URL, not a link. const bytes = await readFile('portrait.jpg'); const dataUrl = 'data:image/jpeg;base64,' + bytes.toString('base64'); const res = await fetch('https://playveo-api.aboessa101.workers.dev/v1/videos/image-to-video', { method: 'POST', headers: { Authorization: 'Bearer ' + process.env.PLAYVEO_API_KEY, 'Content-Type': 'application/json', }, body: JSON.stringify({ image: dataUrl, prompt: 'the person starts walking forward, handheld camera', aspect_ratio: '16:9', model: 'veo3_lite', // Veo models render image modes at a fixed 8s; use model "omni_flash" for 4-10s. // duration_seconds: 4, }), }); if (!res.ok) throw new Error(await res.text()); const { id } = await res.json(); // then poll GET /v1/videos/:id ``` _Python_ ```python import base64, os, requests # The image must be sent as a base64 data URL, not a link. with open("portrait.jpg", "rb") as fh: data_url = "data:image/jpeg;base64," + base64.b64encode(fh.read()).decode() r = requests.post( "https://playveo-api.aboessa101.workers.dev/v1/videos/image-to-video", headers={ "Authorization": "Bearer " + os.environ["PLAYVEO_API_KEY"], "Content-Type": "application/json", }, json={ "image": data_url, "prompt": "the person starts walking forward, handheld camera", "aspect_ratio": "16:9", "model": "veo3_lite", # Veo models render image modes at a fixed 8s; use "omni_flash" for 4-10s. # "duration_seconds": 4, }, timeout=60, ) r.raise_for_status() print(r.json()["id"]) # then poll GET /v1/videos/:id ``` _cURL_ ```bash # Build the data URL from a local file, then post it. IMG="data:image/jpeg;base64,$(base64 -w0 portrait.jpg)" curl -X POST https://playveo-api.aboessa101.workers.dev/v1/videos/image-to-video \ -H "Authorization: Bearer $PLAYVEO_API_KEY" \ -H "Content-Type: application/json" \ -d "{ \"image\": \"$IMG\", \"prompt\": \"the person starts walking forward\", \"aspect_ratio\": \"16:9\", \"model\": \"veo3_lite\" }" ``` _Response_ ```json { "id": "b3179bd6-1d39-4256-8de8-0625af62b9b3", "status": "pending", "mode": "image-to-video", "imageUrl": "https://.../images/b3179bd6.png", "creditCost": 0.4, "estimated_time": 180 } ``` # Frames to Video Generate a video that transitions between a first and last frame. Perfect for before/after effects. `POST /v1/videos/frames-to-video` **Parameters** | Name | Type | Required | Default | Allowed | Description | |---|---|---|---|---|---| | `firstImage` | string | yes | | | Base64 data URL of the first frame. | | `lastImage` | string | yes | | | Base64 data URL of the last frame. | | `optimize_reference` | boolean | no | `true` | | Downscale/re-encode both frames before they are handed to the generator. Applies to the pair. See Image Optimization. | | `prompt` | string | yes | | | Description of the transition. | | `model` | string | no | `omni_flash` | `omni_flash`, `veo3_fast`, `veo3_quality`, `veo3_lite` | Video model. | | `duration_seconds` | number | no | `8` | `8` | This mode always produces 8 second clips. Any other value is rejected with 400 rather than silently ignored — see Credits & Pricing. | | `aspect_ratio` | string | no | `16:9` | `16:9`, `9:16` | Video aspect ratio. | | `resolution` | string | no | `720p` | `720p`, `1080p` | Output resolution. 1080p delivers a 1920×1080 (or 1080×1920) upscale of the rendered clip and takes about a minute longer. Same credit cost. Any other value is rejected with 400. | | `quality` | string | no | `fast` | | Legacy preset, used only if model is omitted. | | `characters` | string[] | no | | | Names of your characters to include (up to 7). Each must already be ready. See Characters. | | `webhook_url` | string | no | | | Completion callback URL. | > Charged at **1.5×** the standard rate due to dual-image processing. **Examples** _Request_ ```bash curl -X POST https://playveo-api.aboessa101.workers.dev/v1/videos/frames-to-video \ -H "Authorization: Bearer pv_your_key" \ -H "Content-Type: application/json" \ -d '{ "firstImage": "data:image/png;base64,...", "lastImage": "data:image/png;base64,...", "prompt": "Smooth transition from day to night", "aspect_ratio": "16:9" }' ``` _Response_ ```json { "id": "vid_abc123xyz", "status": "pending", "creditCost": 0.96, "estimated_time": 180 } ``` # Components to Video Generate a video from 1–10 reference images. Ideal for product showcases. `POST /v1/videos/components-to-video` **Parameters** | Name | Type | Required | Default | Allowed | Description | |---|---|---|---|---|---| | `images` | string[] | yes | | | Ordered base64 data URLs — up to 3 reference images. | | `optimize_reference` | boolean | no | `true` | | Downscale/re-encode every reference image before it is handed to the generator. See Image Optimization. | | `prompt` | string | yes | | | How to combine the images into a video. | | `model` | string | no | `omni_flash` | `omni_flash`, `veo3_fast` | Ingredients models only. | | `duration_seconds` | number | no | `8` | `8` | This mode always produces 8 second clips. Any other value is rejected with 400 rather than silently ignored — see Credits & Pricing. | | `aspect_ratio` | string | no | `16:9` | `16:9`, `9:16` | Video aspect ratio. | | `resolution` | string | no | `720p` | `720p`, `1080p` | Output resolution. 1080p delivers a 1920×1080 (or 1080×1920) upscale of the rendered clip and takes about a minute longer. Same credit cost. Any other value is rejected with 400. | | `quality` | string | no | `fast` | | Legacy preset, used only if model is omitted. | | `characters` | string[] | no | | | Names of your characters to include (up to 7). Each must already be ready. See Characters. | | `webhook_url` | string | no | | | Completion callback URL. | > Pricing: base cost + 0.2× per additional image beyond the first. **Examples** _Request_ ```bash curl -X POST https://playveo-api.aboessa101.workers.dev/v1/videos/components-to-video \ -H "Authorization: Bearer pv_your_key" \ -H "Content-Type: application/json" \ -d '{ "images": [ "data:image/png;base64,...", "data:image/png;base64,...", "data:image/png;base64,..." ], "prompt": "Showcase all product angles with smooth transitions" }' ``` _Response_ ```json { "id": "vid_abc123xyz", "status": "pending", "mode": "components-to-video", "imageCount": 3, "creditCost": 0.90, "estimated_time": 180 } ``` # Get Video Retrieve the status and download URL of a video job. `GET /v1/videos/:id` **Path Parameters** | Name | Type | Required | Default | Allowed | Description | |---|---|---|---|---|---| | `id` | string | yes | | | Video job ID returned on creation. | Status Values - pending Queued, waiting for a worker - processing AI is generating the video - completed Done — videoUrl is available - failed Failed — credits auto-refunded > Videos stored for **10 days** then auto-deleted. **Examples** **Request** ```bash curl https://playveo-api.aboessa101.workers.dev/v1/videos/vid_abc123xyz \ -H "Authorization: Bearer pv_your_key" ``` **Response — completed** ```json { "video": { "id": "vid_abc123xyz", "prompt": "A golden retriever running on a beach...", "aspectRatio": "16:9", "status": "completed", "videoUrl": "https://.../worker/serve/vid_abc123xyz.mp4", "resolution": "1080p", "creditCost": 0.40, "createdAt": "2026-04-19T10:00:00Z", "completedAt": "2026-04-19T10:03:00Z" } } ``` # List Videos Get all video jobs for the authenticated user. `GET /v1/videos` **Examples** _Request_ ```bash curl https://playveo-api.aboessa101.workers.dev/v1/videos \ -H "Authorization: Bearer pv_your_key" ``` _Response_ ```json { "videos": [ { "id": "vid_abc123", "prompt": "A golden retriever...", "aspectRatio": "16:9", "status": "completed", "videoUrl": "https://...", "creditCost": 0.40, "createdAt": "2026-04-19T10:00:00Z" } ] } ``` --- # Images # Text to Image Generate 1–4 AI images from a text description. Poll `GET /v1/images/:id` for results. `POST /v1/images/text-to-image` **Parameters** | Name | Type | Required | Default | Allowed | Description | |---|---|---|---|---|---| | `prompt` | string | yes | | | Text description of the image. | | `aspect_ratio` | string | no | `16:9` | `16:9`, `9:16`, `1:1`, `4:3`, `3:4` | Image aspect ratio. | | `model` | string | no | `nano_banana_2` | | Generation model. See Models reference. | | `count` | number | no | `1` | | Images to generate (1–4). | | `characters` | string[] | no | | | Names of your characters to include (up to 7). Each must already be ready. See Characters. | | `optimize` | boolean | no | `false` | | Deliver optimized (compressed / modern-format) result URLs. Adds ?w&q&f=auto to resultUrls, transformed and edge-cached on delivery. | | `optimize_quality` | number | no | `82` | | Output quality 1–100 when optimize is true. | | `optimize_max_width` | number | no | | | Max output width (px) when optimize is true. | | `optimize_format` | string | no | `auto` | `auto`, `webp`, `avif`, `jpeg` | Output format when optimize is true. | | `webhook_url` | string | no | | | Callback URL on completion. | > Images stored for **10 days** then auto-deleted. **Examples** _cURL_ ```bash curl -X POST https://playveo-api.aboessa101.workers.dev/v1/images/text-to-image \ -H "Authorization: Bearer $PLAYVEO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "prompt": "A majestic lion in a savanna at golden hour, photorealistic 8K", "aspect_ratio": "16:9", "model": "nano_banana2", "count": 2 }' ``` _Node.js_ ```javascript const res = await fetch('https://playveo-api.aboessa101.workers.dev/v1/images/text-to-image', { method: 'POST', headers: { Authorization: 'Bearer ' + process.env.PLAYVEO_API_KEY, 'Content-Type': 'application/json', }, body: JSON.stringify({ prompt: 'A majestic lion in a savanna at golden hour, photorealistic 8K', aspect_ratio: '16:9', count: 2, // billed per image: 2 x 0.1 = 0.2 credits }), }); if (!res.ok) throw new Error(await res.text()); const { id } = await res.json(); // Poll until done, then read resultUrls (one entry per image). const image = await waitForJob('images', id, process.env.PLAYVEO_API_KEY); console.log(image.resultUrls); ``` _Python_ ```python import os, requests r = requests.post( "https://playveo-api.aboessa101.workers.dev/v1/images/text-to-image", headers={ "Authorization": "Bearer " + os.environ["PLAYVEO_API_KEY"], "Content-Type": "application/json", }, json={ "prompt": "A majestic lion in a savanna at golden hour, photorealistic 8K", "aspect_ratio": "16:9", "count": 2, # billed per image: 2 x 0.1 = 0.2 credits }, timeout=30, ) r.raise_for_status() job_id = r.json()["id"] # Poll until done, then read resultUrls (one entry per image). image = wait_for_job("images", job_id, os.environ["PLAYVEO_API_KEY"]) print(image["resultUrls"]) ``` _Response_ ```json { "id": "img_abc123xyz", "status": "pending", "mode": "text-to-image", "model": "nano_banana2", "aspectRatio": "16:9", "count": 2, "creditCost": 0.20, "estimated_time": 60 } ``` _Completed_ ```json { "image": { "id": "img_abc123xyz", "status": "completed", "resultUrls": [ "https://.../serve-image/img_abc123xyz-0.png", "https://.../serve-image/img_abc123xyz-1.png" ], "creditCost": 0.20, "completedAt": "2026-04-19T10:01:00Z" } } ``` # Image to Image Transform or enhance an existing image with a text prompt. `POST /v1/images/image-to-image` **Parameters** | Name | Type | Required | Default | Allowed | Description | |---|---|---|---|---|---| | `image` | string | yes | | | Base64 data URL of the source image. | | `prompt` | string | yes | | | How to transform the image. | | `strength` | number | no | `0.7` | | Transform intensity 0.1–1.0. Higher = more change. | | `aspect_ratio` | string | no | `1:1` | `16:9`, `9:16`, `1:1`, `4:3`, `3:4` | Output aspect ratio. | | `model` | string | no | `nano_banana2` | | Image model. | | `count` | number | no | `1` | | Variations to generate (1–4). | | `characters` | string[] | no | | | Names of your characters to include (up to 7). Each must already be ready. See Characters. | | `optimize_reference` | boolean | no | `true` | | Downscale/compress the source image before generation for a faster upload (no quality impact on output). | | `optimize` | boolean | no | `false` | | Deliver optimized result URLs (adds ?w&q&f=auto, edge-cached). | | `optimize_quality` | number | no | `82` | | Output quality 1–100 when optimize is true. | | `optimize_max_width` | number | no | | | Max output width (px) when optimize is true. | | `optimize_format` | string | no | `auto` | `auto`, `webp`, `avif`, `jpeg` | Output format when optimize is true. | | `webhook_url` | string | no | | | Callback URL on completion. | Strength Guide - `0.1–0.3`Subtle Preserves most of the original - `0.4–0.6`Moderate Balanced transformation - `0.7–0.8`Strong Recommended for style transfer ★ - `0.9–1.0`Complete May lose original details **Examples** _cURL_ ```bash curl -X POST https://playveo-api.aboessa101.workers.dev/v1/images/image-to-image \ -H "Authorization: Bearer $PLAYVEO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "image": "data:image/png;base64,...", "prompt": "Transform into anime style with vibrant colors", "strength": 0.8, "model": "nano_banana2", "count": 2 }' ``` _Node.js_ ```javascript import { readFile } from 'node:fs/promises'; const bytes = await readFile('photo.png'); const dataUrl = 'data:image/png;base64,' + bytes.toString('base64'); const res = await fetch('https://playveo-api.aboessa101.workers.dev/v1/images/image-to-image', { method: 'POST', headers: { Authorization: 'Bearer ' + process.env.PLAYVEO_API_KEY, 'Content-Type': 'application/json', }, body: JSON.stringify({ image: dataUrl, prompt: 'transform into anime style with vibrant colors', strength: 0.8, // higher = further from the original count: 2, // billed per image: 2 x 0.15 = 0.3 credits // optimize_reference: false, // only if fine detail must survive }), }); if (!res.ok) throw new Error(await res.text()); const { id } = await res.json(); ``` _Python_ ```python import base64, os, requests with open("photo.png", "rb") as fh: data_url = "data:image/png;base64," + base64.b64encode(fh.read()).decode() r = requests.post( "https://playveo-api.aboessa101.workers.dev/v1/images/image-to-image", headers={ "Authorization": "Bearer " + os.environ["PLAYVEO_API_KEY"], "Content-Type": "application/json", }, json={ "image": data_url, "prompt": "transform into anime style with vibrant colors", "strength": 0.8, # higher = further from the original "count": 2, # billed per image: 2 x 0.15 = 0.3 credits }, timeout=60, ) r.raise_for_status() print(r.json()["id"]) ``` _Response_ ```json { "id": "img_abc123xyz", "status": "pending", "mode": "image-to-image", "strength": 0.8, "count": 2, "creditCost": 0.30, "estimated_time": 90 } ``` # Remove Background Remove the background from any image and get back a transparent PNG. Works on generated or uploaded images. `POST /v1/images/remove-background` > **Usually answers in the same request.** The call waits up to ~25s and normally returns HTTP 200 with `status: "completed"` and a ready `url`. If it is busy it returns **HTTP 202** with `status: "queued"` and a `poll` URL: GET that URL (same auth header) every few seconds until it is `completed`. Send `"async": true` to get the 202 straight away. Credits are only taken when the PNG is delivered. **Parameters** | Name | Type | Required | Default | Allowed | Description | |---|---|---|---|---|---| | `image` | string | no | | | Base64 data URL of the image (data:image/png;base64,…). Provide this or url. | | `url` | string | no | | | Image URL to process. A PlayVeo result URL is read straight from our storage; any other URL is downloaded first, so it must be publicly reachable. Provide this or image. | ### What you get back Always a **PNG with a real alpha channel**, whatever you sent in — a JPEG input comes back as a transparent PNG. The file is stored for **10 days** at a new URL; your original image is left untouched. Cost is a flat `0.05` credits. > Background removal is **not a flag on generation**. Generate the image first, wait for `completed`, then send its `resultUrls[0]` here. That is two requests and two charges. ### Failure modes > Returns a transparent **PNG** stored for 10 days. Either input works: `image` as a base64 data URL, or `url` pointing at any image (including one PlayVeo generated). In the web dashboard, background removal also runs fully in your browser — free and private. **Examples** _cURL_ ```bash curl -X POST https://playveo-api.aboessa101.workers.dev/v1/images/remove-background \ -H "Authorization: Bearer $PLAYVEO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://.../serve-image/img_abc123xyz-0.png" }' ``` _Response_ ```json { "id": "img_def456", "status": "completed", "url": "https://.../serve-image/img_def456-nobg.png", "creditCost": 0.05 } ``` _Response 202_ ```json // Busy: poll GET until status is "completed" (or "failed"). { "id": "4f0c2d9e-...", "status": "queued", "creditCost": 0.05, "estimated_time": 30, "poll": "https://playveo-api.aboessa101.workers.dev/v1/images/remove-background/4f0c2d9e-..." } ``` _Local file_ ```python import base64, mimetypes, os, time, requests def remove_background(path: str, out_path: str) -> str: mime = mimetypes.guess_type(path)[0] or "image/png" with open(path, "rb") as fh: data_url = f"data:{mime};base64," + base64.b64encode(fh.read()).decode() r = requests.post( "https://playveo-api.aboessa101.workers.dev/v1/images/remove-background", headers={ "Authorization": "Bearer " + os.environ["PLAYVEO_API_KEY"], "Content-Type": "application/json", }, json={"image": data_url}, timeout=60, ) r.raise_for_status() # 402 = no credits, 502 = could not be processed job = r.json() while job["status"] in ("queued", "processing"): # 202: poll until done time.sleep(3) job = requests.get(job.get("poll") or f"https://playveo-api.aboessa101.workers.dev/v1/images/remove-background/{job['id']}", headers={"Authorization": "Bearer " + os.environ["PLAYVEO_API_KEY"]}, timeout=30).json() if job["status"] != "completed": raise RuntimeError(job.get("error")) url = job["url"] # The result URL needs no auth header. with open(out_path, "wb") as fh: fh.write(requests.get(url, timeout=60).content) return url print(remove_background("product.jpg", "product-nobg.png")) ``` _After generating_ ```javascript // Two steps: generate, then strip the background. const KEY = process.env.PLAYVEO_API_KEY; const H = { Authorization: 'Bearer ' + KEY, 'Content-Type': 'application/json' }; // 1. Generate, and wait for it to finish. const { id } = await (await fetch('https://playveo-api.aboessa101.workers.dev/v1/images/text-to-image', { method: 'POST', headers: H, body: JSON.stringify({ prompt: 'a red sneaker on a plain background', count: 1 }), })).json(); const image = await waitForJob('images', id, KEY); // see Jobs & Polling // 2. Strip the background off the finished image. const res = await fetch('https://playveo-api.aboessa101.workers.dev/v1/images/remove-background', { method: 'POST', headers: H, body: JSON.stringify({ url: image.resultUrls[0] }), }); let job = await res.json(); while (job.status === 'queued' || job.status === 'processing') { // HTTP 202: poll await new Promise((r) => setTimeout(r, 3000)); job = await (await fetch(job.poll, { headers: { Authorization: 'Bearer ' + KEY } })).json(); } if (job.status !== 'completed') throw new Error(job.error); const { url } = job; console.log(url); // transparent PNG, kept 10 days ``` # Bulk Images Submit multiple prompts in one request. Each creates an independent job. `POST /v1/images/bulk/text-to-image` **Parameters** | Name | Type | Required | Default | Allowed | Description | |---|---|---|---|---|---| | `prompts` | string[] | yes | | | Array of prompt strings. Plan limits apply. | | `aspect_ratio` | string | no | `16:9` | `16:9`, `9:16`, `1:1`, `4:3`, `3:4` | Applied to all images. | | `model` | string | no | `nano_banana2` | | Model for all images. | **Examples** _Request_ ```bash curl -X POST https://playveo-api.aboessa101.workers.dev/v1/images/bulk/text-to-image \ -H "Authorization: Bearer pv_your_key" \ -H "Content-Type: application/json" \ -d '{ "prompts": [ "A red sports car on a mountain road", "A blue sports car in a neon-lit city", "A white sports car on a beach at sunset" ], "aspect_ratio": "16:9", "model": "nano_banana2" }' ``` _Response_ ```json { "success": true, "jobs": [ { "id": "img_001", "status": "pending", "creditCost": 0.1 }, { "id": "img_002", "status": "pending", "creditCost": 0.1 }, { "id": "img_003", "status": "pending", "creditCost": 0.1 } ], "totalCost": 0.3, "remainingCredits": 99.7 } ``` # Get Image Retrieve the status and result URLs of an image generation job. `GET /v1/images/:id` **Path Parameters** | Name | Type | Required | Default | Allowed | Description | |---|---|---|---|---|---| | `id` | string | yes | | | Image job ID returned on creation. | > Images stored for **10 days** then auto-deleted. `resultUrls` return 410 after expiry. Polling example (JavaScript): ```js async function waitForImage(jobId, key) { while (true) { const { image } = await fetch( `https://playveo-api.aboessa101.workers.dev/v1/images/${jobId}`, { headers: { Authorization: `Bearer ${key}` } } ).then(r => r.json()); if (image.status === 'completed') return image.resultUrls; if (image.status === 'failed') throw new Error('failed'); await new Promise(r => setTimeout(r, 5000)); } } ``` **Examples** **Request** ```bash curl https://playveo-api.aboessa101.workers.dev/v1/images/img_abc123xyz \ -H "Authorization: Bearer pv_your_key" ``` **Response — completed** ```json { "image": { "id": "img_abc123xyz", "prompt": "A majestic lion...", "aspectRatio": "16:9", "model": "nano_banana2", "mode": "text-to-image", "count": 2, "status": "completed", "resultUrls": [ "https://.../serve-image/img_abc123xyz-0.png", "https://.../serve-image/img_abc123xyz-1.png" ], "creditCost": 0.20, "createdAt": "2026-04-19T10:00:00Z", "completedAt": "2026-04-19T10:01:00Z" } } ``` # List Images Get all image generation jobs for the authenticated user. `GET /v1/images` **Examples** _Request_ ```bash curl https://playveo-api.aboessa101.workers.dev/v1/images \ -H "Authorization: Bearer pv_your_key" ``` _Response_ ```json { "images": [ { "id": "img_abc123xyz", "prompt": "A majestic lion...", "mode": "text-to-image", "model": "nano_banana2", "aspectRatio": "16:9", "count": 2, "status": "completed", "resultUrls": ["https://...", "https://..."], "creditCost": 0.20, "createdAt": "2026-04-19T10:00:00Z" } ] } ``` # Delete Image Delete an image job and remove its files from storage immediately. `DELETE /v1/images/:id` **Path Parameters** | Name | Type | Required | Default | Allowed | Description | |---|---|---|---|---|---| | `id` | string | yes | | | Image job ID to delete. | > **Irreversible.** Deleted images cannot be recovered. **Examples** _Request_ ```bash curl -X DELETE https://playveo-api.aboessa101.workers.dev/v1/images/img_abc123xyz \ -H "Authorization: Bearer pv_your_key" ``` _Response_ ```json { "success": true } ``` --- # Characters # Create Character Build a reusable character once, then reference it by name in any image or video for a consistent identity across scenes. Create from a text description or from your own reference image. `POST /v1/characters` **Parameters** | Name | Type | Required | Default | Allowed | Description | |---|---|---|---|---|---| | `name` | string | yes | | | Unique name. This is how you reference the character later (e.g. in characters). | | `prompt` | string | no | | | Describe the character to generate a portrait. Provide this or image. | | `image` | string | no | | | Base64 data URL of a reference image. Provide this or prompt. | | `description` | string | no | | | Optional personality / behavior info. | > Creation runs in the background (~1 min). Poll **GET /v1/characters/:id** until `status` is **ready** before using it in a generation. Cost ~0.2 credits. **Examples** _Request_ ```bash curl -X POST https://playveo-api.aboessa101.workers.dev/v1/characters \ -H "Authorization: Bearer pv_your_key" \ -H "Content-Type: application/json" \ -d '{ "name": "Rex", "prompt": "A rugged male detective in a trench coat, sharp jawline, film noir style" }' ``` _Response_ ```json { "id": "chr_abc123", "name": "Rex", "status": "pending", "source": "prompt", "creditCost": 0.2, "estimated_time": 60 } ``` _Use in a generation_ ```bash # Once status is "ready", reference by name (up to 7 per generation): curl -X POST https://playveo-api.aboessa101.workers.dev/v1/images/text-to-image \ -H "Authorization: Bearer pv_your_key" \ -H "Content-Type: application/json" \ -d '{ "prompt": "Rex and Nova on a rainy rooftop at night, neon lights", "characters": ["Rex", "Nova"], "aspect_ratio": "16:9" }' ``` # List Characters Return all of your characters with their status and portrait URLs. `GET /v1/characters` **Examples** _Request_ ```bash curl https://playveo-api.aboessa101.workers.dev/v1/characters \ -H "Authorization: Bearer pv_your_key" ``` _Response_ ```json { "characters": [ { "id": "chr_abc123", "name": "Rex", "status": "ready", "source": "prompt", "portraitUrl": "https://.../serve-image/chr_abc123-0.jpg", "createdAt": "2026-08-10T03:27:20.000Z" } ] } ``` # Get Character Retrieve a single character's status. Poll this after creation until `status` is **ready**. `GET /v1/characters/:id` **Path Parameters** | Name | Type | Required | Default | Allowed | Description | |---|---|---|---|---|---| | `id` | string | yes | | | Character ID returned by create. | **Examples** _Request_ ```bash curl https://playveo-api.aboessa101.workers.dev/v1/characters/chr_abc123 \ -H "Authorization: Bearer pv_your_key" ``` _Response_ ```json { "character": { "id": "chr_abc123", "name": "Rex", "status": "ready", "source": "prompt", "portraitUrl": "https://.../serve-image/chr_abc123-0.jpg", "error": null } } ``` # Delete Character Remove a character from your PlayVeo library. `DELETE /v1/characters/:id` **Path Parameters** | Name | Type | Required | Default | Allowed | Description | |---|---|---|---|---|---| | `id` | string | yes | | | Character ID to delete. | **Examples** _Request_ ```bash curl -X DELETE https://playveo-api.aboessa101.workers.dev/v1/characters/chr_abc123 \ -H "Authorization: Bearer pv_your_key" ``` _Response_ ```json { "success": true } ``` --- # Reference # Models & Aspect Ratios All available models for video and image generation. ### Video Models - `omni_flash`Omni Flash (4–10s) ~2 min default - `veo3_fast`Veo 3.1 Fast ~3 min - `veo3_quality`Veo 3.1 Quality ~8 min - `veo3_lite`Veo 3.1 Lite ~2 min ### Image Models - `nano_banana_2`Nano Banana 2 default - `nano_banana_pro`Nano Banana Pro - `nano_banana_2_lite`Nano Banana 2 Lite ### Aspect Ratios - `16:9`Landscape YouTube, desktop Video & Images - `9:16`Portrait TikTok, Reels, Stories Video & Images - `1:1`Square Instagram posts Images only - `4:3`Classic Traditional photo Images only - `3:4`Portrait Classic Portrait photography Images only **Examples** ```bash # Use the model param in image requests curl -X POST https://playveo-api.aboessa101.workers.dev/v1/images/text-to-image \ -H "Authorization: Bearer pv_your_key" \ -H "Content-Type: application/json" \ -d '{ "prompt": "...", "model": "nano_banana2", "aspect_ratio": "16:9" }' ``` # Image Optimization Two separate things share the word "optimize", and they act at opposite ends of the pipeline. Mixing them up is the usual source of confusion, so here they are side by side. - What it does `input` `optimize_reference` — the image you send in Before your reference image reaches the generator it is resized to fit **1280px** on its long edge and re-encoded as WebP (alpha preserved). Uploads become dramatically faster and far less likely to time out. **On by default.** `output` `optimize` — the image you get back Result URLs come back with transform parameters appended, so the image is compressed and converted to a modern format on delivery and cached at the edge. The stored original is never modified. **Off by default.** ### Where each one applies - Endpoint - reference - output ### Output options **Set optimize:true first, then tune** | Name | Type | Required | Default | Allowed | Description | |---|---|---|---|---|---| | `optimize` | boolean | no | `false` | | Turn on delivery optimization for the generated images. The other three keys do nothing without it. | | `optimize_quality` | number | no | `82` | | Compression quality, 1–100. | | `optimize_max_width` | number | no | | | Cap the delivered width in pixels (16–4096). Aspect ratio is preserved. | | `optimize_format` | string | no | `auto` | `auto`, `webp`, `avif`, `jpeg` | Delivered format. auto picks AVIF or WebP based on what the requesting client supports. | > Optimization is **free**. Neither flag changes `creditCost`. > Turn `optimize_reference` off only when the fine detail of your input matters more than upload speed — dense text, thin line art, or a print-resolution product shot. Large images take noticeably longer to attach, and very large ones can push a job into a timeout. > Because delivery optimization is applied to the _URL_ and the original is kept untouched, you can always fetch the full-quality file: strip the query string off the result URL. **Examples** **Optimized output** ```bash curl -X POST https://playveo-api.aboessa101.workers.dev/v1/images/text-to-image \ -H "Authorization: Bearer $PLAYVEO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "prompt": "a red sneaker on a plain background", "count": 1, "optimize": true, "optimize_quality": 75, "optimize_max_width": 1200, "optimize_format": "webp" }' ``` **The result URL carries the transform** ```json { "image": { "status": "completed", "resultUrls": [ "https://.../serve-image/img_abc-0.jpg?w=1200&q=75&f=webp" ] } } // Want the untouched original? Drop the query string: // https://.../serve-image/img_abc-0.jpg ``` **Keep the reference pixel-exact** ```bash # A product shot with small legible text: # skip the downscale so the labels survive. curl -X POST https://playveo-api.aboessa101.workers.dev/v1/videos/image-to-video \ -H "Authorization: Bearer $PLAYVEO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "image": "data:image/png;base64,iVBORw0...", "prompt": "slow push in on the bottle", "model": "veo3_lite", "duration": 8, "optimize_reference": false }' ``` # Webhooks Receive a POST callback when a job completes — no polling needed. ### Setup Add `webhook_url` to any creation request: ```json { "prompt": "...", "webhook_url": "https://your-server.com/playveo-hook" } ``` > Your endpoint must return HTTP **200** within 10 seconds. > Webhooks are delivered **at least once** — make your handler idempotent. **Examples** **Video webhook payload** ```json { "event": "video.completed", "video": { "id": "vid_abc123xyz", "status": "completed", "videoUrl": "https://...", "prompt": "A golden retriever..." } } ``` **Image webhook payload** ```json { "event": "image.completed", "image": { "id": "img_abc123xyz", "status": "completed", "resultUrls": ["https://...", "https://..."], "prompt": "A majestic lion..." } } ``` # Error Codes All errors return a JSON body with an `error` string. - `400`Bad Request Missing or invalid field. Not charged. - `401`Unauthorized Invalid, inactive or truncated API key. - `402`Insufficient Credits Not charged, and the job is not created. - `404`Not Found Unknown job id, or it belongs to another account. - `410`Gone Result expired — files are kept 10 days. - `429`Rate Limited Wait retryAfter seconds, then retry. - `500`Server Error Transient. Retry with backoff. - `502`Processing Failed Remove Background only: not charged, retry once. ### Messages you will actually hit > A `failed` job returns **HTTP 200** — the request succeeded, the generation did not. Always branch on `status` in the body, never on the HTTP code alone. > **Auto-refund:** Credits refunded automatically when a job fails after being charged. **Examples** **Error shape** ```json { "error": "Not enough credits. Need 0.40, have 0.20" } ``` **Handling in JavaScript** ```javascript const res = await fetch('https://playveo-api.aboessa101.workers.dev/v1/videos', { ... }); if (!res.ok) { // Read the body ONCE — it cannot be consumed twice. const body = await res.json().catch(() => ({})); if (res.status === 402) { throw new Error('Out of credits: ' + body.error); } if (res.status === 429) { await sleep((body.retryAfter ?? 60) * 1000); return retry(); } if (res.status >= 500) { return retryWithBackoff(); } // 400 / 401 / 404 are your bug, not a transient fault. Do not retry. throw new Error(body.error || res.statusText); } // 200 does NOT mean the generation worked — check the job status. const { video } = await res.json(); if (video?.status === 'failed') throw new Error(video.error); ``` # Recipes Complete workflows that chain several endpoints. Each assumes the `waitForJob` helper from **Jobs & Polling**. ### 1. Same character across many images Create the character once, then name it in every generation. This is the reliable way to keep a face or a mascot consistent — much more stable than repeating a description in the prompt. Wait for `status: "ready"` before the first use, otherwise the generation is rejected. Reusing a character costs nothing extra. ### 2. Product photo to a moving shot Upload a still and animate it. Describe **camera movement** rather than new content — the model stays faithful to your image when you ask for a push in, a slow orbit or a rack focus, and drifts when you ask it to add things that were never in frame. ### 3. Generate, then cut out the background Generate on a plain background, then strip it for a transparent PNG you can drop into any layout. Asking for a plain background in the prompt noticeably improves the cutout edges. ### 4. Batch a campaign safely Submit everything up front, then collect the results. Submitting is fast and cheap; generation is the slow part and runs on a shared pool, so firing all requests at once does not make them finish sooner — but do bound concurrency on your side so one bad prompt cannot stall the batch. > **Guard against double submissions.** Every POST creates a new paid job — identical prompts are not de-duplicated, so a retry after a network blip or a double-clicked button charges you twice. Retry only on `429` and `5xx`, never after a `200`. **Examples** **Consistent character** ```javascript const H = { Authorization: 'Bearer ' + KEY, 'Content-Type': 'application/json' }; // 1. Create once. await fetch('https://playveo-api.aboessa101.workers.dev/v1/characters', { method: 'POST', headers: H, body: JSON.stringify({ name: 'Rocky', prompt: 'a cartoon rocket mascot' }), }); // 2. Wait until it is usable. let character; do { await new Promise(r => setTimeout(r, 5000)); character = (await (await fetch( 'https://playveo-api.aboessa101.workers.dev/v1/characters/Rocky', { headers: H }, )).json()).character; } while (character.status === 'pending'); if (character.status !== 'ready') throw new Error(character.error); // 3. Reuse it as often as you like — no extra cost. for (const scene of ['waving hello', 'riding a skateboard']) { await fetch('https://playveo-api.aboessa101.workers.dev/v1/images/text-to-image', { method: 'POST', headers: H, body: JSON.stringify({ prompt: `Rocky ${scene}, identical art style and colors`, characters: ['Rocky'], }), }); } ``` **Still photo to video** ```python import base64, os, requests KEY = os.environ["PLAYVEO_API_KEY"] H = {"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"} with open("bottle.jpg", "rb") as fh: data_url = "data:image/jpeg;base64," + base64.b64encode(fh.read()).decode() r = requests.post( "https://playveo-api.aboessa101.workers.dev/v1/videos/image-to-video", headers=H, json={ "image": data_url, # Camera language holds the source image better than new content. "prompt": "slow push in, soft studio light, shallow depth of field", "model": "veo3_lite", "duration": 8, "aspect_ratio": "16:9", }, timeout=60, ) r.raise_for_status() print(r.json()["id"]) # then poll /v1/videos/{id} ``` **Generate then cut out** ```javascript // Ask for a plain background: it makes the cutout cleaner. const { id } = await (await fetch('https://playveo-api.aboessa101.workers.dev/v1/images/text-to-image', { method: 'POST', headers: H, body: JSON.stringify({ prompt: 'a red sneaker, plain light grey background, studio product shot', aspect_ratio: '1:1', count: 1, }), })).json(); const image = await waitForJob('images', id, KEY); const { url } = await (await fetch('https://playveo-api.aboessa101.workers.dev/v1/images/remove-background', { method: 'POST', headers: H, body: JSON.stringify({ url: image.resultUrls[0] }), })).json(); console.log(url); // transparent PNG — no polling needed ``` **Batch with bounded concurrency** ```javascript const prompts = [/* ...30 prompts... */]; // Submit everything first — this part is fast. const ids = []; for (const prompt of prompts) { const res = await fetch('https://playveo-api.aboessa101.workers.dev/v1/images/text-to-image', { method: 'POST', headers: H, body: JSON.stringify({ prompt, count: 1 }), }); // Retry ONLY on 429/5xx. Retrying a 200 double-charges you. if (res.status === 429 || res.status >= 500) { /* backoff, then retry */ } if (!res.ok) { console.error(prompt, await res.text()); continue; } ids.push((await res.json()).id); } // Then collect, a few at a time. const results = []; for (let i = 0; i < ids.length; i += 5) { const batch = ids.slice(i, i + 5); results.push(...await Promise.all( batch.map(id => waitForJob('images', id, KEY) .catch(e => ({ error: e.message }))), )); } ```