ZSky API + MCP
Developer API and MCP access: add a prepaid balance with no subscription — or use your Pro or Max plan. Both use the same generation pipeline as the web app: photographic image generation, video with synchronized audio, AI Creative Director output. Prepaid balance (no subscription): add $10, $25, $50 or $100, top up any time, and the balance never expires; each request draws it down at the same rates (see prepaid balance). Pro ($19 a month) is pay per use from the first second: video is billed per second of requested output by mode and resolution tier (Fast $0.10 per second in HD, $0.15 in Full HD, $0.20 in 2.5K; Quality $0.30 in HD, $0.50 in Full HD; Cinematic $0.70 in 2.5K), images $0.05 each ($0.08 above 1 MP), with a monthly cap you choose from $5 to $1,000. Max ($99 a month, or $79 a month billed annually) includes 3,000 video-seconds + 1,000 images per calendar month at any resolution (Cinematic seconds count double), then the same prices if you turn pay per use on. Allowances reset on the 1st of each calendar month, 00:00 UTC. See pricing and every limit.
Authentication
The public API base URL is https://zsky.ai/api. Every API request and MCP
connection uses a dedicated API key through the X-API-Key header. Create or
rotate the key from Settings → API + MCP, or get one from the
terminal with the quickstart. A rotated key is shown
only once and immediately invalidates the previous key, so rotate only when you are ready
to update every client that uses it.
curl https://zsky.ai/api/v1/usage \
-H "X-API-Key: zsky_live_..." \
-H "User-Agent: my-app/1.0"
MCP setup
ZSky MCP is a local stdio adapter for compatible MCP clients, including Claude Code and
Claude Desktop. It connects to the live ZSky API; it is not a separate hosted endpoint.
The adapter requires Python 3.10 or newer. It starts without a key: the first tool call
returns a payment path, zsky_start_access gives your agent one sign-in link to
show you, and zsky_finish_access stores the key at ~/.zsky/api_token
(readable only by you). The key is never shown in the chat.
- Download the package from Settings → API + MCP (Pro and Max accounts), or with your key:
curl -OJ -H "X-API-Key: $ZSKY_API_TOKEN" -H "User-Agent: my-app/1.0" https://zsky.ai/api/v1/me/mcp-package(-OJkeeps the server's file name, which carries the current version). - Install the downloaded
zsky_mcp-*.whlpackage (below). - Add
zsky-mcpto your MCP client. A key inZSKY_API_TOKENis optional; without one, the adapter walks you through getting a key. - Restart the MCP client, then call
zsky_aboutorzsky_usageto confirm the connection.
Install the adapter
python3 -m venv .venv
.venv/bin/pip install ./zsky_mcp-*.whl
On Windows, in PowerShell, use .venv\Scripts\python -m pip install (Resolve-Path .\zsky_mcp-*.whl).
Claude Code
Without a key (the adapter gets one on first use):
claude mcp add zsky -s user -- /absolute/path/to/.venv/bin/zsky-mcp
With a key you already have:
claude mcp add zsky -s user \
-e ZSKY_API_TOKEN=YOUR_ZSKY_API_KEY \
-e ZSKY_MCP_MEDIA_ROOTS=/absolute/path/to/approved/media \
-- /absolute/path/to/.venv/bin/zsky-mcp
ZSKY_MCP_MEDIA_ROOTS is optional for text-to-image and text-to-video. It is
required for image-to-video and must name only directories containing images you want
ZSky MCP to read. Supported inputs are PNG, JPEG, and WebP files up to 12 MiB.
Claude Desktop and other stdio clients
Add the following server to your MCP client configuration, using absolute paths:
{
"mcpServers": {
"zsky": {
"command": "/absolute/path/to/.venv/bin/zsky-mcp",
"env": {
"ZSKY_API_TOKEN": "YOUR_ZSKY_API_KEY",
"ZSKY_MCP_MEDIA_ROOTS": "/absolute/path/to/approved/media"
}
}
}
}
Available tools are zsky_about, zsky_generate_image,
zsky_generate_video, zsky_animate_image,
zsky_check_status, zsky_usage, zsky_delete_output,
zsky_start_access, and zsky_finish_access. Generation is asynchronous: generation tools return a
job ID, and zsky_check_status returns progress and the signed result URL.
zsky_delete_output.
Endpoints
| Method | Path | Purpose |
|---|---|---|
| POST | /v1/images/generate | Create an image (text-to-image) |
| POST | /v1/videos/generate | Create a video (text-to-video) |
| GET | /v1/jobs/<id> | Poll job status, get signed result URL |
| DELETE | /v1/jobs/<id> | Purge the result from storage |
| GET | /v1/usage | Current-month allowance counters |
| GET | /v1/me/overage | Pay-per-use state: on or off, monthly cap |
| POST | /v1/me/overage/cap | Set the monthly cap (cap_cents, 500 to 100000) |
| POST | /v1/me/overage/toggle | Turn pay per use on or off |
| GET | /v1/me/mcp-package | Download the MCP package |
| POST | /v1/pay/start | Start a key claim (no key needed) |
| GET | /v1/pay/claim/<code> | Poll a claim; returns the key once when ready |
POST /v1/images/generate
curl -X POST https://zsky.ai/api/v1/images/generate \
-H "X-API-Key: zsky_live_..." \
-H "User-Agent: my-app/1.0" \
-H "Content-Type: application/json" \
-d '{
"prompt": "A neon-lit alley in Tokyo at dusk, photorealistic",
"width": 1024,
"height": 1024
}'
# Response (HTTP 200):
{
"job_id": "5f3...",
"status": "queued",
"kind": "image",
"poll_url": "/api/v1/jobs/5f3..."
}
Parameters:
prompt(required, string, ≤2000 chars) — the image promptwidth(optional, int, 256–2048, default 1024)height(optional, int, 256–2048, default 1024)seed(optional, int, 0–2147483647) — for reproducible outputs
POST /v1/videos/generate
curl -X POST https://zsky.ai/api/v1/videos/generate \
-H "X-API-Key: zsky_live_..." \
-H "User-Agent: my-app/1.0" \
-H "Content-Type: application/json" \
-d '{
"prompt": "A solitary lighthouse beam sweeping across choppy black sea, cinematic",
"duration": 5
}'
# Response (HTTP 200):
{
"job_id": "8a1...",
"status": "queued",
"kind": "video",
"poll_url": "/api/v1/jobs/8a1..."
}
Parameters:
prompt(required, string, ≤2000 chars)width(optional, int, 256–2560, default 1280; Cinematic defaults to 2560). In Fast mode, HD requests in 16:9, 9:16, 1:1, 3:2 or 2:3 are delivered at the exact 720p size for that shape (for example 1280×720); any other shape is rendered at its own size on the 64-pixel grid, at the same price.height(optional, int, 256–2560, default 720; Cinematic defaults to 1440)duration(optional, number, 1–10, default 5) — preferred duration in seconds; snapped to a multiple of a third of a second (8 frames at 24 fps), so 6.2 becomes 6. The snapped length is the length you are billed for.mode(optional, string, default"fast"):"fast","quality"or"cinema"(Cinematic). Fast renders 1 to 10 seconds at any size from 256 to 2560 px. Quality renders HD or Full HD clips of 5, 7 or 10 seconds. Cinematic renders 2.5K clips;rates.video_modes.cinema.durations_secondslists its lengths (5 seconds today). Quality and Cinematic need one of the five public shapes (16:9, 9:16, 1:1, 3:2 or 2:3), deliver exact sizes (for example 1920×1080 in Full HD) and do not lip-sync uploaded audio. A request a mode cannot render is refused with a 400 before anything is charged.seed(optional, int, 0–2147483647)init_image_base64(optional, strict base64 string) — PNG, JPEG, or WebP up to 12 MiB before encoding
Price: the seconds you request times the rate for the mode and the tier of the longer side of the delivered
video (see pricing). The accepted response returns mode, output_width,
output_height and price_tier, the tier you are billed at. The default 5-second
1280×720 Fast clip is $0.50 when pay per use applies.
Video generation is asynchronous. Use the poll_url to check status.
For image-to-video, include init_image_base64 with the motion prompt. The MCP
adapter handles this conversion through zsky_animate_image after validating
that the local file is inside ZSKY_MCP_MEDIA_ROOTS.
GET /v1/jobs/<id>
curl https://zsky.ai/api/v1/jobs/8a1... -H "X-API-Key: zsky_live_..." -H "User-Agent: my-app/1.0"
# While queued/running:
{ "job_id": "8a1...", "status": "queued", "kind": "video" }
# When completed:
{
"job_id": "8a1...",
"status": "completed",
"kind": "video",
"result_url": "https://...r2.cloudflarestorage.com/...&X-Amz-Signature=...",
"expires_in": 86400
}
# If blocked by safety:
{
"job_id": "8a1...",
"status": "blocked",
"kind": "video",
"error": "Sorry — our safety check stopped this video..."
}
result_url is a 24-hour signed download URL pointing directly at our storage.
Download it once and persist locally if you need the bytes longer.
DELETE /v1/jobs/<id>
Purges the generated asset from our storage before the 7-day auto-delete. Useful when you've downloaded the bytes and want them gone immediately. Returns HTTP 204 on success, 404 if the job doesn't exist or isn't yours.
GET /v1/usage
curl https://zsky.ai/api/v1/usage -H "X-API-Key: zsky_live_..." -H "User-Agent: my-app/1.0"
# Response (Max key; video is metered in seconds, clip counts kept for older clients):
{
"videos_used": 47,
"videos_overage": 0,
"images_used": 180,
"images_overage": 0,
"overage_charged_cents": 0,
"base_videos_allowance": 300,
"base_images_allowance": 1000,
"video_seconds_used": 341,
"video_seconds_overage": 0,
"base_video_seconds_allowance": 3000,
"rates": {
"video_per_second_cents": {"hd": 10, "fhd": 15, "2k": 20},
"default_video_mode": "fast",
"video_modes": {
"fast": {"per_second_cents": {"hd": 10, "fhd": 15, "2k": 20}, "max_included_weight": 1},
"quality": {"per_second_cents": {"hd": 30, "fhd": 50}, "max_included_weight": 1, "durations_seconds": [5, 7, 10]},
"cinema": {"per_second_cents": {"2k": 70}, "max_included_weight": 2, "durations_seconds": [5]}
},
"video_tiers": {"hd": "longer rendered side <= 1280", "fhd": "<= 1920", "2k": "<= 2560"},
"image_cents": {"standard": 5, "large": 8},
"image_sizes": {"standard": "<= 1,048,576 pixels (1024x1024)",
"large": "above 1,048,576 pixels, up to 2048x2048"},
"max_video_seconds": 10,
"render_grid_px": 64
},
"month_start": "2026-10-01"
}
API key management
Create or rotate the dedicated key from Settings → API + MCP, or from the terminal with the quickstart. Save it immediately: the plaintext value is shown only once, and the previous key is invalidated as soon as rotation succeeds.
Pay per use and your cap
The full list of limits is in Limits. On Pro, every API and MCP generation is pay per use and needs a monthly cap: choose one when you get your key ($10, $50, $250, or any amount from $5 to $1,000). A new account's cap is up to $50 for the first 7 days after its first payment, unless a $50 or $100 balance purchase confirmed with its bank has opened the full range. On Max, pay per use is off by default: a request past the included 3,000 video-seconds or 1,000 images stops with HTTP 429 until the monthly reset, unless you turn pay per use on and set a cap. A clip is included only if it fits whole in the included seconds you have left; otherwise the whole clip is billed at its tier rate and the seconds left stay available for shorter clips.
Pay-per-use pricing, per second of video, by mode and by the tier of the longer side of the delivered video:
Fast $0.10 in HD (up to 1280 px), $0.15 in Full HD (up to 1920 px) and $0.20 in 2.5K (up to 2560 px);
Quality $0.30 in HD and $0.50 in Full HD; Cinematic $0.70 in 2.5K. You are billed for
the seconds you request (Fast 1 to 10 per clip; Quality 5, 7 or 10; Cinematic 5 today). Images: $0.05 up to
1,048,576 pixels (1024×1024) and $0.08 above that, up to 2048×2048. Fast output snaps to a
64-pixel grid (1920×1080 renders as 1920×1088); Quality and Cinematic deliver exact sizes, and a Quality Full HD
square is delivered at 1080×1080 and billed HD. The response's price_tier is the tier you are billed at.
No discount by length: the same rate applies to every second.
| Request | Tier | Price |
|---|---|---|
| 5 s Fast video, 1280×720 | Fast HD, $0.10 per second | $0.50 |
| 10 s Fast video, 1920×1080 | Fast Full HD, $0.15 per second | $1.50 |
| 10 s Fast video, 2560×1440 | Fast 2.5K, $0.20 per second | $2.00 |
| 5 s Quality video, 1920×1080 | Quality Full HD, $0.50 per second | $2.50 |
| 5 s Cinematic video, 2560×1440 | Cinematic 2.5K, $0.70 per second | $3.50 |
| Image, 1024×1024 | Up to 1 MP | $0.05 |
| Image, 2048×2048 | Above 1 MP | $0.08 |
A cap buys up to, in Fast HD: $10 = 100 video-seconds or 200 images; $50 = 500 or 1,000; $250 = 2,500 or 5,000.
Every 429 that asks for money and GET /v1/usage carry the same table as data in rates. You are charged at
month-end, on the card on your web subscription, for what you used, and never more than your cap in
a calendar month. A plan bought in the App Store or Google Play (Pro or Max) cannot use pay per
use; it needs a web subscription with a card on file.
Change the cap or turn pay per use on or off in Settings → API + MCP, or
from the API with POST /v1/me/overage/cap and POST /v1/me/overage/toggle.
A request that would pass your cap returns HTTP 429 with code: "monthly_cap_reached".
Prepaid balance
No subscription: add $10, $25, $50 or $100 on the claim page or from the
terminal, and top up any time. Each API or MCP request draws the balance down at the
prices above when it is accepted, and a request that fails or is blocked puts its amount back. A request the
balance does not cover is refused with HTTP 402 balance_insufficient before anything runs; it is
never billed later. Your balance never expires.
Optional auto-recharge adds the amount you choose ($10, $25, $50 or $100) with the card you paid with when the
balance drops below $2, inside the monthly cap you set. If the card is declined, auto-recharge turns off, you
get one email, and a request the balance does not cover returns 402 auto_recharge_declined:
add to your balance with a working card (auto-recharge then uses that card). If you also have Pro or Max, pay per use draws the balance first
(after Max’s included allowance) and the rest follows your plan’s month-end billing.
GET /v1/me/balance returns balance_cents, auto_recharge, your cap and your
latest purchases, each with spent_cents and refundable_until.
POST /v1/me/balance/auto-recharge with {"enabled": true, "amount_usd": 25} turns it on
({"enabled": false} turns it off). GET /v1/usage also carries balance_cents.
A balance purchase can be refunded within 24 hours if none of it has been used. After that, or once any of it is spent, it is final. Your balance never expires. A $50 or $100 balance purchase may ask you to confirm with your bank; once confirmed, it opens the full limits right away: auto-recharge up to $100 and a monthly cap up to $1,000. Otherwise, auto-recharge adds up to $25 at a time until the card has been on your account for 7 days, and a new account's monthly cap is up to $50 for the first 7 days after its first payment.
Storage & Retention
- Signed download URLs are valid for 24 hours after issuance.
- Generated assets remain available for re-fetch via
GET /v1/jobs/<id>for 7 days, then auto-delete. - Customer owns the output — full rights and title to anything generated through your account.
- API outputs never appear in /explore or the public community feed.
- Zero-Data-Retention option: add header
X-ZSky-Store: 0to any generate request — the bytes are returned inline and never persisted. - Watermarking: API outputs carry no visible wordmark. An invisible provenance watermark is embedded in every image.
Safety
Every API request flows through the same safety pipeline as our web app:
input checks on prompts, output checks on generated content. Blocked generations
return status blocked with a human-readable reason. A blocked output is not
charged and does not count against your allowance.
Repeated rejected requests (more than 50% of the last 30 minutes' attempts, minimum 10 events) trigger an automatic 24-hour suspension on the API key. If suspended, you'll get HTTP 403 until the window clears. Email support if you believe the suspension is in error.
Error Codes
| Status | Meaning |
|---|---|
200 | OK |
204 | No content (DELETE success) |
400 | Malformed request body or invalid params. new_account_cap_limit, new_account_recharge_limit or new_card_recharge_limit: the cap or auto top-up you asked for is above what this account or card can use yet; the body's limits object and raises_at say what is allowed now and when it rises. invalid_mode, mode_resolution_unsupported, mode_duration_unsupported or mode_input_unsupported: the mode cannot render this request; the body lists what it supports, and nothing is charged. With an uploaded image, Quality and Cinematic answer a prompt they cannot follow with a 400 prompt code. |
401 | Missing or invalid API key. The body includes a payment object that starts the quickstart flow. |
402 | payment_required: the account has not paid for API access (add a prepaid balance, or choose Pro or Max). balance_insufficient: the prepaid balance does not cover this request (the body has balance_cents, cost_cents and a top_up link); add to your balance or turn on auto-recharge. auto_recharge_declined: the card declined an auto-recharge and the balance does not cover this request; add to your balance with a working card. card_required: pay per use on Pro or Max needs a card on file from a web subscription. arrears: an earlier API invoice is unpaid; pay it in settings, then pay per use works again. |
403 | API key suspended, or a request refused at the edge. Send a User-Agent that identifies your app; requests with a library's default User-Agent may be refused with 403. account_review_required: API payments on this account are paused; contact [email protected]. |
404 | Job not found (or not owned by you) |
429 | Rate or daily limit hit (rate_limited, which also covers too many unrecognised keys from your network); quota_exhausted (included allowance used and pay per use off; for video the body names the video_mode and, on Max, the included_video_seconds_needed); monthly_cap_reached (the request would pass your cap); claims_per_client_limit (5 open sign-in codes from your network). Retry after the Retry-After header. |
451 | Generation blocked by safety filter |
500 | Internal error — please retry |
503 | Temporary: workers unavailable, usage_unavailable, overage_temporarily_unavailable, billing_verification_unavailable, claims_busy, mode_unavailable (that mode has no capacity right now; Fast stays available), mode_temporarily_unavailable, mode_prompt_unavailable (Quality or Cinematic could not prepare a text prompt; try again or use Fast) or video_validation_unavailable. Retry after the Retry-After header. |
Sample Python Client
#!/usr/bin/env python3
"""Minimal ZSky API client — generate an image, poll, download."""
import os, time, json, urllib.request
API = "https://zsky.ai/api"
KEY = os.environ["ZSKY_API_KEY"]
def call(method, path, body=None):
req = urllib.request.Request(
f"{API}{path}",
data=json.dumps(body).encode() if body else None,
# Send your own User-Agent: our edge rejects the default "Python-urllib" one.
headers={"X-API-Key": KEY, "Content-Type": "application/json",
"User-Agent": "my-app/1.0"},
method=method,
)
with urllib.request.urlopen(req, timeout=30) as r:
return json.loads(r.read())
def generate_image(prompt):
job = call("POST", "/v1/images/generate", {"prompt": prompt})
job_id = job["job_id"]
while True:
time.sleep(2)
state = call("GET", f"/v1/jobs/{job_id}")
if state["status"] in ("completed", "failed", "blocked"):
return state
state = generate_image("A coffee cup on a wooden table, photorealistic")
if state["status"] == "completed":
print(f"Result: {state['result_url']}")
urllib.request.urlretrieve(state["result_url"], "out.webp")
else:
print(f"Failed: {state.get('error')}")
Versioning & Stability
The v1 API surface is stable. Breaking API changes will ship under /v2.
The current MCP adapter version is 2.1.3.
Support
Bugs, questions, and daily-limit lifts: write to [email protected] from your account email so we can verify ownership. Keys and the MCP package come from your account, not by email.