Flat lay on model

Virtual try-on from flat lay images with a custom model

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

Quick start

Create a project to organize your images. The project_id will be used in subsequent requests. See Creating a project for details.


Upload one or more flat lay / SKU images to the project. This is a two-step process:

  1. Request a pre-signed upload URL
  2. PUT the image binary to that URL

Collect all file_id values for the next step. See Uploading images for details.

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


Start a flat-to-model job by providing the identity (the model), the list of uploaded file IDs (all combined as inputs), and one or more instructions. Each instruction produces one output image. See Starting a job for details.


Track job progress with SSE or webhooks. Filter events with your job ID and stop when you receive a terminal status. See Tracking progress for details.

Uploading identities

Before starting a flat-to-model job, you need an identity_code. You can either use an existing identity from your gallery or upload a new one.

import requests

api_url = "https://v2.api.piktid.com"
access_token = "your_access_token"
identity_image_path = "path/to/identity.jpg"

with open(identity_image_path, "rb") as f:
    response = requests.post(
        api_url + "/identity/upload",
        headers={"Authorization": "Bearer " + access_token},
        files={"image": f},
        data={"name": "Model A"},  # Optional custom name
    ).json()

identity_code = response["identity_code"]
print(f"Identity uploaded: {identity_code}")
Response
{
  "identity_code": "id_xyz...",  // IDENTITY_CODE
  "name": "Model A",
  "face_detected": true,
  "success": true,
  // ...
}

You can list existing identities using:

Creating a project

import requests

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

response = requests.post(
    api_url + "/project",
    headers={"Authorization": "Bearer " + access_token},
    json={"project_name": "my-flat-lay-project"},
).json()
project_id = response["project_id"]
project_name = response["project_name"]
Response
{
  "project_id": "abc123...",   // PROJECT_ID
  "project_name": "my-flat-lay-project"
}

Uploading images

Upload all flat lay / SKU images (e.g., shirt, pants, shoes) that will be combined for each output. You can repeat this process for each image.

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

import requests

api_url = "https://v2.api.piktid.com"
access_token = "your_access_token"
project_name = "my-flat-lay-project"
image_path = "path/to/sku-image.jpg"

# Step 1: Get pre-signed upload URL
response = requests.post(
    api_url + "/upload",
    headers={"Authorization": "Bearer " + access_token},
    json={
        "project_name": project_name,
        "filename": "sku-shirt.jpg",
    },
).json()

upload_url = response["upload_url"]
content_type = response["content_type"]
file_id = response["file_id"]

# Step 2: Upload the image binary
with open(image_path, "rb") as f:
    requests.put(
        upload_url,
        headers={"Content-Type": content_type},
        data=f.read(),
    )

print(f"Uploaded file ID: {file_id}")
Response
{
  "upload_url": "https://s3...",     // Pre-signed PUT URL
  "download_url": "https://...",
  "project_id": "abc123...",
  "project_name": "my-flat-lay-project",
  "file_id": "sku_001...",           // FILE_ID
  "filename": "sku-shirt.jpg",
  "content_type": "image/jpeg"
}

Starting a job

Unlike model swap (1 input = 1 output), flat-to-model combines all input images and generates outputs based on the instructions list:

  • N input SKU images + M instructions = M output images
  • Each instruction produces one output image combining all inputs
  • Instructions run in parallel
import requests

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

project_id = "abc123..."
identity_code = "id_xyz..."  # From identity upload or gallery
file_ids = ["sku_001...", "sku_002..."]  # All SKU images to combine (simple format)

# Define instructions - each instruction produces one output (or num_variations outputs)
instructions = [
    {
        "pose": "standing front-facing",
        "expression": "neutral",
        "background": "white studio",
        "lighting": "soft",
        "camera": {"framing": "full body", "angle": "eye level"},
        "options": {
            "size": "2K",
            "ar": "9:16",
            "format": "jpg",
        },
    },
    {
        "pose": "walking, mid-stride",
        "expression": "confident",
        "mood": "editorial",
        "background": "gym rooftop at golden hour",
        "lighting": "dramatic",
        "camera": {"framing": "three-quarter", "angle": "low angle"},
        "options": {
            "size": "2K",
            "ar": "3:4",
            "format": "png",
        },
    },
]

response = requests.post(
    api_url + "/flat-2-model",
    headers={"Authorization": "Bearer " + access_token},
    json={
        "identity_code": identity_code,
        "project_id": project_id,
        "images": file_ids,
        "instructions": instructions,
        "post_process": False,  # Optional: enable post-processing
        "options": {
            "model": "auto",  # Optional: see "Generation options" below
        },
    },
).json()

job_id = response["job_id"]
total_outputs = response["total_outputs"]
print(f"Job started: {job_id} ({total_outputs} outputs)")
Response
{
  "job_id": "job_abc123...",  // JOB_ID
  "status": "pending",
  "message": "Job created successfully",
  "total_outputs": 2          // Number of output images
}

