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.

Base URLhttps://zuki.pw/v1
MethodPOST with a JSON array of tasks
AuthenticationAuthorization: Bearer zk-…
OpenAPIhttps://zuki.pw/openapi.json

Authentication

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"])
200 response
{
  "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.

taskTypePurpose
imageInferenceGenerate or edit images.
getResponseStatus and results of a task by taskUUID.
imageUploadStore an input image and get an imageUUID for it (Uploading images).
accountManagementWith "operation": "getDetails": balance, limits and usage.
authenticationAlternative to the Authorization header.
pingHealth 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.

model stringrequired

Identifier of the model to use for generation.

one of zuki-image-1.0, zuki-image-1.0-max
positivePrompt stringrequired

Text prompt describing elements to include in the generated output.

min length 2max length 32000
width integer

Width 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.

min 16max 3840step 16
height integer

Height of the generated image in pixels. Must be used together with width. Multiple of 16.

min 16max 3840step 16

Inputs

Input images, nested inside the inputs object.

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.

min items 1max items 16

Image 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.

Image quality level. Higher quality takes longer and costs more.

one of auto, max, xhigh, high, medium, lowdefault auto

Background handling in generated images: auto, opaque (solid) or transparent. When transparent, outputFormat must be PNG or WEBP.

one of auto, opaque, transparentdefault auto

Content moderation level: auto (standard filtering) or low (less restrictive).

one of auto, lowdefault auto

Output

How generated images are encoded and returned.

Number of images to generate. Each result uses a different seed, producing variations.

default 1min 1max 20
outputType string

How the image is returned: URL, base64Data or dataURI.

one of URL, base64Data, dataURIdefault URL

File format of the generated image: JPG, PNG (lossless, alpha) or WEBP (compact, alpha).

one of JPG, PNG, WEBPdefault JPG

Compression quality of the output file. Higher values preserve quality but increase file size.

default 95min 20max 99
ttl integer

Time-to-live in seconds for the generated image URL. Only applies when outputType is URL.

min 60

Delivery and tracking

Task identity, delivery method and billing options.

taskType stringrequired

Identifier for the type of task being performed.

value imageInference
taskUUID string

UUID 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.

format uuid
includeCost boolean

Include the amount deducted from your balance in the response (cost field, per image).

default false

sync returns complete results in the response. async returns an immediate acknowledgment; fetch results with a getResponse task or receive them on webhookURL.

one of sync, asyncdefault sync
webhookURL string

URL that receives each result as JSON via HTTP POST as soon as it is ready. For numberResults > 1 every image triggers a separate call.

max length 2048format url

URL where every generated image is uploaded with HTTP PUT (raw binary body). Use presigned URLs for S3 / GCS / Azure / any storage.

max length 4096format url

Advanced

Safety checks and watermarking.

Enable additional content safety checking (NSFWContent flag in the response). Increases generation time.

Adds a text or image watermark to the generated image. Provide either text or image, not both.

Watermark text.

min length 2max length 32

Watermark image (image UUID, URL, Data URI, or Base64).

Watermark position.

one of top-left, top-center, top-right, center-left, center-center, center-right, bottom-left, bottom-center, bottom-right, tiled

Watermark opacity from 0.1 to 1.

min 0.1max 1

Text color in hex format.

Background color in hex format.

Compatibility rules

  • width and height go 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" requires outputFormat set to PNG or WEBP.
  • inputs.maskImage requires inputs.referenceImages.
  • Image inputs accept an imageUUID of 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.

FieldTypeDescription
taskTypestringimageInference
taskUUIDstringEchoed from the request.
statusstringprocessing or success, in async responses, polling and webhooks.
imageUUIDstringImage id. Reusable as an input image in later tasks.
imageURLstringImage URL, when outputType is URL.
imageBase64DatastringBase64 image, when outputType is base64Data.
imageDataURIstringData URI, when outputType is dataURI.
seedintegerSeed used for the image, when available.
NSFWContentbooleanPresent when safety.checkContent is enabled.
costnumberUSD 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"
    }
  ]
}
HTTPcodeMeaning
400invalidRequestBodyBody is not a JSON array of task objects.
400missingParameterA required parameter is missing (see parameter).
400invalidParameterA parameter has an invalid value (see parameter).
400unsupportedParameterThe task contains a parameter that is not supported.
400invalidModelUnknown model identifier.
400unsupportedTaskTypeThe taskType is not supported.
400invalidImageUUIDA referenced image UUID does not exist for your key, or the upload has expired.
400invalidImageThe uploaded file is empty, corrupted or larger than 8192×8192 / 40 megapixels.
413imageTooLargeThe uploaded image is larger than 20 MB.
413uploadQuotaExceededYour uploads take more than 500 MB. Older uploads expire automatically.
415unsupportedImageFormatOnly JPEG, PNG and WEBP images can be uploaded.
415unsupportedMediaTypeSend the file as multipart/form-data or as a raw image body.
400invalidURLwebhookURL or uploadEndpoint is not a public http(s) URL.
400contentModeratedThe prompt or input images were rejected by moderation. Not charged.
401invalidApiKeyMissing or invalid API key.
402insufficientCreditsYour balance is too low for this task.
403apiKeyDisabledThe key has been disabled.
403modelNotAllowedThe key is not allowed to use this model.
404taskNotFoundNo task with this taskUUID for your key.
409duplicateTaskUUIDtaskUUID was already used by your key.
413payloadTooLargeRequest body is too large (inline images).
429rateLimitExceededToo many tasks per minute or too many running at once. Retry with backoff.
500internalErrorUnexpected server error. Retry with backoff.
503serviceUnavailableTemporarily at capacity. Retry with backoff.
503modelUnavailableThe model is temporarily disabled.
503interruptedThe task was interrupted by a restart. Not charged, resubmit it.
504timeoutGeneration 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.

acknowledgment
{ "data": [{ "taskType": "imageInference", "taskUUID": "…", "status": "processing" }] }
poll
[{ "taskType": "getResponse", "taskUUID": "a770f077-f413-47de-9dac-be0b26a35da6" }]
finished
{
  "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.

success body
{
  "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"
}
failure body
{ "errors": [{ "code": "contentModerated", "message": "…", "taskType": "imageInference", "taskUUID": "…", "status": "error" }] }
  • Answer with any 2xx within 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.jpg
import { 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..." }]
200 response
{
  "data": [
    {
      "taskType": "imageUpload",
      "imageUUID": "6f1c2a9e-4b7d-4e8a-9c21-3d5f0b8e7a14",
      "format": "JPG",
      "width": 1280,
      "height": 960,
      "size": 348211,
      "expiresAt": "2026-09-30T18:04:11.000Z"
    }
  ]
}
use it
[{
  "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 imageUUID and 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"] }
  }
]