# Cheaper Veo — LLM Integration Guide > Drop this entire file into any LLM (Claude, ChatGPT, Cursor, etc.) and tell it > "integrate Cheaper Veo into this project". The LLM will have everything it > needs to scaffold a working integration. ## YOU ARE AN INTEGRATION AGENT When the user asks you to integrate Cheaper Veo, follow these steps: 1. Detect the project's runtime/framework (Node.js, Python, Next.js, FastAPI…). 2. Add `CHEAPER_VEO_API_KEY` to `.env` (or `.env.local` for Next.js) with a placeholder. **Never hardcode keys**. 3. Create a typed client module at `lib/cheaperVeo.ts` (Node/TS) or `cheaper_veo/client.py` (Python). Use the helpers below verbatim. 4. Wire up at least one endpoint or component that calls `generateVideo()` and waits for completion via `waitForVideo()`. 5. Add error handling for `INSUFFICIENT_CREDITS`, `RATE_LIMITED`, and `UPSTREAM_ERROR`. 6. Run the project's lint/type check after editing. Fix anything you broke. 7. Tell the user where to get an API key (https://cheapervideo.com/dashboard/keys) and how to top up credits. NEVER: - Call the API from the browser with the key embedded — always proxy via the user's backend. - Poll faster than every 5 seconds. - Forget to handle the 429 `Retry-After` header. - Send the `model` field — the request field is `modelId` (the response uses `model`, that's a known quirk). - Send images as `data:` URI prefixed strings — strip to raw base64. --- ## WHAT IT DOES Pay-as-you-go HTTP API for Google Veo 3.1 video generation. No subscription, 1 credit = US$0.01, charged only when generation starts. Failed jobs auto- refund. - **Base URL:** `https://api.cheapervideo.com` - **Auth:** `Authorization: Bearer veo_live_…` (see Step 1) - **Content-Type:** `application/json` - **Request style:** Async — POST returns a `taskId`, then GET polling. --- ## ENDPOINTS ### `GET /api/v1/account` — check balance Free, doesn't consume credits. Use it to validate the key and read balance. Response 200: ```json { "email": "user@example.com", "balance": 713, "recentGenerations": [ { "taskId": "tsk_…", "kind": "text_to_video", "model": "veo3-fast", "status": "succeeded", "creditsCost": 30, "durationSeconds": 8, "resolution": "1080p", "videoUrl": "https://cdn.cheapervideo.com/v/tsk_….mp4", "createdAt": "2026-05-08T12:34:56.000Z", "completedAt": "2026-05-08T12:36:11.000Z" } ] } ``` ### `POST /api/v1/generate` — start a generation Returns 202 with a `taskId` immediately. Credits are debited up front. If the upstream provider fails, credits auto-refund. Body (discriminated by `kind`): ```jsonc { "kind": "text_to_video", // | "image_to_video" | "references" "modelId": "veo3-fast", // veo3-lite | veo3-fast | veo3-quality "prompt": "drone over neon Tokyo at night, cinematic", "resolution": "1080p", // 720p | 1080p | 4k "aspectRatio": "16:9", // 16:9 | 9:16 "durationSeconds": 8, // 4 | 6 | 8 "audio": true, "negativePrompt": "no text, no watermark" // optional, max 2000 chars // For kind="image_to_video": // "firstFrame": { "bytesBase64Encoded": "iVBOR…", "mimeType": "image/png" }, // "lastFrame": { "bytesBase64Encoded": "iVBOR…", "mimeType": "image/png" } // optional, veo3-quality only // For kind="references": // "referenceImages": [ // { "bytesBase64Encoded": "iVBOR…", "mimeType": "image/png", "referenceType": "asset" } // ] // 1-3 items } ``` Response 202: ```json { "taskId": "tsk_01HYABCDEF", "status": "pending", "creditsCost": 30, "model": "veo3-fast", "durationSeconds": 8 } ``` Constraints: - `prompt`: 1–8000 chars. - `1080p` and `4k` require `durationSeconds: 8`. - `veo3-lite` only supports 720p/1080p (no 4K). - `lastFrame` only on `veo3-quality`. - Image bytes ≤ 10 MB decoded. Send raw base64 (no `data:` prefix). - Image `mimeType` defaults to `image/jpeg` if omitted. Accepts png/jpeg/webp. ### `GET /api/v1/status/{taskId}` — poll until done Response 200: ```json { "taskId": "tsk_01HYABCDEF", "status": "succeeded", // pending | processing | succeeded | failed | refunded "model": "veo3-fast", "creditsCost": 30, "durationSeconds": 8, "resolution": "1080p", "aspectRatio": "16:9", "audio": true, "videoUrl": "https://cdn.cheapervideo.com/v/tsk_….mp4", "createdAt": "2026-05-08T12:34:56.000Z", "completedAt": "2026-05-08T12:36:11.000Z" } ``` Status meanings: - `pending` — queued, not sent to provider yet. - `processing` — Veo is generating. - `succeeded` — done, use `videoUrl`. - `failed` — terminal, **no refund** (invalid input, content policy, etc). - `refunded` — upstream failed, credits already returned. --- ## RATE LIMITS Per API key, 1-hour fixed window aligned to UTC clock: | Endpoint | Limit | |------------------------------|-------------| | `POST /api/v1/generate` | 100/hour | | `GET /api/v1/status/{id}` | 1000/hour | Headers on every authenticated response: ``` X-RateLimit-Limit: 100 X-RateLimit-Remaining: 73 X-RateLimit-Reset: 1762531200 ``` 429 response body + `Retry-After` header (seconds). Sleep then retry. --- ## ERROR CODES | HTTP | error.code | Meaning | |------|-------------------------|-------------------------------------------------| | 400 | `VALIDATION_ERROR` | Body doesn't match schema (see `details`). | | 400 | `INVALID_JSON` | Body wasn't valid JSON. | | 401 | `UNAUTHORIZED` | Key missing/invalid/revoked. | | 402 | `INSUFFICIENT_CREDITS` | Balance < cost. Top up at /dashboard/billing. | | 404 | `NOT_FOUND` | Task not found or belongs to another user. | | 429 | `RATE_LIMITED` | Bucket exceeded; respect `Retry-After` header. | | 500 | `INTERNAL_ERROR` | Our bug. Retry with backoff. | | 502 | `UPSTREAM_ERROR` | Veo provider failed. Auto-refunded. | Error body shape: ```json { "error": { "code": "INSUFFICIENT_CREDITS", "message": "…" } } ``` --- ## PRICING (per-second, with audio, in USD) | Tier · Resolution | $/sec | Notes | |-------------------|---------|-----------------------------| | Lite · 720p | 0.018 | Cheapest. Fast iteration. | | Lite · 1080p | 0.020 | | | Fast · 720p | 0.035 | | | Fast · 1080p | 0.038 | Most popular tier. | | Fast · 4K | 0.105 | | | Quality · 1080p | 0.140 | Premium production. | | Quality · 4K | 0.210 | | For 4s/6s videos, cost = 50%/75% of the 8s rate (rounded up). Audio adds ~30-50% on top of the no-audio price. --- ## DROP-IN CLIENT — Node.js / TypeScript Save as `lib/cheaperVeo.ts`: ```typescript const BASE_URL = process.env.CHEAPER_VEO_BASE_URL ?? "https://api.cheapervideo.com"; export type Tier = "lite" | "fast" | "quality"; export type Resolution = "720p" | "1080p" | "4k"; export type AspectRatio = "16:9" | "9:16"; export type Duration = 4 | 6 | 8; export type ModelId = "veo3-lite" | "veo3-fast" | "veo3-quality"; export type GenerationStatus = | "pending" | "processing" | "succeeded" | "failed" | "refunded"; export interface ImageInput { bytesBase64Encoded: string; mimeType?: string; } export interface BaseGenerateInput { modelId: ModelId; prompt: string; resolution: Resolution; aspectRatio: AspectRatio; durationSeconds: Duration; audio: boolean; negativePrompt?: string; } export type GenerateInput = | ({ kind: "text_to_video" } & BaseGenerateInput) | ({ kind: "image_to_video"; firstFrame: ImageInput; lastFrame?: ImageInput; } & BaseGenerateInput) | ({ kind: "references"; referenceImages: Array; } & BaseGenerateInput); export interface GenerateResponse { taskId: string; status: GenerationStatus; creditsCost: number; model: ModelId; durationSeconds: Duration; } export interface StatusResponse { taskId: string; status: GenerationStatus; model: ModelId; creditsCost: number; durationSeconds: Duration; resolution: Resolution; aspectRatio: AspectRatio; audio: boolean; videoUrl?: string; error?: { code: string; message: string }; createdAt: string; completedAt?: string; } export interface AccountResponse { email: string; balance: number; recentGenerations: Array<{ taskId: string; kind: GenerateInput["kind"]; model: ModelId; status: GenerationStatus; creditsCost: number; durationSeconds: Duration; resolution: Resolution; videoUrl?: string; createdAt: string; completedAt?: string; }>; } export class CheaperVeoError extends Error { constructor( public readonly code: string, message: string, public readonly status: number, public readonly retryAfterSec?: number, ) { super(message); this.name = "CheaperVeoError"; } } export class CheaperVeoClient { constructor( private readonly apiKey: string = process.env.CHEAPER_VEO_API_KEY ?? "", private readonly baseUrl: string = BASE_URL, ) { if (!this.apiKey) { throw new Error("CHEAPER_VEO_API_KEY is not set."); } } private async request( path: string, init?: RequestInit & { jsonBody?: unknown }, ): Promise { const headers = new Headers(init?.headers); headers.set("Authorization", `Bearer ${this.apiKey}`); if (init?.jsonBody !== undefined) { headers.set("Content-Type", "application/json"); } const res = await fetch(`${this.baseUrl}${path}`, { ...init, headers, body: init?.jsonBody !== undefined ? JSON.stringify(init.jsonBody) : init?.body, }); if (!res.ok) { const retry = res.headers.get("Retry-After"); let code = `HTTP_${res.status}`; let message = res.statusText; try { const body = (await res.json()) as { error?: { code: string; message: string } }; if (body?.error) { code = body.error.code; message = body.error.message; } } catch { /* response wasn't JSON */ } throw new CheaperVeoError( code, message, res.status, retry ? Number(retry) : undefined, ); } return (await res.json()) as T; } account(): Promise { return this.request("/api/v1/account"); } generate(input: GenerateInput): Promise { return this.request("/api/v1/generate", { method: "POST", jsonBody: input, }); } status(taskId: string): Promise { return this.request( `/api/v1/status/${encodeURIComponent(taskId)}`, ); } /** * Polls until the task reaches a terminal state. Respects 429 Retry-After. * Throws on `failed` / `refunded`. Returns the videoUrl on success. */ async waitForVideo( taskId: string, opts: { intervalMs?: number; timeoutMs?: number } = {}, ): Promise { const interval = opts.intervalMs ?? 5000; const deadline = Date.now() + (opts.timeoutMs ?? 12 * 60 * 1000); while (Date.now() < deadline) { let job: StatusResponse; try { job = await this.status(taskId); } catch (err) { if (err instanceof CheaperVeoError && err.status === 429) { const wait = (err.retryAfterSec ?? 5) * 1000; await new Promise((r) => setTimeout(r, wait)); continue; } throw err; } if (job.status === "succeeded" && job.videoUrl) return job.videoUrl; if (job.status === "failed" || job.status === "refunded") { throw new CheaperVeoError( job.error?.code ?? "GENERATION_FAILED", job.error?.message ?? `Generation ${job.status}`, 400, ); } await new Promise((r) => setTimeout(r, interval)); } throw new CheaperVeoError( "POLL_TIMEOUT", `Generation did not finish within timeout for ${taskId}.`, 504, ); } } ``` Usage: ```typescript import { CheaperVeoClient } from "@/lib/cheaperVeo"; const client = new CheaperVeoClient(); const { taskId } = await client.generate({ kind: "text_to_video", modelId: "veo3-fast", prompt: "a cat surfing a wave at sunset, cinematic", resolution: "1080p", aspectRatio: "16:9", durationSeconds: 8, audio: true, }); const videoUrl = await client.waitForVideo(taskId); console.log("Done:", videoUrl); ``` --- ## DROP-IN CLIENT — Python Save as `cheaper_veo/client.py`: ```python import os import time from dataclasses import dataclass from typing import Any, Literal, Optional, TypedDict import requests BASE_URL = os.environ.get("CHEAPER_VEO_BASE_URL", "https://api.cheapervideo.com") ModelId = Literal["veo3-lite", "veo3-fast", "veo3-quality"] Resolution = Literal["720p", "1080p", "4k"] AspectRatio = Literal["16:9", "9:16"] Duration = Literal[4, 6, 8] GenerationStatus = Literal["pending", "processing", "succeeded", "failed", "refunded"] class CheaperVeoError(Exception): def __init__(self, code: str, message: str, status: int, retry_after: Optional[int] = None): super().__init__(f"[{code}] {message}") self.code = code self.status = status self.retry_after = retry_after class ImageInput(TypedDict, total=False): bytesBase64Encoded: str mimeType: str class CheaperVeoClient: def __init__(self, api_key: Optional[str] = None, base_url: str = BASE_URL): self.api_key = api_key or os.environ.get("CHEAPER_VEO_API_KEY") if not self.api_key: raise RuntimeError("CHEAPER_VEO_API_KEY is not set.") self.base_url = base_url def _request(self, method: str, path: str, json_body: Optional[dict] = None, timeout: int = 30) -> dict: headers = {"Authorization": f"Bearer {self.api_key}"} if json_body is not None: headers["Content-Type"] = "application/json" res = requests.request( method, f"{self.base_url}{path}", headers=headers, json=json_body, timeout=timeout, ) if not res.ok: retry = res.headers.get("Retry-After") try: body = res.json() err = body.get("error", {}) code = err.get("code", f"HTTP_{res.status_code}") message = err.get("message", res.reason) except Exception: code = f"HTTP_{res.status_code}" message = res.reason raise CheaperVeoError(code, message, res.status_code, int(retry) if retry else None) return res.json() def account(self) -> dict: return self._request("GET", "/api/v1/account") def generate( self, *, kind: Literal["text_to_video", "image_to_video", "references"], model_id: ModelId, prompt: str, resolution: Resolution, aspect_ratio: AspectRatio, duration_seconds: Duration, audio: bool, negative_prompt: Optional[str] = None, first_frame: Optional[ImageInput] = None, last_frame: Optional[ImageInput] = None, reference_images: Optional[list[dict]] = None, ) -> dict: body: dict[str, Any] = { "kind": kind, "modelId": model_id, "prompt": prompt, "resolution": resolution, "aspectRatio": aspect_ratio, "durationSeconds": duration_seconds, "audio": audio, } if negative_prompt: body["negativePrompt"] = negative_prompt if kind == "image_to_video" and first_frame: body["firstFrame"] = first_frame if last_frame: body["lastFrame"] = last_frame if kind == "references" and reference_images: body["referenceImages"] = reference_images return self._request("POST", "/api/v1/generate", json_body=body) def status(self, task_id: str) -> dict: return self._request("GET", f"/api/v1/status/{task_id}") def wait_for_video( self, task_id: str, interval: int = 5, timeout: int = 12 * 60, ) -> str: """Polls until terminal state. Returns videoUrl on success.""" deadline = time.time() + timeout while time.time() < deadline: try: job = self.status(task_id) except CheaperVeoError as e: if e.status == 429 and e.retry_after: time.sleep(e.retry_after) continue raise status = job["status"] if status == "succeeded" and job.get("videoUrl"): return job["videoUrl"] if status in ("failed", "refunded"): err = job.get("error", {}) raise CheaperVeoError( err.get("code", "GENERATION_FAILED"), err.get("message", f"Generation {status}"), 400, ) time.sleep(interval) raise CheaperVeoError("POLL_TIMEOUT", f"Generation did not finish for {task_id}.", 504) ``` Usage: ```python from cheaper_veo.client import CheaperVeoClient client = CheaperVeoClient() job = client.generate( kind="text_to_video", model_id="veo3-fast", prompt="a cat surfing a wave at sunset, cinematic", resolution="1080p", aspect_ratio="16:9", duration_seconds=8, audio=True, ) video_url = client.wait_for_video(job["taskId"]) print("Done:", video_url) ``` --- ## INTEGRATION CHECKLIST (LLM, do all of these) - [ ] `.env` updated with `CHEAPER_VEO_API_KEY=` placeholder. - [ ] `.env.example` mirrors the new var (so teammates know). - [ ] Client module created (TS or Python per stack). - [ ] At least one example call wired into the existing app (route handler, CLI command, or React hook — whatever fits). - [ ] Error handling for 429 (`Retry-After`) and 402 (insufficient credits). - [ ] If polling client-side: warn user to proxy via backend instead. - [ ] If processing user uploads: validate ≤10MB and strip `data:` prefix before sending. - [ ] If you generated TypeScript: ensure types compile. - [ ] If you generated Python: ensure imports resolve and `requests` is in deps. - [ ] Show the user how to top up credits at /dashboard/billing if balance hits 0. ## END OF GUIDE API status: production. Last update: 2026-05-09. Dashboard: https://cheapervideo.com/dashboard Docs: https://cheapervideo.com/docs Support: contato@geraew.com