# ZOOOP > ZOOOP is an AI-native creative platform and filmmaking workstation. Generate > images, video, audio, speech, and music from every top AI model in one > browser-based workspace — through focused generator pages, reusable recipe > templates, or an infinite collaborative canvas. Credit-based and > pay-as-you-go; credits never expire and there is no subscription required. This file follows the `llmstxt.org` convention so language-model agents and AI search engines can understand what ZOOOP offers without crawling HTML. Website: · Model catalog: · Pricing: ## What is ZOOOP ZOOOP brings every leading generative-AI model under one roof so creators don't have to juggle a dozen separate tools and subscriptions. There are three ways to create: - **Generator pages** — a focused page per task (image, video, speech, music…) for when you just need one result. - **Recipe templates & tools** — one-click presets built on top of the models; creators can publish their own and earn from others' usage. - **Generative canvas** — an infinite pan/zoom workspace for storyboarding and chaining shots together, with real-time multiplayer collaboration. ## Capabilities - **Image**: text-to-image, image-to-image, image editing & inpainting, SVG / vector graphics, upscaling, background removal. - **Video**: text-to-video, image-to-video, motion control, first/last-frame interpolation, video extension, video-to-video editing, lip sync, and automatic sound effects & background music. - **Audio**: text-to-speech, voice cloning, sound-effect generation, and music generation. ## Supported models ZOOOP aggregates 70+ models from leading providers. A representative selection: - **Image**: Flux 2 (Pro / Flash), Nano Banana (Pro / 2), Seedream, GPT Image, Recraft (including vector), Grok Imagine. - **Video**: Google Veo 3 / 3.1, Kling (v2.6 / v3), ByteDance Seedance, MiniMax Hailuo, Alibaba Wan, Luma Ray 2, Pika, Vidu, LTX. - **Speech & audio**: ElevenLabs (multilingual v3, sound effects), MiniMax Speech, Qwen TTS, Gemini TTS, Google Lyria, MiniMax Music. The full, always-current catalog — with a dedicated page per model — lives at . ## Pricing - Pay-as-you-go credits. **Credits never expire**, and individuals need no monthly subscription. Free daily login credits are granted on check-in. - The cost of each generation depends on the model and the output (resolution / duration). - Teams get a shared credit pool with per-creator seats billed monthly. Current rates: . ## Why ZOOOP - **Every top model in one place** — switch models without switching tools or paying for multiple subscriptions. - **No subscription, no expiry** — buy credits and use them whenever you want. - **Two modes** — a focused generator when you need a single shot, an infinite canvas when you're building a whole sequence. - **Creator economy** — publish recipe templates and earn from others' usage. - **Runs in the browser** — no GPU and nothing to install. ## For developers — REST API ZOOOP also exposes a public REST API for image / video / audio generation. Authenticate with a Personal Access Token; per-call charges hit your existing credit balance. - [API reference](https://github.com/zooopai/skill-zooop/blob/main/references/api-docs.md) — full v1 endpoint specification - [llms-full.txt](/llms-full.txt) — every doc concatenated for single-fetch ingest ## Skill bundle (Claude Code / agent integration) The canonical agent skill bundle lives on GitHub at — `SKILL.md`, helper shell scripts, and install / contribution docs. Claude Code users: ``` claude install github:zooopai/skill-zooop ``` Other agents: clone the repo or fetch . ## Quick start 1. Visit and create a PAT (`zpk_live_…`). Bind it to a project — all PAT-submitted tasks + uploads land there. 2. (Optional) Self-check budget + project before doing real work: `GET https://api.zooop.ai/v1/me` 3. List models for the task type: `GET https://api.zooop.ai/v1/models?type=image&subtype=default` 4. (Optional) Upload a local file the model will consume: `POST https://api.zooop.ai/v1/uploads` (raw body + `Content-Type`) 5. Submit a task: `POST https://api.zooop.ai/v1/tasks` with `{interfaceId, versionId, params}` 6. Poll until terminal: `GET https://api.zooop.ai/v1/tasks/{id}` All requests carry `Authorization: Bearer $ZOOOP_API_KEY`. ## Public categories (`type` × `subType`) The 12 (type, subType) pairs the API currently exposes match the homepage sidebar one-for-one: ``` image / default text-to-image / image-to-image image / edit-image image editing with mask video / text-ref text-to-video video / motion-control image-to-video with motion direction video / first-last-frame generate video between two keyframes video / audio-lipsync sync video to audio video / extend-video extend an existing video video / video-edit edit / restyle existing video audio / text-to-speech TTS audio / voice-clone voice cloning from sample audio / sound-effect text-to-SFX audio / music text-to-music ``` --- # SKILL.md (Agent integration guide) --- name: zooop description: | Generate or edit images / videos / audio via the ZOOOP AI platform — t2i, i2i, t2v, i2v, lipsync, upscale, remove-background, TTS, voice-clone, sound-effect, music. Use whenever the user asks to generate, create, make, edit, or transform any of those media types. --- # ZOOOP AI generation Public REST API host: `https://api.zooop.ai` (override via `$ZOOOP_API_HOST`). Auth: `Authorization: Bearer $ZOOOP_API_KEY` on every request. `scripts/{upload,quote,submit,poll,download,ai-tools,describe}.sh` ship with this skill. Inline `curl` recipes below work the same way if the bundle isn't cloned. Full request / response / error reference: [`references/api-docs.md`](./references/api-docs.md) — read it whenever this guide doesn't cover a field. ## API key — setup **The token must NEVER appear in the agent conversation.** Token in chat risks leaking into training corpora, telemetry, or shared transcripts — once leaked it spends the user's credits until revoked. Always have the **user** set the env var themselves, in their own terminal. Agent: check `$ZOOOP_API_KEY` (or OS-equivalent). If empty, give the user these instructions **verbatim** — do not ask them to paste the token back: 1. Get a token. **Personal** (spends your own credits): visit [https://zooop.ai/user#apiKeys](https://zooop.ai/user#apiKeys) → **Create token** → pick project (immutable) → **set a daily credit cap** → copy token (shown ONCE). **Team** (spends the team's shared credits): a team owner/admin creates it in the team admin → API Keys tab. Everything below works identically. 2. In their **own terminal** (NOT this chat), set the env var: - macOS / Linux / WSL / Git Bash — `echo 'export ZOOOP_API_KEY=zpk_live_…' >> ~/.zshrc && source ~/.zshrc` - Windows PowerShell — `[Environment]::SetEnvironmentVariable('ZOOOP_API_KEY','zpk_live_…','User')` - Windows cmd — `setx ZOOOP_API_KEY "zpk_live_…"` 3. **Restart the agent** so the new env var is inherited. Tasks and uploads land under the token's bound project. `GET /v1/me` shows the active wallet and balance (`user.creditBalance` personal, `team.creditBalance` team). ## Two paths: raw model vs AI tool **Default to raw models.** AI tools are a deliberately narrow surface for specialized capabilities that raw text-to-image / text-to-video models CAN'T replicate — chiefly precise background removal and image / video upscaling. For everything else (style transfer, edits, age changes, character animations, etc.) prefer a raw model: GPT Image 2 / Nanobanana handle most prompt-driven edits more flexibly AND cheaper. | Path | When | How | | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | | **Raw model** (`interfaceId` + `versionId`) | Default. Text-driven generation, prompt-driven edits, anything where you'd write a prompt | `GET /v1/models?type=…&subtype=…` → submit with `interfaceId` + `versionId` + `params` | | **AI tool** (`aiTool` slug) | Specialized non-prompt-driven capabilities only — currently background removal + image/video upscale. Browse `GET /v1/ai-tools` to see what's actually exposed | `GET /v1/ai-tools` → submit with `aiTool: ` + the tool's `params[]` | `/v1/ai-tools` is a short curated list. If your task isn't on it, don't force it through — use a raw model with a tailored prompt. ```bash bash scripts/ai-tools.sh [image|video] # browse catalog, optionally filtered bash scripts/ai-tools.sh -s # one tool's full param schema ``` Then reuse the raw-path `submit.sh` / `quote.sh`, swapping ` ` for `--ai-tool `: ```bash bash scripts/submit.sh --ai-tool background-removal '{"image_url":"https://storage.zooop.ai/…"}' ``` ## How model selection works (raw path) Pick a (type, subType) pair by **what the user wants to do**, not by what inputs they happen to provide. Slug names like `motion-control` / `text-ref` aren't self-explanatory — read the descriptions, don't guess from the name. | type | subType | What it's for | | ----- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | image | default | Generate a new image, **or edit an existing one from a plain text description** (text-to-image, img-to-img, style transfer, prompt-driven edits — GPT Image 2 / Nanobanana handle "把背景换成…" / "去掉左边的人" without any mask) | | image | edit-image | Targeted region edit driven by a **black-and-white mask or hand-drawn annotation** the user supplies. Only pick this when the user actually provides (or asks to draw) a mask/region — never for plain text-described edits | | image | image-svg | Generate **scalable vector graphics** (`.svg`) — logos, icons, flat illustrations, infographics — from a text prompt, or vectorize a raster image. Recraft Text/Image-to-Vector models; output is editable vector paths, NOT a raster PNG. Pick this only when the user explicitly wants vector / SVG output | | video | text-ref | Generate a video from a prompt. Some models also accept reference images **for style** (referenced as `@Image1` / `@Image2` in the prompt) — these guide look, NOT motion | | video | first-last-frame | **Animate a still image** — provide one image as the start frame and the model generates motion outward. Optional end frame interpolates between two keyframes | | video | motion-control | Make an image move using motion **copied from a reference video** (e.g. the character in the image performs the action / dance in the reference clip) | | video | audio-lipsync | Make a character image lip-sync to a given audio track | | video | extend-video | Continue an existing video — append more frames after its last one | | video | video-edit | Restyle or re-edit an existing video clip | | audio | text-to-speech | TTS in a named voice | | audio | voice-clone | Speak text using a voice cloned from a reference audio sample | | audio | sound-effect | One-shot sound effect from a text description | | audio | music | Music track from a text description | For the per-model required fields (which image / video / audio params, and which are optional), read `params[].required` on the model returned by `/v1/models`. Don't infer from the subType — different models in the same subType can have different param requirements. ### Disambiguation — pick subType by intent The hard cases are video subTypes (and image edits), where the user's **goal** decides, not which media they happen to hand you: | User says… | Pick | Why | | ---------------------------------------------------------------------------------------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | "把这张图的背景换成海边" / "remove the person on the left" / "edit this photo to…" (image + text only, no mask) | **image/default** | Plain-text edits go through `default` with GPT Image 2 or Nanobanana — they take a reference image + edit prompt natively. `edit-image` is the wrong pick because it requires a mask the user didn't provide. | | "我画了个 mask / 涂了一块区域,只改这里" | **image/edit-image** | The user actually has a mask or hand-drawn region. That's the only time `edit-image` is right. | | "做个矢量图 / SVG" / "我要可缩放可编辑的 logo 矢量文件" / "as an SVG / vector" | **image/image-svg** | The deciding signal is **vector / SVG**, not the word "logo" or "icon" — those alone usually mean a normal raster image (use `default`). Only route here when the user wants an editable, infinitely scalable vector file. | | "让这张图动起来" / "animate this poster" | **video/first-last-frame** | Goal: bring a still image to life. The image is a *start frame*. | | "用这张图当风格参考生成视频" / "use this image as a style reference" | **video/text-ref** | Goal: generate a new scene; image only shapes look. Reference image goes into the model's `image_urls` / `reference_image_urls` param and is cited as `@Image1` in the prompt. | | "让这个角色跳这段舞" / "make this character do this dance" | **video/motion-control** | Goal: transplant motion from a reference video onto an image. | | "让这个人对口型说这段话" / "lip-sync this audio to my photo" | **video/audio-lipsync** | Goal: drive an image's mouth from audio. | | "续写这段视频" / "extend this clip" | **video/extend-video** | Goal: more frames after the last one. | | "把这段视频换风格" / "restyle this video" | **video/video-edit** | Goal: change look/feel of existing video. | | Pure prompt → video, no input image | **video/text-ref** | Goal: generate from text alone. | Each model has one or more `versions` (e.g. `standard` / `pro` / `fast`) with a coarse `typicalPrice` summary like `{ typicalCredits: 8, unit: "second", note: "~40 credits for 5s @ 720p" }`. That's enough to explain ballpark cost to the user. For an **exact** quote, call `POST /v1/quote` (see "Quote before submit"). Final cost is enforced server-side at submit time and echoed in `creditsCharged`. ## Standard workflow 1. **(Optional, first call only)** `GET /v1/me` — remaining daily budget, wallet balance (personal or team), bound project name, rate-limit numbers. 2. **Discover models** for the matching subtype: ```bash curl -fsS "$ZOOOP_API_HOST/v1/models?type=video&subtype=motion-control" \ -H "Authorization: Bearer $ZOOOP_API_KEY" ``` Match the user's hint (e.g. "用 seedance2") against `name` / `brand.name`. No hint → follow "Default model selection" below. 3. **(Optional) Upload local files.** If the user gave a path on disk: ```bash bash scripts/upload.sh /path/to/file.png # → prints the storage URL to feed into params. ``` Wraps `POST /v1/uploads`. Image / audio return sync; video is processed asynchronously — the script polls until ready (5–30s typical). Rejected content → exits non-zero. 4. **Quote the task** — exact credits + ETA, no side effects: ```bash bash scripts/quote.sh '' # → { "credits": 8, "estimatedSeconds": 18, "breakdown": {...} } ``` Safe to repeat. See "Quote before submit" for the confirmation policy. 5. **Submit the task**: ```bash bash scripts/submit.sh '' # → { "taskId": "...", "status": "queued", "modelId": "...", "versionId": "..." } ``` 6. **Poll until terminal** (`succeeded` / `failed` / `cancelled`): ```bash bash scripts/poll.sh # → { "status": "succeeded", "outputs": [{"url": "..."}], "creditsCharged": 4 } ``` 7. **Show the URL, then offer download.** Proactively ask *"Want me to download it to a local file?"* (unless the user already said they only want the URL) — on yes, run: ## Quote before submit Before every `POST /v1/tasks`, call `POST /v1/quote` with the **same body** to get exact credits + P50 ETA. No charge, no DB row, no capacity hold — safe to call as often as you like. ```bash curl -fsS -X POST "$ZOOOP_API_HOST/v1/quote" \ -H "Authorization: Bearer $ZOOOP_API_KEY" \ -H "Content-Type: application/json" \ -d '{"interfaceId":"...","versionId":"...","params":{...}}' # → { "credits": 8, "estimatedSeconds": 18, "modelName": "Kling V3", # "breakdown": { "pricingKey": "720p", "base": 8, "multiplier": 1, ... } } ``` After the quote, print **one compact summary line** before submitting — just the agent's choice + cost, so the user can sanity-check it. Don't echo the prompt back; they just typed it and re-reading it is noise. ``` → Kling V3 · 720p · 8 credits · ~18s ``` **Confirmation policy** (don't confirm on every task — annoying): | Situation | Behaviour | | ------------------------------------------------ | ---------------------------------------------------------- | | Default — single task, no opt-in/out | Print summary line, submit. No "should I continue?". | | User said "不用问我" / "just go" / "make N variants" | Submit silently, no extra confirmation. | | User said "ask me before spending" / "贵的让我确认" | Confirm before submitting. | | Batch (≥ 3 tasks OR total ≥ 100 credits) | Confirm once for the whole batch unless already opted out. | | `estimatedSeconds == null` | Say "ETA unknown" — don't invent a number. | If the quote returns 400 / 404, fix the payload and re-quote — never submit a request that just failed to quote. ## Default model selection When the user's request is vague and does NOT name a model / brand / quality tier, pick the curated defaults below. ### Images (`type=image, subtype=default`) - Default model: **GPT Image 2.0** with `quality: "low"`, `resolution: "2k"`, `aspect_ratio: "1:1"`. ~80% of cases are fine at `low`; `medium` handles ~95% of needs. Also strong for prompt-driven edits. - **Quality-sounding words inside the user's prompt are prompt-words, not param hints.** Phrases like "highest quality", "最高质量", "4k", "ultra-realistic", "cinematic", "电影级" appear as decorative descriptors in almost every prompt — they do NOT mean the user is asking you to bump the `quality` param. Keep `quality: "low"`. - Bump to `quality: "medium"` when the request is non-trivial (detailed composition, important visual fidelity) or when a `low` result didn't satisfy. - `quality: "high"` only for genuinely complex requests (intricate detail, fine typography, multi-subject composition) OR when the user explicitly passes `params.quality: "high"`. - Switch to a different image model only when the user explicitly asks (e.g. "use Flux" / "用 Seedream"). Don't switch on a quality complaint. ### Videos (`type=video, subtype=text-ref`) | User signal | Pick | | --------------------------------------- | ------------------------------------------------------------------------------------------------------- | | "highest quality" / "best" / "电影级" | **Seedance 2** | | Balanced (default — no explicit signal) | **Kling O3** → fallback to **Kling V3** → **Happy Horse** → **Grok Imagine** (pick first one available) | | "cheap" / "fastest" / "性价比" / "便宜" | **Grok Imagine** | Match by `name` (case-insensitive substring) or `brand.name` against `/v1/models`. ### Other categories For every other (type, subType), use `**models[0]`** from `/v1/models` — it's pre-sorted, so the first row is the recommended default. ## Reference image → prompt (`POST /v1/describe-image`) Turn a user-supplied reference image into a structured, ready-to-feed prompt. **Call this only when** you need to extract a specific dimension from the image (subject / style / lighting / palette / …), OR the user explicitly asks to reverse-engineer a prompt from a picture. Otherwise **skip it** — most image / video models accept a reference image directly through their own param (e.g. `image_urls` / `reference_image_urls`), so feed the picture straight into the submit without a describe round-trip (saves a credit and a step). ```bash bash scripts/describe.sh [language] # language: optional locale code (zh / ja / fr / es / …); omitted → English # → { "credits": 1, "overallDescription": "A vibrant ...", "subject": "...", # "composition": "...", "style": "...", "lighting": "...", "palette": "...", # "mood": "...", "camera": "..." } ``` - Input MUST be a ZOOOP CDN URL (returned by `/v1/uploads`). Foreign URLs are rejected — pipe local files through `upload.sh` first. - Costs 1 credit per call. - Optional `language` sets the output language — every field (including `overallDescription`) comes back in it. Pass the locale the user is working in so they can read the result; unknown / omitted → English. - `overallDescription` is the canonical one-paragraph prompt; other fields are best-effort and may be empty when the model can't tell. ## Endpoints | Method | Path | What | | ------ | ------------------------ | ------------------------------------------------------- | | GET | `/v1/me` | Self-introspection | | GET | `/v1/models` | List public models by `type` × `subtype` | | GET | `/v1/ai-tools` | List curated AI tools (optional `?type=image|video`) | | GET | `/v1/ai-tools/{slug}` | Full param schema for one tool | | POST | `/v1/quote` | Price a task (accepts `interfaceId` OR `aiTool`) | | POST | `/v1/tasks` | Submit a generation (accepts `interfaceId` OR `aiTool`) | | GET | `/v1/tasks/{id}` | Poll status / outputs | | POST | `/v1/uploads` | Upload a file (raw body + `Content-Type`) | | GET | `/v1/uploads/{uploadId}` | Poll async (video) upload status | | POST | `/v1/describe-image` | Reference image → structured prompt (1 credit) | Full request / response shapes live in `references/api-docs.md`. ## Reading a model's params schema `/models` returns `params: InterfaceParam[]` per model. Each entry carries `id` (the key to set in `params`), `type`, `required`, `options`, `constraints`, `default`. Media-URL types (`image_url`, `video_url`, `audio_url`, `first_frame`, `last_frame`, `mask_url`, `voice`, …) accept only ZOOOP-hosted URLs — pipe foreign URLs / local paths through `upload.sh` first. Full type list and behaviour: `references/api-docs.md`. ## Uploads — single-step raw-body ```bash curl -X POST "$ZOOOP_API_HOST/v1/uploads" \ -H "Authorization: Bearer $ZOOOP_API_KEY" \ -H "Content-Type: image/png" \ --data-binary "@$HOME/Desktop/cat.png" ``` `Content-Type` drives how the file is processed AND the resulting file extension — pass the file's REAL mime, not a guess. Image / audio return sync (`{ "status": "ready", "url": ..., "size": ..., "contentType": ... }`). Video returns `{ "status": "processing", "uploadId": ..., "pollUrl": ... }` — then poll `GET /v1/uploads/{uploadId}` until `ready` / `blocked` / `errored`. Allowed mimes, size cap (100 MB), and full poll-status shapes: see `references/api-docs.md` → `POST /v1/uploads`. ## Error response shape OpenAI-style envelope: ```json { "error": { "message": "...", "type": "invalid_request_error", "code": "missing_required_params", "params": [...] } } ``` - `type` — broad family: `invalid_request_error` / `authentication_error` / `permission_error` / `rate_limit_error` / `api_error`. Branch on `code`. - `param` — set when one field is at fault. - `missing_required_params` carries a structured `params` array with `id`, `type`, `default`, `options`, `constraints` — self-correct in one round-trip without re-fetching the model. Most common codes (full table in `references/api-docs.md`): | HTTP | code | Meaning | | ---- | -------------------------- | ------------------------------------------------ | | 400 | `missing_required_params` | Required model params absent — see `params` hint | | 400 | `invalid_payload` | Bad body shape | | 401 | `invalid_token` | Token missing / revoked / expired | | 402 | `token_daily_cap_exceeded` | This task's cost would breach the daily cap | | 404 | `unknown_model` | Bogus `interfaceId` or disabled model | | 422 | `moderation_blocked` | Text or image violates content policy | | 422 | `token_project_unbound` | Bound project deleted — revoke + recreate token | | 429 | `rate_limited` | Honor `Retry-After` | | 503 | various | No enabled provider — try another model | ## Transient errors Up to 3 attempts with exponential backoff (1s → 3s → 9s), honor `Retry-After`. Retry only: connection errors with no HTTP status, `5xx`, `429`. Don't retry other `4xx`, `451 moderation_blocked`, or `422 token_project_unbound`. Full retry table + outage-diagnostic curls: `references/api-docs.md` → "Transient errors". ## Idempotency (optional) Pass `Idempotency-Key: ` on `POST /v1/tasks`. Server caches the key for 24h; repeat submits with the same `(token, key)` return the original `taskId` plus `"idempotent": true`. Stripe semantics. ## Rate limits Per-PAT, 60s sliding window. Headline: `tasks 60/min`, `quote 120/min`, `uploads 30/min`, `me / upload-poll / ai-tools 120/min`, `describe-image 10/min`. `/models` and `/tasks/{id}` are unbounded. `429` carries `Retry-After`. Full table: `references/api-docs.md` → "Limits". ## What this skill does NOT do - It does **not** manage projects, canvases, or organizations — single-shot generation only. PAT's bound project is fixed at creation time. - It does **not** stream progress — poll instead. - It does **not** auto-download outputs; always **ask** first (step 7) and pick the save extension from response `content-type`. - It does **not** allow uploads > 100 MB via the API — direct the user to the Web UI for those. --- # API Reference # ZOOOP Public REST API (v1) ## Quick start ```bash # 1. Create a Personal Access Token at https://zooop.ai/user#apiKeys export ZOOOP_API_KEY=zpk_live_... # (Quote every URL: zsh treats `?` as a glob and `&` as a job separator.) # 2. Discover models for the kind of task you want curl -fsS -H "Authorization: Bearer $ZOOOP_API_KEY" \ "https://api.zooop.ai/v1/models?type=image&subtype=default" # 3. Submit a task with one of the model ids + a version id curl -fsS -X POST -H "Authorization: Bearer $ZOOOP_API_KEY" \ -H "Content-Type: application/json" \ -d '{"interfaceId":"","versionId":"standard","params":{"prompt":"a red panda"}}' \ "https://api.zooop.ai/v1/tasks" # 4. Poll for the result curl -fsS -H "Authorization: Bearer $ZOOOP_API_KEY" \ "https://api.zooop.ai/v1/tasks/" ``` ## Authentication All endpoints require `Authorization: Bearer `. Tokens look like `zpk_live_<32 chars>` and are created at . The plaintext is shown ONCE at creation; afterwards only the leading prefix is visible in the settings UI. To rotate, create a new token and revoke the old one (revocation is immediate). A token is either **personal** (created at the URL above) or **team** (issued by a team owner/admin in the team admin → API Keys tab). Credits are debited from that token's wallet — the user's personal balance for a personal token, or the team's shared credit pool for a team token. The endpoints are identical; `GET /v1/me` reports which wallet funds the token. **Project binding**: every PAT is bound to one project at creation time and cannot be re-bound. Tasks submitted via the PAT land under that project; uploaded files do too. If the bound project is later deleted, all submits return 422 `token_project_unbound` — the user must revoke + recreate. See `GET /v1/me` for the bound project info. ## Error envelope All errors follow an OpenAI-style envelope so SDKs that parse that shape work out of the box: ```json { "error": { "message": "Missing required parameters: aspect_ratio, resolution", "type": "invalid_request_error", "code": "missing_required_params", "params": [ { "id": "aspect_ratio", "type": "aspect_ratio", "required": true, "default": "1:1", "options": [{"value": "1:1", "label": "1:1"}, {"value": "16:9", "label": "16:9"}] }, { "id": "resolution", "type": "resolution", "required": true, "default": "1024", "options": [{"value": "1024", "label": "1024 × 1024"}, ...] } ] } } ``` - `type` — broad family: `invalid_request_error`, `authentication_error`, `permission_error`, `rate_limit_error`, `api_error`. - `code` — stable machine-readable reason. Branch on this in client code. - `param` — set when the failure points at a single body / query field. - Free-form extras (`validCategories`, `spent`, `cap`, `params`, `labels`, …) live under `error` too and are stable for any documented `code`. - For `missing_required_params`, the `params` array carries structured hints per missing field (`id` / `type` / `required` / `default` / `options` / `constraints`) — agents can self-correct in one round-trip without re-fetching the model. Per-endpoint error tables below. ## Endpoints ### `GET /v1/me` PAT self-introspection. Returns everything the calling agent needs to plan its requests without trial-and-error 429s / 402s — bound project, daily credit headroom, account balance, current rate-limit numbers. ```bash curl -fsS -H "Authorization: Bearer $ZOOOP_API_KEY" \ "https://api.zooop.ai/v1/me" ``` Response: ```json { "key": { "id": "uuid", "prefix": "zpk_live_aBcD…", "label": "claude-code-mac", "createdAt": "2026-05-15T10:00:00.000Z", "expiresAt": null, "dailyCreditCap": 1000, "creditsSpentToday": 42, "creditsRemainingToday": 958 }, "project": { "id": "uuid", "name": "My Project" }, "user": { "creditBalance": 84495 }, "limits": { "tasksPerMin": 60, "uploadsPerMin": 30, "uploadPollPerMin": 120 } } ``` Field notes: - `key.dailyCreditCap` is `null` when no cap is set; `creditsRemainingToday` is also `null` in that case. - `key.expiresAt` is `null` when the token never expires. Warn the user to renew before this date. - `project.id` / `project.name` are both `null` (with `project.bound: false`) when the bound project was deleted — submits will fail with `token_project_unbound` until the token is rotated. - **Wallet** — exactly one key is present: `user: { creditBalance }` for a **personal** token, or `team: { id, creditBalance }` for a **team** token (the team's shared pool; a team token never exposes its creator's personal balance). This is the wallet that funds tasks — even with `creditsRemainingToday > 0`, an empty wallet still blocks expensive submits. - `limits.*` reflect the current per-PAT rate-limit numbers. Honor them. All PATs grant full access to every `/v1/*` endpoint. Privacy: deliberately omits `user.id / email / name` (PAT bearer doesn't need user identity — the token itself is the identity) and never enumerates other PATs of the same user. ### `GET /v1/models?type=&subtype=` Lists the enabled model interfaces that match one `(type, subType)` pair. Both query params are **required** — calling without them returns 400 plus the full set of valid `(type, subType)` pairs so the caller can self-correct. Exposure rule: a `(type, subType)` pair is exposed via the public API iff the corresponding generator tool is currently in the homepage sidebar. The single source of truth is `src/lib/seo/tool-pages-seo.ts` (`inSidebar: true`). Response shape: ```json { "models": [ { "id": "uuid", "name": "Flux Pro", "type": "image", "subType": "default", "description": "Photorealistic text-to-image model …", "brand": { "id": "flux", "name": "Black Forest Labs", "logo": "https://..." }, "promptRequired": true, "promptMaxLength": 2000, "params": [ { "id": "prompt", "type": "long_text", "required": true, "maxLength": 2000 }, { "id": "aspect_ratio", "type": "aspect_ratio", "required": false, "enumValues": ["1:1","16:9","9:16"] } ], "versions": [ { "id": "standard", "name": "Standard", "typicalPrice": { "typicalCredits": 4, "unit": "image", "note": "1024×1024, 1:1" } }, { "id": "pro", "name": "Pro", "typicalPrice": { "typicalCredits": 8, "unit": "image", "note": "1024×1024, 1:1" } } ], "tags": ["HOT"] } ] } ``` ### Public categories | type | subType | Tool | | ------ | ------------------ | ------------------------------- | | image | default | Image generator | | image | edit-image | Image editor | | image | image-svg | SVG generator (text/image-to-vector) | | video | text-ref | Video generator | | video | motion-control | Motion control (image-to-video) | | video | first-last-frame | First / last frame video | | video | audio-lipsync | Lip sync | | video | extend-video | Extend video | | video | video-edit | Video editor | | audio | text-to-speech | Text-to-speech | | audio | voice-clone | Voice clone | | audio | sound-effect | Sound effect | | audio | music | Music generation | `typicalPrice.unit` values: - `image` — credits per generated image (total at default dimensions) - `second` — credits per second (output for duration-defaulted models, OR per second of input for extend / lipsync-style models) - `frame` — credits per frame of input video - `1k_chars` — credits per 1000 characters (TTS; all per-character pricing is normalized to this unit so the number is always a relatable integer / one-decimal value rather than a fractional per-char rate) - `call` — credits per call (flat, no dimension scaling) For flat units (`image`, `call`) `typicalCredits` is an integer and the `minPrice` floor is already applied. For rate units (`second`, `frame`, `1k_chars`) it may be fractional (e.g. `50.5`) to preserve fidelity for sub-credit rates. `typicalPrice` is a coarse ballpark only — call `POST /v1/quote` (below) with the exact payload you'd submit to get the authoritative number. ### `GET /v1/ai-tools[?type=image|video]` List active AI tools — admin-curated recipes that pre-bind a model + version + tuned params for a specific outcome (background removal, age modification, style transfer, video upscale, etc.). Agents submit by stable `slug` and supply only the recipe's visible inputs instead of configuring the raw underlying interface. Optional `?type=image|video` filters by media type. Other values return 400 `invalid_type`. There is **no** `subtype` query — the recipe's `subType` field is admin free-form ("特效", "动作模仿") and not a filter axis. Exposure rule: a tool appears iff (a) `recipe.isActive = true`, (b) the recipe carries the `api` tag (admin opt-in — fail-closed by default), and (c) its underlying interface is still enabled. This is a deliberately narrow surface: most /tools/* recipes are prompt-wrappers around generic text-to-image models that an agent could replicate cheaper by calling the raw model directly. Only the tools that genuinely add capability beyond `/v1/models` (background removal, upscalers, etc.) are tagged. Response: ```json { "aiTools": [ { "slug": "background-removal", "name": "Background Removal", "mediaType": "image", "subType": "", "summary": "Remove the background and keep only the subject.", "coverUrl": "https://storage.zooop.ai/...", "tags": ["NEW"], "params": [ { "id": "image_url", "type": "image_url", "required": true, "displayName": "" } ], "typicalPrice": { "typicalCredits": 2, "unit": "image" } }, { "slug": "style-transfer", "name": "Style Transfer", "mediaType": "image", "params": [ { "id": "image_url", "type": "image_url", "required": true }, { "id": "target_style", "type": "enum", "required": true, "options": [ { "value": "impressionism", "label": "Impressionism" }, { "value": "cubism", "label": "Cubism" } ] }, { "id": "aspect_ratio.ratio", "type": "aspect_ratio", "required": true, "options": ["1:1","16:9","9:16","4:3","3:4"] } ], "typicalPrice": { "typicalCredits": 6, "unit": "image" } } ] } ``` The `params[]` shape mirrors `/v1/models` exactly — same `id` / `type` / `required` / `options` / `default` / `constraints` semantics. Voice-type params on TTS-style tools expand options to `{value, label, previewUrl?}` identical to `/v1/models`. Fields the agent doesn't need (mapping internals, preset overrides, hidden / fixed params) are stripped. ### `GET /v1/ai-tools/{slug}` Fetch one AI tool by stable slug. Same shape as a single entry from the list endpoint. Returns 404 `unknown_ai_tool` for unknown / inactive slugs and for slugs whose interface is no longer enabled. ```bash curl -fsS -H "Authorization: Bearer $ZOOOP_API_KEY" \ "https://api.zooop.ai/v1/ai-tools/background-removal" ``` ### `POST /v1/quote` Price a hypothetical task without submitting. Same request body as `POST /v1/tasks` (so an agent can reuse one payload for both calls). No credits are charged, no DB row is created, no capacity is reserved — safe to repeat freely. Body — **one of two shapes**, mutually exclusive: ```json // (a) Raw model { "interfaceId": "", "versionId": "standard", "params": { "prompt": "…", "duration": 5 } } // (b) AI tool (curated recipe) { "aiTool": "", "params": { "image_url": "https://storage.zooop.ai/…" } } ``` Sending both `aiTool` and `interfaceId` returns 400 `invalid_payload`. Response: ```json { "credits": 8, "estimatedSeconds": 18, "modelId": "uuid", "versionId": "standard", "modelName": "Kling V3", "breakdown": { "pricingKey": "720p", "base": 8, "multiplier": 1, "surcharges": 0, "basePrice": 0, "minPriceApplied": false } } ``` - `credits` — authoritative cost for this exact payload. The `POST /v1/tasks` call WILL charge this number, provided input media durations don't change between quote and submit (media duration is part of the pricing basis). - `estimatedSeconds` — P50 of the last 10 successful generations on this model. `null` when the model has no completion history yet. - `breakdown` — itemised cost so the agent can explain "why" to the user: `final = max(minPrice, ceil(base × multiplier + surcharges + basePrice))`. Errors mirror `POST /v1/tasks` for the same payload-validation paths (`invalid_payload`, `missing_required_params`, `unknown_model`, `model_not_public`). Quote also has its own `rate_limited` 429 (default 120 / min per token). ### `POST /v1/tasks` Submit one generation task. Two body shapes are accepted — mutually exclusive. Body — **raw model path**: ```json { "interfaceId": "", "versionId": "standard", "params": { "prompt": "…", "image_url": "…", "duration": 5 } } ``` Body — **AI tool path** (curated recipe): ```json { "aiTool": "", "params": { "image_url": "https://storage.zooop.ai/…" } } ``` On the AI-tool path the server resolves slug → recipe → underlying interfaceId + versionId, merges your `params` with the recipe's fixedParams / defaultParams / customParams, validates the merged shape, and applies the recipe's pricing override. The keys allowed in `params` come from the tool's `params[]` spec returned by `/v1/ai-tools/{slug}`. On the raw path, the keys come from the model's `params[]` spec returned by `/v1/models`. Param values are validated server-side via the same path the web `/api/ai/execute` route uses. **Optional header** `Idempotency-Key: <≤256 chars>` — repeat submissions with the same `(token, key)` within 24 hours return the original `taskId` plus `"idempotent": true` instead of creating a duplicate. Stripe semantics. Response: ```json { "taskId": "uuid", "status": "queued", "modelId": "...", "versionId": "..." } ``` Errors: | HTTP | code | Cause | | ---- | -------------------------- | ------------------------------------------- | | 400 | `invalid_payload` | Missing required input, bad shape, OR both `aiTool` and `interfaceId` set | | 400 | `missing_required_params` | Model `required: true` params absent; `error.params` lists each with type / options / default | | 400 | `invalid_idempotency_key` | `Idempotency-Key` header missing or > 256 chars | | 401 | `missing_token` / `invalid_token` | Missing or revoked / expired PAT | | 402 | `arrears` | Account in arrears | | 402 | `token_daily_cap_exceeded` | This task's cost would breach the token's daily cap (post-pricing precise check) | | 403 | `model_not_public` | Raw-path model exists but isn't on the public API surface (does not apply to AI-tool path) | | 403 | `account_banned` | Token's owner is banned | | 404 | `unknown_model` | Bogus `interfaceId` or disabled model | | 404 | `unknown_ai_tool` | Bogus `aiTool` slug, inactive recipe, or underlying interface disabled | | 422 | `moderation_blocked` | Text or image violates content policy | | 422 | `token_project_unbound` | PAT's bound project was deleted — revoke + recreate | | 429 | `rate_limited` | Per-token rate-limit ceiling (default 60 req/min) | | 429 | `token_daily_cap_reached` | Token's `dailyCreditCap` already met before this call (coarse pre-check) | | 503 | various | No enabled provider — pick a different model | ### `GET /v1/tasks/{id}` Returns the current state of a task. Path-id is the `taskId` from submit. ```json { "taskId": "uuid", "status": "succeeded", "outputs": [{ "url": "https://storage.zooop.ai/cdn-cgi/image/format=png,quality=95/.../uuid.webp", "thumbnailUrl": "https://storage.zooop.ai/cdn-cgi/image/format=png,quality=95/.../uuid-thumb.webp" }], "error": null, "creditsCharged": 4, "createdAt": "2026-05-14T01:23:45.000Z", "completedAt": "2026-05-14T01:24:12.000Z" } ``` Public `status` values: `queued | running | succeeded | failed | cancelled`. Internal granular states (`pending`, `processing`, `uploading`, `moderating`) are collapsed into `running` since they're not actionable for agents. **Image output URLs** are served via Cloudflare Image Transformations (`cdn-cgi/image/format=png,quality=95`). The `format=png` is a soft hint that Cloudflare honours selectively: - Source has **alpha** (background-removal, transparent stickers) → response is **`image/png`** with transparency preserved. - Source is fully **opaque** (typical t2i / i2i output) → Cloudflare downgrades to **`image/jpeg`** to keep delivery size small. This is Cloudflare's standard behaviour and there is no URL switch to override. Either way the response `content-type` reflects the actual format. Trust the header, not the URL pathname (which still ends in `.webp` — that's the source-object key, not the delivered format). When saving locally, pick the extension from `content-type`: ```bash URL="https://storage.zooop.ai/cdn-cgi/image/format=png,quality=95/.../uuid.webp" CT=$(curl -sI "$URL" | awk 'tolower($1)=="content-type:" {print $2}' | tr -d '\r') EXT=$(case "$CT" in image/png) echo png;; image/jpeg) echo jpg;; *) echo bin;; esac) curl -fL -o "out.$EXT" "$URL" ``` Video / audio URLs pass through unchanged. ### `POST /v1/uploads` Upload a local file in a single request. The file content is the raw HTTP body (NOT multipart). `Content-Type` selects the media type. Returns a public URL ready to feed into a subsequent `/tasks` submission. ```bash curl -X POST \ -H "Authorization: Bearer $ZOOOP_API_KEY" \ -H "Content-Type: image/png" \ --data-binary @cat.png \ https://api.zooop.ai/v1/uploads ``` The upload lands under the PAT's bound project (same path as Web UI uploads), so it shows up in the "Uploads" tab of the in-app history picker and counts toward your storage quota. **Image / audio — synchronous response** (typically < 1s): ```json { "status": "ready", "url": "https://storage.zooop.ai///.png", "size": 123456, "contentType": "image/png" } ``` **Video — asynchronous**. Returns `202` with an upload id: ```json { "status": "processing", "uploadId": "", "pollUrl": "/v1/uploads/" } ``` Poll `GET /v1/uploads/{uploadId}` until you get a terminal status. Recommended cadence: 5s first poll, then 10–20s. Most clips resolve in under a minute; the workflow has a 30-minute deadline. **Image rejected by content policy**: ```json { "error": { "message": "Uploaded content violates content policy", "type": "invalid_request_error", "code": "moderation_blocked", "labels": "porn" } } ``` HTTP 451. **Allowed Content-Type values**: `image/png`, `image/jpeg`, `image/webp`, `image/gif`, `audio/mpeg`, `audio/wav`, `audio/webm`, `audio/ogg`, `video/mp4`, `video/webm`, `video/quicktime`. Off-list values return 400 with the allowlist in the response. **Size cap**: 100 MB (Cloudflare request-body limit). If you need to upload something larger, use the Web UI then pass that URL to `/tasks`. ### `GET /v1/uploads/{uploadId}` Poll the status of an async (video) upload returned by `POST /v1/uploads`. ```bash curl -H "Authorization: Bearer $ZOOOP_API_KEY" \ https://api.zooop.ai/v1/uploads/ ``` Response shapes: ```json { "status": "processing" } # 202, still moderating { "status": "ready", "url": "...", "contentType": "..." } # 200, ready to use { "status": "blocked", "labels": "porn", "error": "..." } # 200, rejected { "status": "errored", "reason": "..." } # 200, workflow gave up ``` Ownership: the upload id can only be polled by the PAT that created it. Other tokens (even from the same user account) get 404 `unknown_upload`. ### `POST /v1/describe-image` Analyse a reference image and return a structured prompt suitable for feeding back into a generator (`subject` / `composition` / `style` / `lighting` / `palette` / `mood` / `camera`, plus a single flowing `overallDescription` paragraph). Body: ```json { "imageUrl": "https://storage.zooop.ai///.png", "language": "zh" } ``` - `imageUrl` is **required** and MUST be a ZOOOP CDN URL (i.e. one returned by `POST /v1/uploads`). Foreign URLs are rejected — this avoids SSRF and guarantees the input has already been validated. - `language` is **optional** — a locale code (`zh`, `zh-TW`, `ja`, `fr`, `es`, `de`, `pt`, `ru`, … ) that sets the output language. Every field, including `overallDescription`, is written in it; pass the locale the user is working in so the result is readable to them. Unknown / omitted → English. Response: ```json { "credits": 1, "subject": "A young woman with light skin in her 20s", "composition": "Centered portrait, eye-level framing", "style": "Soft photographic realism with film grain", "lighting": "Warm window light from the left, gentle shadows", "palette": "Muted earth tones with desaturated greens", "mood": "Contemplative and serene", "camera": "Shot at 50mm with shallow depth of field", "overallDescription": "A young woman with light skin in her 20s, ..." } ``` - `overallDescription` is the canonical "ready-to-use" field — drop it straight into a generator's `prompt` param. The other dimension fields are best-effort and may be empty strings depending on the active vision model. Treat `overallDescription` as the only field with strong stability. - `credits: 1` is charged on success (failures are refunded — see the error table below). - `camera` may be `null` for compositionally minimal images. Errors: | HTTP | code | Cause | | ---- | ----------------------- | -------------------------------------------------------- | | 400 | `invalid_payload` | `imageUrl` missing or not on a ZOOOP CDN host | | 402 | `insufficient_credits` | Balance below 1 credit (no refund issued — nothing was charged) | | 422 | `policy_violation` | Vision model refused the image on content policy (refund issued) | | 429 | `rate_limited` | Default 10 / min per PAT | | 429 | `upstream_rate_limited` | Upstream LLM rate-limited us (refund issued — retry with backoff) | | 503 | `vision_unavailable` | Vision service unreachable (refund issued) | ## Limits - **Rate limit** (per PAT, 60-second sliding window): - `POST /v1/tasks` — 60 req/min - `POST /v1/quote` — 120 req/min - `POST /v1/uploads` — 30 req/min - `GET /v1/uploads/{id}` — 120 req/min - `GET /v1/me` — 120 req/min - `GET /v1/ai-tools` and `GET /v1/ai-tools/{slug}` — 120 req/min - `POST /v1/describe-image` — 10 req/min - `GET /v1/models`, `GET /v1/tasks/{id}` — unbounded - 429 responses carry `Retry-After` (seconds). - **Tokens per user**: max 10 active. Revoke an old one to create a new one. - **Daily credit cap (optional)**: per token; UTC-midnight rollover. Two enforcement points — coarse pre-check (`token_daily_cap_reached`, 429) and precise post-pricing check (`token_daily_cap_exceeded`, 402, prevents over-cap submission even when current spend < cap). - **Concurrency**: `maxConcurrency` (personal token → the user's, default 3; team token → the team's shared pool) caps how many tasks run in parallel — excess queues in FIFO order and drains as earlier tasks complete. Rate-limit and concurrency are orthogonal: you can submit 60/min but only a few run at once. - **Content policy**: prompts are checked before any credits are charged; uploaded images are checked synchronously on `POST /v1/uploads`, uploaded videos asynchronously via the poll route. Rejections surface as `moderation_blocked`. - **Upload size**: 100 MB hard ceiling per file (Cloudflare request-body limit). Larger files: use the Web UI and pass the URL. ## Out of scope - **Workspace management**: a token's scope (personal vs team) and bound project are fixed at creation in the web UI — the API can't switch them. A team token is issued by a team owner/admin and spends the team's shared credits; otherwise the endpoints are identical to a personal token. - **Project management**: tokens are bound to one project at creation. To switch projects, revoke and create a new token. - **Canvases / story timeline**: the API exposes single-shot generation only. - **Streaming progress**: poll instead. - **Listing tasks / uploads**: no `/tasks?since=` or `/uploads?since=` endpoints — track task IDs client-side. ## Transient errors — what to retry, how to diagnose A single failed request often has nothing to do with the API. Retry the transient class with exponential backoff before declaring an outage. **Retry (transient, usually self-heals):** - TCP / TLS errors with no HTTP status — `ECONNRESET`, `EAI_AGAIN`, curl exit codes `6` (DNS), `7` (connect), `35` / `52` / `56` (TLS / recv). - HTTP `502` / `503` / `504`. - HTTP `429` — honor `Retry-After`. - Cloudflare `520`–`526`. **Do NOT retry:** - Other `4xx` (auth / validation — retry won't help). - `451 moderation_blocked` — same bytes will be rejected again. - `422 token_project_unbound` — bound project deleted; user must rotate the token. **Pattern:** up to 3 attempts, exponential backoff 1s → 3s → 9s, honor `Retry-After`. If all 3 fail, surface the error. **Diagnostic before declaring outage:** ```bash curl -fsS -o /dev/null -w "%{http_code}\n" https://zooop.ai curl -fsS -o /dev/null -w "%{http_code}\n" https://api.zooop.ai/llms.txt ``` If `zooop.ai` is up but `api.zooop.ai` keeps failing, only THEN report an outage — otherwise it's almost always local network or transient.