Instruction parameters

Each instruction can contain the following parameters. All fields are optional: an empty instruction produces a default on-model output. Stylistic fields accept three shapes ("Design Value" format):

  • A plain string (e.g. "standing front-facing").
  • An integer index referencing a saved Design Value in your account.
  • An object { "text": "...", "image": "uuid" } pairing a text descriptor with a reference image UUID.
ParameterTypeDescription
posestring | int | objectPose descriptor (e.g. "standing front-facing", "walking, mid-stride").
expressionstring | int | objectFacial expression descriptor (e.g. "neutral", "smiling", "confident").
moodstring | int | objectOverall mood / atmosphere descriptor (e.g. "editorial", "casual").
camerastring | int | objectCamera descriptor. Plain text or a structured object with optional framing, angle, lens, aperture sub-fields.
lightingstring | int | objectLighting descriptor. Plain text or a structured object with optional direction, quality, complexity sub-fields.
backgroundstring | int | objectBackground descriptor (e.g. "white studio", "gym rooftop at golden hour").
stylestring | int | objectVisual style descriptor.
color_palettestring | int | objectColor scheme descriptor.
promptstringFree-form prompt overlay. When provided, the engine combines it with the structured fields above.
seedintegerReproducibility seed for this instruction. Top-level seed is preferred over options.seed.
num_variationsinteger (1-8)Number of output variations to generate from this instruction. Defaults to 1.
anglestring | int | objectDeprecated. Use camera.angle instead. Kept for backward compatibility.
preset_namestringMetadata only. Name of the preset this instruction came from, surfaced in the UI.
category_namesstring[]Metadata only. Categories the preset belongs to.
optionsobjectPer-instruction output options. See Output options.

Output options

The options object within each instruction can contain:

ParameterValuesDescription
size"1K", "2K", "4K"Output image resolution.
ar"1:1", "3:4", "4:3", "9:16", "16:9"Aspect ratio.
format"jpg", "png"Output file format.
widthinteger (256-7000, multiple of 8)Custom output width. Must be set together with height. Aspect ratio (width / height) must be between 1:4 and 4:1. Requires the OUTPUT_CUSTOM_DIMENSIONS policy on your account. When provided, overrides size and ar.
heightinteger (256-7000, multiple of 8)Custom output height. Must be set together with width.
seedintegerRandom seed for this output. Prefer the top-level seed field on the instruction.

Generation options

Top-level fields inside the request's options object that control how the batch is generated (as opposed to per-instruction styling).

ParameterTypeDefaultDescription
model"auto" | "nano_banana_pro" | "seedream""auto"Which engine generates outputs. auto runs the default engine with a safety fallback if content is refused. Specifying an engine disables the fallback.
use_anchorbooleantrueKeeps multi-output generations visually aligned so the set feels cohesive, with steadier styling details and a more unified overall look. Set to false to generate each output independently.
anchor_indexinteger0Which instruction (zero-indexed into instructions) is used as the anchor reference when use_anchor is true. Must satisfy 0 <= anchor_index < len(instructions).
Request with generation options
{
  "identity_code": "id_xyz...",
  "project_id": "abc123...",
  "images": ["sku_001...", "sku_002..."],
  "instructions": [/* ... */],
  "options": {
    "model": "nano_banana_pro",  
    "use_anchor": false
  }
}

The legacy use_alternative_method flag is still accepted for backward compatibility and is mapped internally to model: "seedream". New integrations should use model directly.

AI-generated disclosure watermark

If your jurisdiction or distribution platform requires AI-generated images to be visibly marked as such (for example, the EU AI Act), set add_ai_watermark to true inside the top-level options object. When enabled, the batch image processor bakes a small "AI-generated" disclosure mark into the bottom-right corner of every output image (and its thumbnails). The mark is irreversible as it is part of the clean output file, not a removable overlay.

This flag is independent of the on-model branding watermark and defaults to false, so existing integrations are unaffected.

Request with AI disclosure
{
  "identity_code": "id_xyz...",
  "project_id": "abc123...",
  "images": ["sku_001...", "sku_002..."],
  "instructions": [/* ... */],
  "options": {
    "add_ai_watermark": true
  }
}

Per-image annotations

You can provide optional styling notes for individual images to control how garments should be worn. This is useful for specifying details like tucking, layering order, sleeve rolling, zipper position, or draping that the AI might otherwise interpret differently.

The images field accepts two formats:

FormatExampleDescription
Simple["uuid-1", "uuid-2"]List of file IDs (default, backward compatible)
Annotated[{"file_id": "uuid-1", "note": "..."}, ...]Objects with optional note per image

Both formats can be mixed. Images without notes behave exactly as before.

