Build with the Zappy API
Create and edit images from your own code or AI assistant — the same Zappy creative engine as the Studio, with your credits, your limits and one simple REST contract.
Available to every verified Zappy account · create a key yourself · REST, MCP and Remote MCP
Get started
- Sign in at zappyquote.com and verify your email address.
- Open Account → Developer, name a key, choose its permissions, and create it.
- Copy the key. It is shown once — if you lose it, rotate the key rather than hunting for it.
The key spends the credits of the account that created it, at the same prices as the Studio. Machine-readable contract: OpenAPI 3.1 document.
Quickstart
Six calls, from key to finished image. Every example uses a placeholder key — replace zp_live_YOUR_KEY with the key you were given.
1. Keep your key in an environment variable
export ZAPPY_API_KEY="zp_live_YOUR_KEY"2. See what you can create
curl -s https://zappyquote.com/api/v1/capabilities \
-H "Authorization: Bearer $ZAPPY_API_KEY"3. Ask what it will cost
curl -s https://zappyquote.com/api/v1/quote \
-H "Authorization: Bearer $ZAPPY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"capability":"zappy-image-pro"}'4. Create it
curl -s https://zappyquote.com/api/v1/images \
-H "Authorization: Bearer $ZAPPY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: my-first-image-001" \
-d '{"prompt":"a red fox in fresh snow, golden hour","aspect_ratio":"1:1"}'You get one of two answers. Handle both:
{
"ok": true,
"job_id": "01J...",
"status": "succeeded",
"capability": "zappy-image-pro",
"credits_quoted": 15,
"assets": [ { "asset_id": "01M...", "kind": "image" } ]
}{
"ok": true,
"job_id": "01J...",
"status": "processing"
}5. If you got 202, poll the job
curl -si https://zappyquote.com/api/v1/jobs/01J... \
-H "Authorization: Bearer $ZAPPY_API_KEY"While it says processing, wait the number of seconds in Retry-After (also retry_after in the body) and ask again. When it says succeeded, it carries the assets.
6. Download the result
curl -s https://zappyquote.com/api/v1/assets/01M... \
-H "Authorization: Bearer $ZAPPY_API_KEY"The answer holds a short-lived signed url and its expires_at. Fetch the file from that URL; ask again for a fresh link later.
Authentication
Send your key on every request as a bearer token. Keys start with zp_live_.
Authorization: Bearer zp_live_YOUR_KEY- Header only. A key in a URL ends up in logs and browser history, so it is never accepted there.
- Shown once. Zappy stores only a one-way hash of your key. If you lose it, it cannot be recovered — ask for it to be revoked and a new one issued.
- Scoped. Each key carries only the permissions it was issued with:
| Scope | Allows |
|---|---|
generation:read | Job status and your creations list |
generation:write | Create images and image edits (spends credits) |
assets:read | Fetch a download link for your assets |
billing:read | Your balance, and quotes |
webhooks:read | List your webhook endpoints and recent deliveries (API keys only) |
webhooks:write | Add, pause, resume, rotate, revoke and redeliver webhook endpoints (API keys only) |
agent:write | Approve, advance and stop Zappy Agent runs within a credit budget you set (API keys only) |
Managing your keys
Everything about a key is in Account → Developer: what it may do, its ceilings, when it was last used, how many credits it has spent today, and the two controls that matter.
| Control | What happens |
|---|---|
| Revoke | The key stops working immediately and permanently. Anything using it starts getting 401 unauthorized on its next call. This cannot be undone. |
| Rotate | The old key is revoked and a replacement with the same name, permissions and ceilings is shown once. Update every integration using the old one. |
Zappy stores only a one-way hash of your key, so there is no "show key again" — not as a policy, but because the value genuinely is not kept. The first few characters are shown so you can tell keys apart.
If a key leaks, revoke it yourself. You do not need to contact support and you should not wait for anyone: revocation takes effect on the very next call.
Capabilities
What the API can create today. This table is generated from the API's own route table. Whether each capability can be called right now is answered live by GET /api/v1/capabilities, which lists only what you can use at this moment — a capability that is temporarily unavailable is not listed, and is never quoted or charged.
| Capability | What it does | Operation | Answers |
|---|---|---|---|
zappy-image-pro | Create an image from a text prompt. | POST /api/v1/images | 200 or 202 |
zappy-image-edit | Edit an image you already created with Zappy, guided by a prompt. | POST /api/v1/images/edit | 200 or 202 |
Current credit prices come from GET /api/v1/capabilities and POST /api/v1/quote, never from a page, so they are always the prices you will actually be charged. Zappy chooses the best engine for each job behind the scenes; the capability you call is the contract.
Quotes, credits and billing
- Your account pays. The API spends the Zappy credits of the account the key belongs to — the same credits the Studio uses.
GET /api/v1/balanceshows them. - Quote first, if you like.
POST /api/v1/quotewith acapabilityreturnscredits_quoted. A quote reserves nothing and charges nothing. - Charged only for work that is delivered. Credits are held while a job runs and charged when it succeeds. If a generation fails, or finishes but can never be delivered, the credits come back automatically — the job then reports
credits_restored. - Your own ceiling. Send
max_creditson any paid call to refuse anything that would cost more. The lower of that and your key's per-request cap wins.
Images and edits
Every paid call needs an Idempotency-Key header (see Idempotency).
Create an image — POST /api/v1/images
{
"prompt": "a lighthouse on a cliff at dusk, cinematic",
"aspect_ratio": "16:9",
"max_credits": 30
}Edit an image — POST /api/v1/images/edit
source_asset_id is the asset_id of an image your account already created with Zappy. Uploading your own files through the API is not offered yet - create or edit within Zappy first.
{
"source_asset_id": "01M...",
"prompt": "make it night, with warm window light"
}Jobs and polling
GET /api/v1/jobs/{job_id} is the single source of truth for a job.
| status | reason | What it means | What to do |
|---|---|---|---|
processing | — | Your job is being generated. | Wait Retry-After (15 s), then poll again. |
processing | delivery_pending | Generation complete — Zappy is preparing your asset. | Wait Retry-After (60 s). Polling faster does not make it arrive sooner. |
succeeded | — | Done. assets lists what was made. | Stop polling. Fetch each asset. |
failed | varies | The job ended without a result. Any credits for it were restored. | Stop polling. You may start a NEW job with a new Idempotency-Key. |
failed | delivery_unavailable | Generation finished but could never be delivered. credits_restored says what came back. | Stop polling. |
Retry-After. Its absence is your signal to stop. Delivery normally completes within a few minutes of generation; a job still processing after about 30 minutes is worth an email to support — do not re-send it.import os, time, requests
API = "https://zappyquote.com/api/v1"
H = {"Authorization": "Bearer " + os.environ["ZAPPY_API_KEY"]}
r = requests.post(f"{API}/images", headers={**H, "Idempotency-Key": "order-42-hero"},
json={"prompt": "a red fox in fresh snow"}, timeout=120)
job = r.json()
while job.get("status") == "processing":
time.sleep(int(r.headers.get("Retry-After", 15)))
r = requests.get(f"{API}/jobs/{job['job_id']}", headers=H, timeout=30)
job = r.json()
if job.get("status") == "succeeded":
for a in job["assets"]:
link = requests.get(f"{API}/assets/{a['asset_id']}", headers=H, timeout=30).json()
print(link["url"])
else:
print("not delivered:", job.get("reason"), "restored:", job.get("credits_restored"))Assets and creations
GET /api/v1/assets/{asset_id}returnskind,mime,bytes,sha256, a signedurlandexpires_at. Links are short-lived by design; request a new one whenever you need it.GET /api/v1/creations?limit=25lists the jobs your account created through the API, newest first. Passnext_cursorback ascursorfor the next page.limitis 1–100.- You only ever see your own account's jobs and assets. Anything else answers
404 not_found, exactly as if it did not exist.
Idempotency — never pay twice
Every paid call (/images, /images/edit) requires an Idempotency-Key header: 1–100 letters, digits, . _ : -. Use one key per thing you want made.
- Sending the same key again never charges again — it returns the same job.
- If a paid call times out or the answer is unclear, do not send a new request. Re-send with the SAME Idempotency-Key, or poll the
job_idif you have one. A new key means a new, separately charged job. - Reusing a key with a different request body is refused (
idempotency_conflict).
Spending caps on a key
Every key carries three ceilings. New keys start at these values, and you may set any of them LOWER when you create the key:
| Ceiling | Default on a new key | Most you may set | When a call passes it |
|---|---|---|---|
| Daily credit cap | 150 credits / day (UTC) | 2000 | 429 daily_cap_reached — resets at midnight UTC |
| Per-request cap | 60 credits | 400 | 400 request_exceeds_ceiling |
| Rate limit | 20 requests / minute | 60 | 429 rate_limited with Retry-After |
Credits held for jobs still running count toward the daily cap, so running jobs in parallel cannot get you past it. You can hold up to 5 active keys at once; revoke one to create another.
max_credits on a request is a fourth ceiling you set per call. It can only ever LOWER the limit for that call — the lower of it and your key cap applies, and it can never raise either.
Seeing where you stand
GET /api/v1/usage (scope billing:read) reports the calling key’s own day: credits_spent_today — the exact figure the daily cap is enforced against, including credits held for running jobs — credits_remaining_today, its ceilings, and how many of its jobs today succeeded, failed or are still processing. It never describes another key. A number that cannot be read right now is null, never 0.
Every answer to an authenticated key carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (seconds until the minute window resets), and every response carries an X-Request-Id you can log on your side to tell calls apart. When you contact support about a job, quote its job_id.
Recipes
A recipe is a curated Zappy intent — “a premium product hero shot” — with a starting direction that has a few blanks for you to fill. The Recipe API lists them, composes one with your values, quotes it and runs it. It adds no pricing or billing of its own: a run is exactly POST /api/v1/images with the recipe’s direction and framing, so the same quote, ceilings, daily cap, idempotency and job polling apply.
| Call | Scope | Spends |
|---|---|---|
GET /api/v1/recipes | any key | nothing |
GET /api/v1/recipes/{slug} | any key | nothing |
POST /api/v1/recipes/{slug}/quote | billing:read | nothing |
POST /api/v1/recipes/{slug}/run | generation:write | credits (send Idempotency-Key) |
Send {"inputs": {"<input>": "<value>"}, "max_credits": 50}. Every input the recipe names is required; values are cleaned and limited to 120 characters. You cannot send your own prompt to a recipe — use POST /api/v1/images for that.
Runnable through the API today: product-hero-image, lifestyle-product-scene, restaurant-promotion, product-launch-creative. The other 7 recipes (video and ad workflows) say "runnable": false, answer 409 recipe_not_api_runnable, and point at their Studio page.
curl -s https://zappyquote.com/api/v1/recipes/product-hero-image/quote \
-H "Authorization: Bearer $ZAPPY_API_KEY" -H 'Content-Type: application/json' \
-d '{"inputs":{"your_product":"a ceramic coffee mug"}}'Zappy Agent
The Zappy Agent turns a short goal into a plan, prices it, and — once you approve a credit budget — carries it out one step at a time. It adds no pricing or billing of its own: every step is an ordinary POST /api/v1/images job with the same price, key caps, daily cap and idempotency.
| Call | Scope | Spends |
|---|---|---|
POST /api/v1/agent/plan | billing:read | nothing |
POST /api/v1/agent/runs | generation:write + agent:write | nothing (this is your approval) |
POST /api/v1/agent/runs/{id}/advance | generation:write + agent:write | at most one step |
GET /api/v1/agent/runs, GET /api/v1/agent/runs/{id} | generation:read | nothing |
POST /api/v1/agent/runs/{id}/stop | agent:write | nothing |
- Plan. Send
{"goal": "hero images of my ceramic mug", "variants": 3}. The agent picks the best-matching recipe (or therecipeyou name), fills its inputs from your goal (or yourinputs), and plans up to 4 distinct variations, each priced from the live price list. - Approve. Send the same body to
POST /api/v1/agent/runswith"max_credits_total"(at least the planned total) and"approve": true. The server re-plans from your body, so the plan you approve is the one that runs. - Advance. Send
{"step": N}(the run’snext_step). It starts step N only, with what is left of your budget as its ceiling. Repeating the same step never starts anything else — it answers step N’s job — so a lost response is recovered by sending the same step again. - Stop.
POST /api/v1/agent/runs/{id}/stop,zappy agent stop, or the Stop button in Account → Developer — always available. A step started before the stop still settles and is recorded. Revoking the key that approved a run also ends it.
What the agent will not do: spend beyond the budget you approved; run two steps at once; retry a failed step (a failed step, or one the paid route refuses for balance, caps or price, stops the run and says why); or act for an app connected through OAuth or Remote MCP (agent:write exists on API keys only). One running run per account; every transition is in the run’s audit log.
zappy agent plan "hero images of my ceramic mug" --variants 3
zappy agent run "hero images of my ceramic mug" --variants 3 --max-credits-total 60 --confirm-billable --wait
zappy agent advance ar_0123456789abcdef --step 1 --confirm-billable
zappy agent stop ar_0123456789abcdefWebhooks
Instead of polling, Zappy can POST a signed JSON event to your HTTPS endpoint when an API job finishes. Add endpoints in Account → Developer (up to 5), or through the API. Events cover jobs created through this API.
Managing endpoints through the API
With an API key carrying webhooks:read / webhooks:write: GET /api/v1/webhooks, POST /api/v1/webhooks ({"url", "events"}; the signing secret is returned once), POST /api/v1/webhooks/{id}/pause|resume|rotate|revoke and POST /api/v1/webhooks/{id}/redeliver. These are the same rules as the Developer page — public https:// destinations only, the same limits and retries. The webhook scopes exist on API keys only: an app connected through OAuth or Remote MCP can never be granted them, so it can never redirect your events. Creating a second endpoint for a URL already in use answers 409 endpoint_exists; rotate that endpoint if you need a new secret.
Event types
job.completedjob.failedasset.readyrequest.refunded
request.refunded is sent when a job that could not be delivered had its credits restored automatically.
{"id":"evt_3f2a9c01b7d4e5f6a7b8c9d0","type":"job.completed","version":"1","occurred_at":"2026-10-17T12:00:00Z","data":{"job_id":"01HZX...","capability":"zappy-image-pro","status":"succeeded"}}Payloads carry ids and status only — never your prompt, your files or anything about another account. Fetch details with GET /api/v1/jobs/{id} and GET /api/v1/assets/{id}.
Verifying a delivery
Each request carries Zappy-Webhook-Id (the event id), Zappy-Delivery-Id (new on every attempt), Zappy-Event-Type and Zappy-Signature: t=<unix seconds>,v1=<hex>. To verify:
- Take the raw request body exactly as received (do not re-serialise the JSON).
- Compute HMAC-SHA256 of
<t>.<raw body>with your signing secret; hex-encode it. - Compare with
v1in constant time. - Reject the request if
tis more than 5 minutes from your clock. - Only after the signature checks out, read
idfrom the JSON body and deduplicate on it: delivery is at-least-once, so the same event can arrive more than once. The body is what the signature covers; theZappy-Webhook-Idheader always equals it and is there for logging — never trust a header that disagrees with the signed body.
$raw = file_get_contents('php://input');
preg_match('/t=(\d+),v1=([a-f0-9]{64})/', $_SERVER['HTTP_ZAPPY_SIGNATURE'] ?? '', $m);
$ok = $m && abs(time() - (int) $m[1]) <= 300
&& hash_equals(hash_hmac('sha256', $m[1] . '.' . $raw, $secret), $m[2]);The signing secret is shown once, when you add the endpoint or rotate it; afterwards only its fingerprint is shown. Rotating replaces it immediately.
Delivery and retries
- Answer with any
2xxwithin 4 seconds. Process slow work after you respond. 408,425,429,5xx, timeouts and network errors are retried with the same event id and body after 1 min, 5 min, 30 min, 2 h, 6 h — at most 6 attempts. Other4xxand any redirect are not retried.- Redirects are never followed. A paused endpoint receives nothing until resumed; a revoked endpoint receives nothing again. Undelivered events expire after 72 hours.
- You can redeliver a past event from Account → Developer. It is the same event with the same id.
Allowed destinations
HTTPS on port 443 to a public hostname only. URLs with credentials, IP addresses, or hosts that resolve to private, loopback, link-local or reserved addresses are refused, and the address is checked again before every delivery.
OAuth apps (connect other Zappy accounts)
An API key is for your own account. To let other Zappy users connect their accounts to your application, register an OAuth app in Account → Developer (up to 5). Each person approves your app on a Zappy consent screen that names it and lists exactly what it may do; they can disconnect it at any time under Connected apps.
Flow: authorization code with PKCE (S256 only)
- Create a random
code_verifier(43–128 chars) and itscode_challenge= BASE64URL(SHA-256(verifier)). - Send the person to
https://zappyquote.com/oauth/authorize?response_type=code&client_id=…&redirect_uri=…&scope=…&state=…&code_challenge=…&code_challenge_method=S256. Theredirect_urimust match a registered URI exactly (https, no wildcards). - On approval Zappy redirects to your URI with
?code=…&state=…; on refusal with?error=access_denied&state=…. Checkstate. - Within 5 minutes, POST to
https://zappyquote.com/oauth/token:grant_type=authorization_code,code,redirect_uri,code_verifier,client_id(+client_secretfor a confidential app). A code works once; using it twice revokes what it issued. - Call the API with
Authorization: Bearer <access_token>. Access tokens last 60 minutes. - Refresh with
grant_type=refresh_token. Every refresh returns a NEW refresh token; presenting an old one revokes the whole connection. A refresh can never add permissions.
Permissions are the API scopes (generation:read, generation:write, assets:read, billing:read). An app may only request scopes it was registered with — asking for more is refused, not narrowed. Calls made with an OAuth token follow exactly the same rules as an API key: the connecting account’s credits, a capped daily spend, rate limits, idempotency, ownership.
Confidential apps keep a client secret on a server (shown once, rotatable). Public apps (browser-only) have no secret and rely on PKCE. Self-service (dynamic) registration with native loopback redirect URIs is available for Remote MCP clients - see Remote MCP; owner-registered apps use https redirects on a public hostname. Revoke tokens at https://zappyquote.com/oauth/revoke.
Command line — zappy
The official CLI, @zappyquote/cli v0.2.0 — a thin client over this API. It embeds no credential, bypasses nothing, and never retries a paid call.
Install
curl -O https://zappyquote.com/dl/zappy-cli.mjs
node zappy-cli.mjs help # or: alias zappy='node /full/path/zappy-cli.mjs'Also on npm: npm install -g @zappyquote/cli and npm install @zappyquote/sdk. Also on PyPI: pip install zappyquote. Registry lag: npm serves CLI 0.1.1, npm serves the JS SDK 0.1.1, PyPI serves 0.2.1 - the newest methods (recipes, webhook management, the Zappy Agent) are in the downloads above until the registries are updated.
Set up (the key is read from a hidden prompt or ZAPPY_API_KEY, never from the command line)
zappy auth set-key # stored in ~/.config/zappy/credentials.json, mode 0600
zappy auth status # key prefix only; the key is never printed againRead (free)
zappy capabilities
zappy balance
zappy whoami # = GET /usage: today's spend against your caps
zappy quote zappy-image-pro
zappy creations list --limit 5
zappy jobs watch 01J...
zappy assets download 01M... --out result.png # verifies size and sha256Create (spends credits — explicit opt-in)
zappy image create "a lighthouse at dusk" --max-credits 50 --confirm-billable --wait
# prints: idempotency-key img-... <- keep it. To replay an unclear result:
zappy image create "a lighthouse at dusk" --max-credits 50 --confirm-billable --idempotency-key img-...A paid command refuses to run without --confirm-billable and --max-credits. It prints the idempotency key it used and never retries on its own: re-running with the same --idempotency-key returns the same job, never a second charge.
Add --json for machine-readable output. Exit codes: 0 ok · 1 error · 2 usage · 3 credential/scope · 4 refused by the API · 5 rate limited · 6 unreachable. Credentials are redacted from everything the CLI prints.
SDKs — JavaScript/TypeScript and Python
Small official client libraries, thin wrappers over this API. Both carry the contract pin openapi-1.0.0@eb409487719ef8a9 (the hash of the published OpenAPI document they were verified against), typed errors mapped from the error envelope, rate-limit metadata, idempotency support and a webhook signature helper. No business rule lives in them: the server remains authoritative.
JavaScript / TypeScript — @zappyquote/sdk v0.2.0 (Node 18+, zero dependencies, types included)
curl -O https://zappyquote.com/dl/zappy.mjs # and https://zappyquote.com/dl/zappy.d.ts for typesimport { Zappy, ZappyError, newIdempotencyKey, verifyWebhookSignature } from './zappy.mjs';
const zappy = new Zappy(); // reads ZAPPY_API_KEY
const { capabilities } = await zappy.capabilities(); // free
const quote = await zappy.quote({ capability: 'zappy-image-pro' }); // free, reserves nothing
// BILLABLE - opt in deliberately. maxCredits is a hard ceiling; keep the idempotency key with your own record.
const key = newIdempotencyKey('order-123');
const r = await zappy.createImage({ prompt: 'a lighthouse at dusk', maxCredits: quote.credits_quoted, idempotencyKey: key });
const job = r.status === 'succeeded' ? r : await zappy.waitForJob(r.job_id); // 202 => poll (free)
const asset = await zappy.getAsset(job.assets[0].asset_id); // short-lived signed URL on Zappy's host
// webhooks: verify the exact raw body against the Zappy-Signature header
const authentic = verifyWebhookSignature(process.env.ZAPPY_WEBHOOK_SECRET, req.headers['zappy-signature'], rawBody);Python — zappyquote v0.3.0 (Python 3.9+, standard library only)
curl -O https://zappyquote.com/dl/zappyquote.py # then: import zappyquoteMoving from zappy.py? The Python SDK is now zappyquote: download zappyquote.py, change import zappy to import zappyquote (nothing else changes), then delete your old project-local zappy.py. The short name zappy belongs to an unrelated Python project, and the two would overwrite each other.
from zappyquote import Zappy, ZappyError, new_idempotency_key, verify_webhook_signature
zappy = Zappy() # reads ZAPPY_API_KEY
caps = zappy.capabilities()["capabilities"] # free
quote = zappy.quote("zappy-image-pro") # free
# BILLABLE - opt in deliberately. max_credits is a hard ceiling; keep the idempotency key with your own record.
key = new_idempotency_key("order-123")
r = zappy.create_image("a lighthouse at dusk", max_credits=quote["credits_quoted"], idempotency_key=key)
job = r if r["status"] == "succeeded" else zappy.wait_for_job(r["job_id"]) # 202 => poll (free)
asset = zappy.get_asset(job["assets"][0]["asset_id"])
authentic = verify_webhook_signature(secret, request.headers["Zappy-Signature"], request.get_data())Also on npm: npm install -g @zappyquote/cli and npm install @zappyquote/sdk. Also on PyPI: pip install zappyquote. Registry lag: npm serves CLI 0.1.1, npm serves the JS SDK 0.1.1, PyPI serves 0.2.1 - the newest methods (recipes, webhook management, the Zappy Agent) are in the downloads above until the registries are updated.
Errors and metadata
Every failure is a typed error — ZappyAuthError (401, insufficient_scope, not configured), ZappyRateLimitError (429), ZappySpendError (ceiling, cap, balance, idempotency), ZappyNotFoundError, ZappyUnavailableError, ZappyTransportError — carrying the public code, the HTTP status, the X-Request-Id, the RateLimit-* window and Retry-After. Messages are the server's customer-safe text and never contain a credential.
Safe credential handling
- Pass the key from your secret store or
ZAPPY_API_KEY; never hard-code it, never pass it on a command line, never commit it. - The clients read only their own environment variables and redact every credential shape from errors and logs.
- Without a credential they fail safely before sending anything.
Development and testing
There is no sandbox key: reads and quotes are free and safe to run against production, and the paid examples run only when you opt in (ZAPPY_RUN_PAID=1 / --confirm-billable). The package test suites run in fixture mode against a local test server with a deterministic test double for generation, so a CI run can cover the whole flow — including idempotent replay — without spending anything.
Remote MCP (read) from an AI assistant
zappy mcp config # prints a Remote MCP client configuration with placeholder valuesRegister an OAuth app with Remote MCP ticked in Account → Developer, then connect from your client; see Remote MCP.
Versions and compatibility
Packages follow semantic versioning; a breaking change to a command, method, exit code or error class is a major release. The API stays /v1. Each release names the contract it was verified against (currently openapi-1.0.0@eb409487719ef8a9) and the API version it is compatible with. Current: CLI 0.2.0 · JS/TS SDK 0.2.0 · Python SDK 0.3.0.
Errors
Every error has the same shape. Branch on error.code, never on the message text.
{
"ok": false,
"error": { "code": "daily_cap_reached", "message": "This key has committed 140 of 150 credits today; this request needs 15." }
}| code | HTTP | Meaning | What to do |
|---|---|---|---|
unauthorized | 401 | The key is missing, unknown or revoked. | Check the header. If the key was revoked, ask for a new one. |
not_available | 403 | The Zappy API is paused, or this key is not enabled. | Your key is valid. Try again later. |
insufficient_scope | 403 | This key does not have the permission for this call. | Create a key with the permission you need. |
key_misconfigured | 403 | Your key is missing a usage limit. | Rotate the key in Account -> Developer to reissue it. |
rate_limited | 429 | Too many requests this minute. | Wait Retry-After seconds. |
daily_cap_reached | 429 | This key has reached its daily spending ceiling. | Wait until midnight UTC, or create a key with a higher ceiling. |
request_exceeds_ceiling | 400 | This request costs more than your per-request cap, or than the max_credits you sent. | Raise max_credits (up to your ceiling), or choose a smaller request. |
insufficient_credits | 402 | The account does not have enough credits. | Top up the account, then send the request again with a NEW Idempotency-Key. |
request_key_retired | 409 | That Idempotency-Key belongs to a request that already finished. | Poll its job, or start a new request with a new key. |
idempotency_key_required | 400 | A paid call was sent without an Idempotency-Key. | Add the header. |
idempotency_conflict | 409 | The Idempotency-Key was already used for a different request. | Use a new key for a new request. |
capability_retired | 410 | That capability has been retired and can no longer be created. | Use GET /api/v1/capabilities for what you can use now; jobs you already created stay readable. |
delivery_unavailable | 502 | Generated, but could not be delivered. Credits restored. | Start a new job if you still need it. |
busy | 503 | Zappy could not finish starting the job in time. No credits are held for it. | Start a NEW request with a NEW Idempotency-Key. |
not_found | 404 | No such job or asset on your account. | Check the id. |
When a paid call fails after a job was created, the error also carries the job_id — keep it; it is how support finds your job.
MCP clients — use Zappy from AI assistants
Zappy ships a Model Context Protocol server, so Claude, Cursor, Codex, OpenClaw, n8n and other MCP clients can create with Zappy on your behalf. It is a thin client over this same API: your key, your ceilings, your credits, and it cannot do anything the API would refuse. It is one file with no dependencies and needs Node 18 or newer.
Download
Download the server once and note where you saved it:
curl -O https://zappyquote.com/dl/zappy-mcp.mjsClaude Desktop
Add Zappy to claude_desktop_config.json:
{
"mcpServers": {
"zappy": {
"command": "node",
"args": ["/full/path/to/zappy-mcp.mjs"],
"env": { "ZAPPY_API_KEY": "zp_live_YOUR_KEY" }
}
}
}Cursor
The same block in .cursor/mcp.json for one project, or ~/.cursor/mcp.json for all of them.
Codex
In ~/.codex/config.toml:
[mcp_servers.zappy]
command = "node"
args = ["/full/path/to/zappy-mcp.mjs"]
env = { ZAPPY_API_KEY = "zp_live_YOUR_KEY" }OpenClaw and other stdio clients
Any client that launches a stdio MCP server takes the same command, args and env.
n8n
Add an MCP Client node, set the connection type to Command (stdio), use node with the argument /full/path/to/zappy-mcp.mjs, and put ZAPPY_API_KEY in its environment. Store the key as an n8n credential rather than typing it into the workflow, so it is not exported with the workflow JSON. Without MCP, n8n can call this REST API directly with an HTTP Request node and a Header Auth credential named Authorization with the value Bearer zp_live_YOUR_KEY.
Tools
Eight tools: zappy_list_capabilities, zappy_get_balance, zappy_quote, zappy_create_image, zappy_edit_image, zappy_get_job, zappy_get_asset, zappy_list_creations. The two creation tools are declared not read-only, so a well-behaved client asks you before calling them, and each one requires max_credits and idempotency_key.
Keep the key in the client’s environment or secret store — never in a prompt, a shared workflow export or a screenshot. If you would rather not put a key on your machine at all, use Remote MCP instead.
Remote MCP — connect without installing anything
Clients that connect to a hosted MCP server over HTTPS, rather than launching one on your machine, can use Zappy directly at:
https://zappyquote.com/mcpThere is no file to download and no key to paste. You sign in to Zappy in your browser, see exactly what the application is asking for, and approve it — the client receives a token, never your password and never an API key.
What you will be asked to approve
The consent screen lists each permission in plain language, and says separately and prominently when an application will be able to spend your credits. Read-only connections are the default: spending is never granted unless the application asked for it and you approved it.
Every connection is listed in Account → Developer, and disconnecting one revokes its access immediately.
For client developers
Remote MCP uses the same OAuth authority as OAuth apps. Self-service registration is open: an MCP client can register itself (RFC 7591) at https://zappyquote.com/oauth/register, which the authorization server metadata advertises, so most clients connect with no setup at all. You can also register an application yourself in Account → Developer → OAuth applications and tick Remote MCP. Either way a client gets a token only after a signed-in, email-verified person approves it on the consent screen, and a self-registered client is shown there as Not verified by Zappy.
Self-registration: redirect_uris must be https:// on a public hostname, or http:// on 127.0.0.1, [::1] or localhost for a native client (any port); no fragment, no user info. Public clients get no secret and use PKCE. Registration is rate limited per address and overall; a client that never connects anyone expires after 30 days. Registering grants nothing by itself.
| What | Where |
|---|---|
| MCP endpoint | https://zappyquote.com/mcp |
| Protected resource metadata (RFC 9728) | https://zappyquote.com/.well-known/oauth-protected-resource |
| Authorization server metadata (RFC 8414) | https://zappyquote.com/.well-known/oauth-authorization-server |
| Authorize / token / revoke | /oauth/authorize, /oauth/token, /oauth/revoke |
- PKCE with
S256is required.plainis refused, and it is not advertised. - Send
resource=https://zappyquote.com/mcp(RFC 8707) on the authorization request. The token is bound to the Remote MCP audience. The REST API also accepts it (that is how the endpoint runs every tool), under the same Remote MCP switches; a token issued for the REST API is refused at the MCP endpoint. - Redirect URIs are matched exactly. Owner-registered apps use
https://on a public hostname; self-registered native clients may also use a loopbackhttp://address. - Access tokens last 60 minutes.
offline_accessmay be requested; refresh tokens rotate on every use and can never add a permission. - Transport is Streamable HTTP over
POST, answeringapplication/json, one JSON-RPC message per request (no batching, body up to 256 KB). The endpoint is stateless: no session id is issued; each request is authorised by its token. - Limits: 120 requests a minute per connection and 240 per account; one creation at a time per connection.
Tools and permissions
| Tool | Permission (scope) | Kind |
|---|---|---|
zappy_list_capabilitiesList Zappy capabilities | billing:read | read |
zappy_get_balanceGet credit balance | billing:read | read |
zappy_quotePrice a capability | billing:read | read |
zappy_create_imageCreate an image (spends credits) | generation:write | write — spends credits |
zappy_edit_imageEdit an image (spends credits) | generation:write | write — spends credits |
zappy_get_jobRead a job | generation:read | read |
zappy_get_assetGet an asset URL | assets:read | read |
zappy_list_creationsList recent creations | generation:read | read |
Read and write are separate decisions. A connection approved without generation:write is read-only for its whole life: refreshing it can never add write, and getting write needs a new connection that the person approves on a consent screen that says, separately, that the app will be able to spend credits.
Read and creation tools are available through Remote MCP.
Billing
Creation spends the credits of the account that approved the connection, through the same quote, per-request ceiling, daily cap and idempotency rules as an API key. Each creation call requires max_credits and an idempotency_key; repeating a call with the same key replays it instead of charging again. Reading is free.
Example client configuration
Values below are placeholders, not working credentials.
{
"mcpServers": {
"zappy": {
"url": "https://zappyquote.com/mcp",
"oauth": {
"client_id": "zc_EXAMPLE_CLIENT_ID",
"redirect_uri": "https://your-app.example.com/oauth/callback",
"scope": "billing:read generation:read assets:read"
}
}
}
}Security and privacy
- Treat your key like a password. Keep it server-side. Never ship it in a web page, a mobile app, a public repository or a support ticket.
- Think it leaked? Revoke or rotate it yourself in Account → Developer, right now — do not wait for anyone. Revocation is immediate: the very next call with that key is refused. Never send a whole key to support, or to anyone else.
- Your ceilings limit the damage. Even a leaked key can never spend more than its daily cap, which is why the cap is worth setting as low as your integration can live with.
- Your creations stay yours. Jobs and assets are visible only to your account. Asset links are signed and expire.
- Use of the API is covered by the Zappy Terms and Privacy Policy.
Good to know
Is the Zappy API generally available?
Yes. Any Zappy account with a verified email address can create an API key in Account -> Developer. There is no waitlist and no approval step.
What does the API cost?
It spends the Zappy credits of the account your key belongs to, at the same prices as the Studio. Ask the API for exact prices with GET /capabilities or POST /quote. A key daily and per-request caps are spending ceilings, not an allowance of free credits. Failed or undeliverable jobs are restored automatically.
Which AI models does it use?
Zappy chooses the best engine for each job behind the scenes. You call a Zappy capability, and that capability is the contract.
Can I use it from Claude, Cursor, Codex, OpenClaw or n8n?
Yes. Zappy ships an MCP server those clients can run with Node from one downloadable file, and Zappy also hosts a Remote MCP endpoint for clients that connect over HTTPS instead. Any HTTP tool can call the REST API directly.
Keep exploring
Ready to build?
Create a key in Account -> Developer and make your first call in a couple of minutes.
Create an API keyFree to start · no credit card · the credit cost is shown before every render