Image playground

Generate or edit a single image from a prompt, existing images, or a saved identity

You will need an API token to send HTTP requests. See Authentication for instructions.

Image Playground is a single-image generate-and-edit endpoint. Send a text prompt to create an image from scratch, add image_ids to edit or compose from images you have uploaded, or pass identity_codes to place a saved On-Model identity in the shot. Each call returns one image, or up to four variations of that one image.

It is the general-purpose companion to the batch services. When you need consistent sets at scale (many PDPs, a full lookbook), use Model swap, Flat lay on model, Create packshot, or Garment recolor. Reach for Image Playground for one-off touch-ups and creative edits those fixed tools do not cover, including finishing another tool's output: upload a Flat-to-Model or Packshot result and refine it here.

Quick start

(Optional) Upload an image to edit or use as a reference, and collect its file_id. Skip this step for a from-scratch text-to-image prompt. See Uploading images.


Start a job with a prompt and any optional image_ids / identity_codes. The response returns a job_id. See Starting a job.


Track progress with the notifications stream (SSE) or a webhook, and stop when your image is ready. See Tracking progress.


Image Playground does not take a project_id and does not require an identity. A prompt on its own is a valid request.

Uploading images

Only needed when you want to edit, compose, or reference existing images. Every image is uploaded the same way: request a pre-signed URL, then PUT the bytes. The returned file_id is what you pass in image_ids.

import requests

api_url = "https://v2.api.piktid.com"
access_token = "your_access_token"


def upload(path, filename):
    r = requests.post(
        api_url + "/upload",
        headers={"Authorization": "Bearer " + access_token},
        json={"project_name": "image-playground", "filename": filename},
    ).json()
    with open(path, "rb") as f:
        requests.put(r["upload_url"], headers={"Content-Type": r["content_type"]}, data=f.read())
    return r["file_id"]


image_id = upload("product.jpg", "product.jpg")

The upload_url is only valid for a limited time. Upload the image immediately after receiving the response.

Starting a job

The endpoint always generates (there is no clarifying step). Provide at least one of prompt, image_ids, or identity_codes.

Generate from a prompt

import requests

api_url = "https://v2.api.piktid.com"
access_token = "your_access_token"

response = requests.post(
    api_url + "/edit-chat",
    headers={"Authorization": "Bearer " + access_token},
    json={
        "prompt": "A bottle of amber perfume half-submerged in a calm ocean at sunset, photorealistic.",
        "size": "2K",
        "num_variations": 1,
    },
).json()

job_id = response["job_id"]
print(f"Job started: {job_id}")
Response
{
  "job_id": "job_abc123...",          // JOB_ID
  "conversation_id": "job_abc123...", // same value as job_id
  "status": "processing"
}

Edit or compose existing images

Pass one or more uploaded image_ids. The first image is the one being edited; any others act as references to compose from.

response = requests.post(
    api_url + "/edit-chat",
    headers={"Authorization": "Bearer " + access_token},
    json={
        "prompt": "Place this sneaker on a wet city street at night with neon reflections.",
        "image_ids": [image_id],  
        "size": "2K",
    },
).json()

Use a saved identity

Pass identity_codes to bring a saved On-Model identity into the image as the model. Up to two identities per request. List your identities with GET /identity.

response = requests.post(
    api_url + "/edit-chat",
    headers={"Authorization": "Bearer " + access_token},
    json={
        "prompt": "Full-body editorial shot of the model in a beige trench coat, studio backdrop.",
        "identity_codes": ["default-pro-xxxxxxxx"],  
        "size": "2K",
    },
).json()

Request parameters

FieldTypeDefaultDescription
promptstringnullThe generation or edit instruction (or a from-scratch text-to-image prompt).
image_idsstring[]nullfile_ids of uploaded images to edit, compose, or reference. The first is the image being edited; the rest are references.
identity_codesstring[]nullCodes of saved identities to use as the model. Max 2.
model"auto" | "nano_banana_2" | "nano_banana_pro" | "gpt_image" | "seedream""auto"Generation engine. auto runs the default engine with a safety fallback; specifying an engine disables the fallback.
size"1K" | "2K" | "4K""2K"Quality / processing size. Drives resolution and the per-output price (see Credits). 2K and 4K require a paid plan.
num_variationsinteger1How many images to generate, 1–4. Each is billed separately.
output_mode"auto" | "match" | "ratio" | "custom""auto"Delivered dimensions. match keeps the input image's size; ratio uses aspect_ratio; custom uses exact width / height. On this endpoint auto behaves as match.
aspect_ratiostringnullFor output_mode: "ratio". One of 1:1, 3:2, 2:3, 3:4, 4:3, 4:5, 5:4, 9:16, 16:9, 21:9.
width, heightintegernullExact output size for output_mode: "custom" (256–7000, multiple of 8). Requires the custom-dimensions entitlement (Pro).
conversation_idstringnullContinue a prior generation instead of starting fresh (see Continuing a conversation). Omit to start a new one.

A request must include at least one of prompt, image_ids, or identity_codes. A prompt with no images generates from scratch; images or an identity with no prompt restyles the input.

Credits

Image Playground is billed per output image by size:

sizeCredits per image
1K3
2K5
4K10

Total cost is the per-image cost times num_variations, charged on completion. Failed variations are not charged. See Credits.