Request with per-image annotations
{
  "identity_code": "id_xyz...",
  "project_id": "abc123...",
  "images": [
    {"file_id": "sku_001...", "note": "tucked into pants"},           
    {"file_id": "sku_002...", "note": "slim fit, cuffed at ankles"},  
    {"file_id": "sku_003..."}                                         // No note (auto-detected)
  ],
  "instructions": [/* ... */]
}

Example notes:

  • "tucked into pants" or "untucked, hanging loose"
  • "outer layer, zipper open" or "zipped up to the neck"
  • "sleeves rolled up to elbows"
  • "draped over shoulders, not worn through sleeves"
  • "this is the inner layer, worn under the jacket"

Post-processing

Set post_process to true in the job request to enable automatic post-processing of results. The job status will include post_processing_status to track this additional step.

Request with post-processing
{
  "identity_code": "id_xyz...",
  "project_id": "abc123...",
  "images": ["sku_001...", "sku_002..."],
  "instructions": [/* ... */],
  "post_process": true
}

Tracking progress

Use either SSE or webhooks to receive notifications for job updates.

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:])
        data = notification.get("data", {})
        task_id = data.get("id_task") or data.get("job_id")
        if task_id != job_id:
            continue

        print(f"Notification: {notification['name']}")
        print(f"Data: {data}")

        if notification["name"] == "batch_edit":
            status = data.get("status")
            if status == "completed":
                print("Job completed!")
                break
            if status == "failed":
                raise RuntimeError(data.get("error_message", "Job 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()
    data = notification.get("data", {})
    task_id = data.get("id_task") or data.get("job_id")
    if task_id != job_id:
        return "", 204

    if notification["name"] == "batch_edit":
        status = data.get("status")
        if status == "completed":
            print("Job completed!")
        elif status == "failed":
            print(f"Error: {data.get('error_message')}")

    return "", 204


app.run(port=8000)
Response
[
  {
    "id": 12345,
    "name": "batch_edit",             // Notification type
    "timestamp": 1702819200.0,
    "data": {                         // Job-specific data
      "id_task": "job_abc123...",
      "status": "completed",
      "total_images": 2,
      "processed_images": 2
    }
  }
]

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

Retrieving results

Once the job is complete, retrieve the processed images.

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"]:
    print(f"Image {result['image_index']}: {result['output']['full_size']}")
Response
{
  "job_id": "job_abc123...",
  "job_type": "flat_2_model",
  "status": "completed",
  "results": [
    {
      "image_index": 0,
      "group_index": 0,
      "output": {
        "full_size": "https://...",   // Result image URL
        "thumbnail": "https://..."
      },
      "model_used": "nano_banana_pro", // Engine that produced this output
      "status": "completed"
    },
    {
      "image_index": 1,
      "group_index": 0,
      "output": {
        "full_size": "https://...",
        "thumbnail": "https://..."
      },
      "model_used": "seedream",
      "status": "completed"
    }
  ],
  "summary": {
    // Job statistics
  }
}

Which engine produced each output

Each result carries a model_used string indicating which engine actually generated the image. When model: "auto" is requested, most outputs return the default engine, but individual outputs may fall back to the alternative engine if the default refuses the content. Inspect this field when you need to know per output.

for result in response["results"]:
    print(f"Image {result['image_index']}: model_used = {result.get('model_used')}")

Bulk download

For bulk downloads, generate a temporary download URL that packages all results into a ZIP file.

import requests

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

# Generate download URL
response = requests.post(
    api_url + "/download",
    headers={"Authorization": "Bearer " + access_token},
    json={"job_id": job_id},
).json()

download_url = response["download_url"]
expires = response["expires"]

print(f"Download URL: {download_url}")
print(f"Expires: {expires}")

# Download the ZIP file (no auth required for the token URL)
zip_response = requests.get(download_url)
with open("results.zip", "wb") as f:
    f.write(zip_response.content)
Response
{
  "download_url": "https://v2.api.piktid.com/download/token123...",
  "expires": "2024-12-17T11:00:00Z"  // URL expiration time
}

Checking job status

You can also check the job status directly without waiting for notifications.

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}/status",
    headers={"Authorization": "Bearer " + access_token},
).json()

print(f"Status: {response['status']}")
print(f"Progress: {response['progress']}%")
print(f"Processed: {response['processed_images']}/{response['total_images']}")
Response
{
  "job_id": "job_abc123...",
  "job_type": "flat_2_model",
  "status": "processing",
  "progress": 50.0,
  "total_images": 2,
  "processed_images": 1,
  "should_post_process": false,
  "post_processing_status": null,
  "created_at": "2024-12-17T10:00:00Z",
  "updated_at": "2024-12-17T10:05:00Z"
}

Error handling

Jobs may fail due to various reasons. Check the error_message field in the job status or results.

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()

if response["status"] == "failed":
    print(f"Job failed: {response['error_message']}")
else:
    for result in response["results"]:
        if result["status"] == "failed":
            print(f"Image {result['image_index']} failed: {result['error_message']}")

On this page