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.
Fetch the generated image. See Retrieving results.
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}"){
"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
| Field | Type | Default | Description |
|---|---|---|---|
prompt | string | null | The generation or edit instruction (or a from-scratch text-to-image prompt). |
image_ids | string[] | null | file_ids of uploaded images to edit, compose, or reference. The first is the image being edited; the rest are references. |
identity_codes | string[] | null | Codes 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_variations | integer | 1 | How 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_ratio | string | null | For 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, height | integer | null | Exact output size for output_mode: "custom" (256–7000, multiple of 8). Requires the custom-dimensions entitlement (Pro). |
conversation_id | string | null | Continue 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:
size | Credits per image |
|---|---|
1K | 3 |
2K | 5 |
4K | 10 |
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){
"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']}"){
"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:
| HTTP | Meaning |
|---|---|
400 | No prompt, image_ids, or identity_codes supplied; invalid custom dimensions; or an unsupported model. |
402 | Insufficient credits. Response body includes required_credits, in_progress_credits, user_credits. |
403 | The requested size exceeds your plan, output_mode: "custom" without the custom-dimensions entitlement, or a subject blocked by the content filter. |
404 | A referenced resource was not found. |
429 | Rate limit reached (15 requests per minute per user). |
See Errors for the full error model.