Continuing a conversation

Passing conversation_id continues from a previous generation: the last output becomes the working image for the next edit, and any new image_ids are added as references. Use the job_id (which equals conversation_id) returned by an earlier call.

# Refine the image produced by the previous call.
response = requests.post(
    api_url + "/edit-chat",
    headers={"Authorization": "Bearer " + access_token},
    json={
        "prompt": "Warm the lighting and add a soft rim light on the left.",
        "conversation_id": job_id,  # continue the same conversation
    },
).json()

Every call is a single generation. The same job_id accumulates each generation as a new result (keyed by image_index). The full multi-turn chat experience lives in the On-Model app; the API exposes the same underlying generation one call at a time.

Tracking progress

An Image Playground job is a long-lived conversation held at status: "completed", so GET /jobs/{job_id}/status always reports completed and is not a readiness signal. Track the per-image result instead: watch for the image_result notification below, or poll GET /jobs/{job_id}/results and check each result's own status.

Filter the notifications stream on your job_id and watch for an image_result event whose status is completed (or failed). The notification carries the output URL directly.

import json
import requests

api_url = "https://v2.api.piktid.com"
access_token = "your_access_token"
job_id = "job_abc123..."

headers = {"Authorization": "Bearer " + access_token}

with requests.get(
    api_url + "/notifications/events",
    headers=headers,
    stream=True,
    timeout=600,
) as response:
    response.raise_for_status()

    for raw_line in response.iter_lines(decode_unicode=True):
        if not raw_line or raw_line.startswith(":"):
            continue
        if not raw_line.startswith("data: "):
            continue

        notification = json.loads(raw_line[6:])
        if notification["name"] != "image_result":
            continue

        data = notification.get("data", {})
        if data.get("id_task") != job_id:
            continue

        status = data.get("status")
        if status == "completed":
            print(f"Image ready: {data['output_link']}")
            break
        if status == "failed":
            raise RuntimeError(data.get("error_message", "Generation failed"))
import hashlib
import hmac

import requests
from flask import Flask, abort, request

api_url = "https://v2.api.piktid.com"
access_token = "your_access_token"
job_id = "job_abc123..."
public_webhook_url = "https://example.com/webhooks/piktid"

setup = requests.put(
    api_url + "/webhooks",
    headers={"Authorization": "Bearer " + access_token},
    json={"url": public_webhook_url},
).json()
webhook_secret = setup["secret"]

app = Flask(__name__)


def verify_signature(secret: str, body: bytes, signature_header: str) -> bool:
    expected = "sha256=" + hmac.new(secret.encode("utf-8"), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature_header)


@app.post("/webhooks/piktid")
def handle_piktid_webhook():
    body = request.get_data()
    signature = request.headers.get("X-Webhook-Signature", "")
    if not verify_signature(webhook_secret, body, signature):
        abort(401)

    notification = request.get_json()
    if notification["name"] == "image_result":
        data = notification.get("data", {})
        if data.get("id_task") == job_id and data.get("status") == "completed":
            print(f"Image ready: {data['output_link']}")

    return "", 204


app.run(port=8000)
image_result notification
{
  "id": 12345,
  "name": "image_result",       // Per-image event
  "timestamp": 1702819200.0,
  "data": {
    "id_task": "job_abc123...",  // your job_id
    "image_index": 0,            // which generation (0 = first)
    "group_index": 0,            // which variation
    "status": "completed",
    "output_link": "https://...",           // full-size result URL
    "output_thumbnail_link": "https://..."
  }
}

Use DELETE /notifications/{id} after processing events so they are not replayed on reconnect.

Retrieving results

Fetch the generated images with GET /jobs/{job_id}/results. Each output is a standard image result; for Image Playground, image_index identifies the generation and group_index the variation.

import requests

api_url = "https://v2.api.piktid.com"
access_token = "your_access_token"
job_id = "job_abc123..."

response = requests.get(
    api_url + f"/jobs/{job_id}/results",
    headers={"Authorization": "Bearer " + access_token},
).json()

for result in response["results"]:
    if result["status"] == "completed":
        print(f"{result['image_index']}.{result['group_index']}: {result['output']['full_size']}")
Response
{
  "job_id": "job_abc123...",
  "job_type": "edit_chat",
  "status": "completed",
  "results": [
    {
      "image_index": 0,             // generation index
      "group_index": 0,             // variation index
      "version": 0,
      "output": {
        "full_size": "https://...", // result image URL
        "thumbnail": "https://..."
      },
      "model_used": "nano_banana_2",
      "status": "completed"
    }
  ],
  "summary": {
    // Job statistics
  }
}

GET /jobs/{job_id}/results returns the latest version of each generation, plus edit_chat_attachments (images you supplied) and edit_chat_identities (identities you tagged) for reference.

Error handling

POST /edit-chat returns the following errors before the job is queued:

HTTPMeaning
400No prompt, image_ids, or identity_codes supplied; invalid custom dimensions; or an unsupported model.
402Insufficient credits. Response body includes required_credits, in_progress_credits, user_credits.
403The requested size exceeds your plan, output_mode: "custom" without the custom-dimensions entitlement, or a subject blocked by the content filter.
404A referenced resource was not found.
429Rate limit reached (15 requests per minute per user).

See Errors for the full error model.

On this page