API reference
Zuki Image API
Generate and edit images with zuki-image-1.0 and zuki-image-1.0-max over HTTPS.
Text-to-image, up to 16 reference images, masked editing, transparent output and batches, delivered synchronously, by polling or by webhook.
https://zuki.pw/v1POST with a JSON array of tasksAuthorization: Bearer zk-…https://zuki.pw/openapi.jsonAuthentication
Every request needs your API key. Keys start with zk- and are tied to a prepaid balance.
Send it in the Authorization header, or as the first task of the request array.
Authorization: Bearer zk-your-api-key[
{ "taskType": "authentication", "apiKey": "zk-your-api-key" },
{ "taskType": "imageInference", "model": "zuki-image-1.0", "positivePrompt": "…" }
]Keep the key on your server. Never ship it inside browser or mobile apps.
You can optionally pass X-End-User: <id> with an identifier of your own end user; it is stored with the task for usage breakdowns and abuse investigations.
Models
zuki-image-1.0
Zuki Image 1.0
Fast, high-quality image generation and editing. Typical time per image: ~20 s.
zuki-image-1.0-max
Zuki Image 1.0 Max
Premium generation and editing with precise control. Typical time per image: 30-70 s.
Both models take exactly the same parameters, so switching is a one-word change. List available models with GET https://zuki.pw/v1/models.
Quickstart
Send one task and get an image URL back.
curl https://zuki.pw/v1 \
-H "Authorization: Bearer $ZUKI_API_KEY" \
-H "Content-Type: application/json" \
-d '[
{
"taskType": "imageInference",
"taskUUID": "39d7207a-343e-4198-9c40-ef6d302cadb3",
"model": "zuki-image-1.0",
"positivePrompt": "A reading nook by a rainy window, warm lamp light, film photo",
"width": 1024,
"height": 1024,
"includeCost": true
}
]'// Node.js 18+ (built-in fetch)
const res = await fetch('https://zuki.pw/v1', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.ZUKI_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify([
{
taskType: 'imageInference',
taskUUID: crypto.randomUUID(),
model: 'zuki-image-1.0',
positivePrompt: 'A reading nook by a rainy window, warm lamp light, film photo',
width: 1024,
height: 1024,
includeCost: true,
},
]),
});
const body = await res.json();
if (body.errors) throw new Error(body.errors[0].message);
console.log(body.data[0].imageURL, body.data[0].cost);import os, uuid, requests
res = requests.post(
"https://zuki.pw/v1",
headers={"Authorization": f"Bearer {os.environ['ZUKI_API_KEY']}"},
json=[{
"taskType": "imageInference",
"taskUUID": str(uuid.uuid4()),
"model": "zuki-image-1.0",
"positivePrompt": "A reading nook by a rainy window, warm lamp light, film photo",
"width": 1024,
"height": 1024,
"includeCost": True,
}],
timeout=300,
)
body = res.json()
if "errors" in body:
raise RuntimeError(body["errors"][0]["message"])
print(body["data"][0]["imageURL"]){
"data": [
{
"taskType": "imageInference",
"taskUUID": "39d7207a-343e-4198-9c40-ef6d302cadb3",
"imageUUID": "84a325ac-3fab-42ac-a303-93b50a6de979",
"imageURL": "https://zuki.pw/v1/images/84a325ac-3fab-42ac-a303-93b50a6de979.jpg",
"cost": 0.0431
}
]
}Request format
Always POST a JSON array of task objects to https://zuki.pw/v1; a single object is accepted too.
Up to 20 tasks per request run in parallel, and results are matched by taskUUID.
| taskType | Purpose |
|---|---|
imageInference | Generate or edit images. |
getResponse | Status and results of a task by taskUUID. |
imageUpload | Store an input image and get an imageUUID for it (Uploading images). |
accountManagement | With "operation": "getDetails": balance, limits and usage. |
authentication | Alternative to the Authorization header. |
ping | Health check, returns {"pong": true}. |
imageInference parameters
Only taskType, model and positivePrompt are required. Unknown parameters are rejected.
Core parameters
What to generate and at what size.
Identifier of the model to use for generation.
Text prompt describing elements to include in the generated output.
width
integerWidth of the generated image in pixels. Must be used together with height. Multiple of 16. Total area (width × height) must be between 655,360 and 8,294,400 pixels, aspect ratio between 1:3 and 3:1.
height
integerHeight of the generated image in pixels. Must be used together with width. Multiple of 16.
Inputs
Input images, nested inside the inputs object.
inputs.referenceImages
array<string>List of input images: a public URL, a Data URI, raw Base64, the imageUUID of one of your uploads (see Uploading images) or of a previous generation. Used for image-to-image, editing and style/subject reference.
inputs.maskImage
stringImage that marks which areas of the input image should be edited (URL, Data URI, Base64 or image UUID). White marks the area to edit, black is preserved. The mask is treated as guidance rather than a hard boundary. With several reference images the mask applies to the first one. Requires inputs.referenceImages.
Settings
Model settings, nested inside the settings object.
settings.quality
stringImage quality level. Higher quality takes longer and costs more.
settings.background
stringBackground handling in generated images: auto, opaque (solid) or transparent. When transparent, outputFormat must be PNG or WEBP.
settings.moderation
stringContent moderation level: auto (standard filtering) or low (less restrictive).
Output
How generated images are encoded and returned.
numberResults
integerNumber of images to generate. Each result uses a different seed, producing variations.
outputType
stringHow the image is returned: URL, base64Data or dataURI.
outputFormat
stringFile format of the generated image: JPG, PNG (lossless, alpha) or WEBP (compact, alpha).
outputQuality
integerCompression quality of the output file. Higher values preserve quality but increase file size.
ttl
integerTime-to-live in seconds for the generated image URL. Only applies when outputType is URL.
Delivery and tracking
Task identity, delivery method and billing options.
Identifier for the type of task being performed.
taskUUID
stringUUID v4 identifier for tracking tasks and matching async responses. Must be unique per task for your API key. If omitted, the server generates one and returns it in the response.
includeCost
booleanInclude the amount deducted from your balance in the response (cost field, per image).
deliveryMethod
stringsync returns complete results in the response. async returns an immediate acknowledgment; fetch results with a getResponse task or receive them on webhookURL.
webhookURL
stringURL that receives each result as JSON via HTTP POST as soon as it is ready. For numberResults > 1 every image triggers a separate call.
uploadEndpoint
stringURL where every generated image is uploaded with HTTP PUT (raw binary body). Use presigned URLs for S3 / GCS / Azure / any storage.
Advanced
Safety checks and watermarking.
safety.checkContent
booleanEnable additional content safety checking (NSFWContent flag in the response). Increases generation time.
advancedFeatures.watermark
objectAdds a text or image watermark to the generated image. Provide either text or image, not both.
Watermark text.
Watermark image (image UUID, URL, Data URI, or Base64).
Watermark position.
Watermark opacity from 0.1 to 1.
Text color in hex format.
Background color in hex format.
Compatibility rules
widthandheightgo together. Both are multiples of 16, from 16 to 3840.- Total pixels between 655,360 and 8,294,400 (for example 1024×640 up to 3840×2160). Aspect ratio between 1:3 and 3:1. Omit both to let the model pick.
settings.background: "transparent"requiresoutputFormatset toPNGorWEBP.inputs.maskImagerequiresinputs.referenceImages.- Image inputs accept an
imageUUIDof one of your previous generations, a public URL, a Data URI or raw Base64.
Common sizes: 1024×1024, 1536×1024, 1024×1536, 2048×2048, 1920×1088, 3840×2160.
Response
Results come back in the data array, one object per image. Failures come back in the errors array.
A request with several tasks can contain both.
Image URLs are temporary. Download and store the images you want to keep; expired URLs return 410.
Embedded metadata (EXIF, XMP, comments, provenance manifests) is removed from every image.
| Field | Type | Description |
|---|---|---|
taskType | string | imageInference |
taskUUID | string | Echoed from the request. |
status | string | processing or success, in async responses, polling and webhooks. |
imageUUID | string | Image id. Reusable as an input image in later tasks. |
imageURL | string | Image URL, when outputType is URL. |
imageBase64Data | string | Base64 image, when outputType is base64Data. |
imageDataURI | string | Data URI, when outputType is dataURI. |
seed | integer | Seed used for the image, when available. |
NSFWContent | boolean | Present when safety.checkContent is enabled. |
cost | number | USD deducted from your balance for this image, when includeCost is true. |
Errors
Each error is scoped to the task that failed. Other tasks in the same request may still succeed.
{
"errors": [
{
"code": "insufficientCredits",
"message": "Insufficient balance to run this task. Top up your balance and try again.",
"taskType": "imageInference",
"taskUUID": "39d7207a-343e-4198-9c40-ef6d302cadb3",
"documentation": "https://zuki.pw/docs#errors"
}
]
}| HTTP | code | Meaning |
|---|---|---|
| 400 | invalidRequestBody | Body is not a JSON array of task objects. |
| 400 | missingParameter | A required parameter is missing (see parameter). |
| 400 | invalidParameter | A parameter has an invalid value (see parameter). |
| 400 | unsupportedParameter | The task contains a parameter that is not supported. |
| 400 | invalidModel | Unknown model identifier. |
| 400 | unsupportedTaskType | The taskType is not supported. |
| 400 | invalidImageUUID | A referenced image UUID does not exist for your key, or the upload has expired. |
| 400 | invalidImage | The uploaded file is empty, corrupted or larger than 8192×8192 / 40 megapixels. |
| 413 | imageTooLarge | The uploaded image is larger than 20 MB. |
| 413 | uploadQuotaExceeded | Your uploads take more than 500 MB. Older uploads expire automatically. |
| 415 | unsupportedImageFormat | Only JPEG, PNG and WEBP images can be uploaded. |
| 415 | unsupportedMediaType | Send the file as multipart/form-data or as a raw image body. |
| 400 | invalidURL | webhookURL or uploadEndpoint is not a public http(s) URL. |
| 400 | contentModerated | The prompt or input images were rejected by moderation. Not charged. |
| 401 | invalidApiKey | Missing or invalid API key. |
| 402 | insufficientCredits | Your balance is too low for this task. |
| 403 | apiKeyDisabled | The key has been disabled. |
| 403 | modelNotAllowed | The key is not allowed to use this model. |
| 404 | taskNotFound | No task with this taskUUID for your key. |
| 409 | duplicateTaskUUID | taskUUID was already used by your key. |
| 413 | payloadTooLarge | Request body is too large (inline images). |
| 429 | rateLimitExceeded | Too many tasks per minute or too many running at once. Retry with backoff. |
| 500 | internalError | Unexpected server error. Retry with backoff. |
| 503 | serviceUnavailable | Temporarily at capacity. Retry with backoff. |
| 503 | modelUnavailable | The model is temporarily disabled. |
| 503 | interrupted | The task was interrupted by a restart. Not charged, resubmit it. |
| 504 | timeout | Generation took too long and was stopped. Not charged. |
Log the taskUUID and message of every error. Support needs them to trace a task.
Async and polling
A generation takes 20 to 70 seconds. To avoid holding the connection, set "deliveryMethod": "async".
The API acknowledges the task at once and you fetch the result with getResponse.
{ "data": [{ "taskType": "imageInference", "taskUUID": "…", "status": "processing" }] }[{ "taskType": "getResponse", "taskUUID": "a770f077-f413-47de-9dac-be0b26a35da6" }]{
"data": [
{
"taskType": "imageInference",
"taskUUID": "a770f077-f413-47de-9dac-be0b26a35da6",
"status": "success",
"imageUUID": "b7db282d-2943-4f12-992f-77df3ad3ec71",
"imageURL": "https://zuki.pw/v1/images/b7db282d-2943-4f12-992f-77df3ad3ec71.jpg"
}
]
}Poll every 2 to 5 seconds with backoff. Finished images appear in data with status: "success"; failures appear in errors with status: "error".
getResponse works for sync tasks too, so a client that lost its connection can still fetch the result.
const API = 'https://zuki.pw/v1';
const headers = { Authorization: `Bearer ${process.env.ZUKI_API_KEY}`, 'Content-Type': 'application/json' };
const call = async (tasks) => (await fetch(API, { method: 'POST', headers, body: JSON.stringify(tasks) })).json();
// 1. Submit
const taskUUID = crypto.randomUUID();
await call([{
taskType: 'imageInference',
taskUUID,
model: 'zuki-image-1.0-max',
positivePrompt: 'Isometric cutaway of a small underground bakery, highly detailed',
deliveryMethod: 'async',
}]);
// 2. Poll with backoff
for (let delay = 2000; ; delay = Math.min(delay * 1.5, 10000)) {
await new Promise((r) => setTimeout(r, delay));
const res = await call([{ taskType: 'getResponse', taskUUID }]);
if (res.errors?.length) throw new Error(res.errors[0].message);
const done = res.data.filter((d) => d.status === 'success');
if (done.length) { console.log(done.map((d) => d.imageURL)); break; }
}Webhooks
Add webhookURL to a task and every image is sent to that URL as a JSON POST as soon as it is ready, one call per image.
Combine it with deliveryMethod: "async" so you never wait on the request.
{
"taskType": "imageInference",
"taskUUID": "a770f077-f413-47de-9dac-be0b26a35da6",
"status": "success",
"imageUUID": "550e8400-e29b-41d4-a716-446655440000",
"imageURL": "https://zuki.pw/v1/images/550e8400-e29b-41d4-a716-446655440000.jpg"
}{ "errors": [{ "code": "contentModerated", "message": "…", "taskType": "imageInference", "taskUUID": "…", "status": "error" }] }- Answer with any
2xxwithin 15 seconds and do heavy work afterwards. - Failed deliveries are retried after 250 ms, 500 ms, 1 s, 2 s, 4 s and then every 8 s, with 20% jitter.
- A delivery can arrive twice. De-duplicate by
imageUUID. - Put a secret in the URL query (for example
?token=…) and check it. Use HTTPS.
import express from 'express';
const app = express();
app.use(express.json({ limit: '50mb' }));
const processed = new Set();
app.post('/hooks/zuki', (req, res) => {
if (req.query.token !== process.env.WEBHOOK_TOKEN) return res.sendStatus(401);
res.sendStatus(200); // respond fast, process asynchronously
const body = req.body;
if (body.errors) return console.error('Generation failed', body.errors[0]);
if (processed.has(body.imageUUID)) return; // deliveries can repeat
processed.add(body.imageUUID);
console.log('Image ready', body.taskUUID, body.imageURL);
});
app.listen(3000);Uploading images
To edit a user's own photo, send it with the task. There are two ways.
Inline. Put the file into inputs.referenceImages as a Data URI or raw Base64. One request, nothing to store.
[{
"taskType": "imageInference",
"model": "zuki-image-1.0",
"positivePrompt": "Turn this photo into a watercolor painting",
"inputs": { "referenceImages": ["data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ..."] }
}]Upload first. Send the file to POST https://zuki.pw/v1/uploads and use the returned imageUUID in any number of tasks.
Best for large files, repeated edits of the same photo and clients that can't build Base64 easily.
curl https://zuki.pw/v1/uploads \
-H "Authorization: Bearer $ZUKI_API_KEY" \
-F "file=@photo.jpg"curl https://zuki.pw/v1/uploads \
-H "Authorization: Bearer $ZUKI_API_KEY" \
-H "Content-Type: image/jpeg" \
--data-binary @photo.jpgimport { readFile } from 'node:fs/promises';
const form = new FormData();
form.append('file', new Blob([await readFile('photo.jpg')]), 'photo.jpg');
const res = await fetch('https://zuki.pw/v1/uploads', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.ZUKI_API_KEY}` },
body: form,
});
const { data } = await res.json();
const imageUUID = data[0].imageUUID;import os, requests
res = requests.post(
"https://zuki.pw/v1/uploads",
headers={"Authorization": f"Bearer {os.environ['ZUKI_API_KEY']}"},
files={"file": open("photo.jpg", "rb")},
)
image_uuid = res.json()["data"][0]["imageUUID"][{ "taskType": "imageUpload", "image": "data:image/png;base64,iVBORw0KGgo..." }]{
"data": [
{
"taskType": "imageUpload",
"imageUUID": "6f1c2a9e-4b7d-4e8a-9c21-3d5f0b8e7a14",
"format": "JPG",
"width": 1280,
"height": 960,
"size": 348211,
"expiresAt": "2026-09-30T18:04:11.000Z"
}
]
}[{
"taskType": "imageInference",
"model": "zuki-image-1.0",
"positivePrompt": "Same person in a spacesuit on the Moon",
"inputs": { "referenceImages": ["6f1c2a9e-4b7d-4e8a-9c21-3d5f0b8e7a14"] }
}]- Accepted formats: JPEG, PNG and WEBP, detected from the file content. Up to 20 MB and 8192 px per side.
- Uploads are private to your API key and have no public URL. They are kept for 7 days; uploading the same file again returns the same
imageUUIDand extends it. - EXIF (including GPS location), XMP and other embedded metadata are removed on upload.
- Uploads are free. Up to 60 uploads per minute and 500 MB of stored uploads per key.
Upload endpoint
Set uploadEndpoint to a presigned URL and each image is uploaded there with HTTP PUT,
a raw binary body and the matching Content-Type. Works with S3, Google Cloud Storage, Azure Blob and any storage that accepts PUT.
{ "uploadEndpoint": "https://your-bucket.s3.amazonaws.com/out/teapot.jpg?X-Amz-Signature=…" }Balance and usage
curl https://zuki.pw/v1 \
-H "Authorization: Bearer $ZUKI_API_KEY" \
-d '[{ "taskType": "accountManagement", "operation": "getDetails" }]'curl https://zuki.pw/v1/account -H "Authorization: Bearer $ZUKI_API_KEY"{
"data": [
{
"taskType": "accountManagement",
"operation": "getDetails",
"name": "production",
"balance": { "amount": 41.27, "currency": "USD" },
"models": ["zuki-image-1.0", "zuki-image-1.0-max"],
"rateLimits": { "tasksPerMinute": 60, "concurrentTasks": 4 },
"usage": {
"today": { "credits": 1.84, "requests": 42 },
"last7Days": { "credits": 12.3, "requests": 301 },
"last30Days": { "credits": 58.73, "requests": 1420 },
"total": { "credits": 58.73, "requests": 1420 }
}
}
]
}Pricing
You pay for what each generation actually uses. The price depends on the model, resolution, settings.quality,
the number and size of input images, and numberResults. Set includeCost: true to get the exact amount in cost.
- When a task starts, an estimate is reserved on your balance. When it finishes, the reserve is released and the real cost is deducted.
- If the balance can't cover the reserve, the task is rejected with
402 insufficientCredits. - Failed generations are not charged.
Rate limits
Each key has a limit on tasks per minute and on tasks running at the same time; see rateLimits in account details.
Going over returns 429 rateLimitExceeded. Under heavy load you may also get 503 or slower responses.
Retry 429 and 5xx with exponential backoff. Don't retry other 4xx errors without changing the request.
async function callWithRetry(tasks, maxRetries = 4) {
for (let i = 0; ; i++) {
const res = await fetch('https://zuki.pw/v1', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.ZUKI_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify(tasks),
});
const retryable = res.status === 429 || res.status >= 500;
if (!retryable || i >= maxRetries) return res.json();
await new Promise((r) => setTimeout(r, 1000 * 2 ** i + Math.random() * 250)); // 1s, 2s, 4s, 8s
}
}Examples
Generate and save a file
import { writeFile } from 'node:fs/promises';
const res = await fetch('https://zuki.pw/v1', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.ZUKI_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify([{
taskType: 'imageInference',
model: 'zuki-image-1.0',
positivePrompt: 'Studio photo of a ceramic teapot on linen, soft window light',
outputType: 'base64Data',
outputFormat: 'PNG',
}]),
signal: AbortSignal.timeout(300_000),
});
const { data, errors } = await res.json();
if (errors) throw new Error(errors[0].message);
await writeFile('teapot.png', Buffer.from(data[0].imageBase64Data, 'base64'));import base64, os, requests
res = requests.post(
"https://zuki.pw/v1",
headers={"Authorization": f"Bearer {os.environ['ZUKI_API_KEY']}"},
json=[{
"taskType": "imageInference",
"model": "zuki-image-1.0-max",
"positivePrompt": "Studio photo of a ceramic teapot on linen, soft window light",
"outputType": "base64Data",
"outputFormat": "PNG",
}],
timeout=300,
)
body = res.json()
if "errors" in body:
raise RuntimeError(body["errors"][0]["message"])
with open("teapot.png", "wb") as f:
f.write(base64.b64decode(body["data"][0]["imageBase64Data"]))Image-to-image with references
[
{
"taskType": "imageInference",
"model": "zuki-image-1.0",
"positivePrompt": "Put the jacket from image 1 on a model walking down the street from image 2, editorial photo",
"width": 1024,
"height": 1536,
"inputs": {
"referenceImages": [
"https://example.com/jacket.jpg",
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
]
}
}
]Masked editing
White areas of the mask are edited. Black areas stay as they are.
[
{
"taskType": "imageInference",
"model": "zuki-image-1.0-max",
"positivePrompt": "Replace the sky with a dramatic sunset, keep the building untouched",
"inputs": {
"referenceImages": ["https://example.com/photo.jpg"],
"maskImage": "https://example.com/sky-mask.png"
},
"settings": { "quality": "high" }
}
]Transparent background
[
{
"taskType": "imageInference",
"model": "zuki-image-1.0",
"positivePrompt": "A cartoon fox sticker with a thick white outline",
"width": 1024,
"height": 1024,
"settings": { "background": "transparent" },
"outputFormat": "PNG"
}
]Several variations
[
{
"taskType": "imageInference",
"model": "zuki-image-1.0",
"positivePrompt": "Flat vector logo of a paper crane",
"numberResults": 4,
"outputFormat": "WEBP",
"outputQuality": 90
}
]Iterating on a previous result
Pass the imageUUID of an earlier generation as a reference instead of uploading it again.
[
{
"taskType": "imageInference",
"model": "zuki-image-1.0",
"positivePrompt": "Same scene at night with neon signs",
"inputs": { "referenceImages": ["84a325ac-3fab-42ac-a303-93b50a6de979"] }
}
]