# Anonymize
URL: /docs/v1/anonymize
Applies to API version: v1
Description: Remove face identities and replace them with AI generated ones
You will need an API token to send HTTP requests. See [Authentication](/docs/v1/auth) for instructions.
## Quick start [#quick-start]
Firstly, you'll need to upload the image you want to process. These parameters must be supplied as **multipart form data**:
| Parameter | Example | Description |
| --------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------ |
| `file` | `@file.png` | The image file as binary data. |
| `options` | `"{\"flag_hair\":true,\"flag_sync\":true,\"mode\":\"random\"}"{:json}` | Extra options for the generation process. See [Options](#options). |
Please note that we only support the following formats: **WEBP**, **JPEG** and **PNG**.
Note down these fields from the response:
```jsonc title="Response"
{
"image_id": "67762577...", // IMAGE_ID [!code highlight]
"face_description_list": [
{
"a": { "Age": 25, "Gender": "male" }, // [!code highlight]
"f": 0 // FACE_ID [!code highlight]
}
],
"faces": {
"number_of_faces": 2,
"coordinates_list": [
{
"id": 0 // FACE_ID [!code highlight]
// ...
},
{
"id": 1 // FACE_ID [!code highlight]
// ...
}
]
}
}
```
For more information about the data contained in `coordinates_list` and how to process it, see [Coordinates](/docs/v1/concepts/coordinates).
To understand how to use the `image_id` parameter, also see [Image ID](/docs/v1/concepts/image-id).
You can also find out how to use the objects in `face_description_list` in the [Prompts](#prompts) section.
***
To anonymize a face, you will have to call the endpoint **once for each face** you want to change.
This will start asynchronous processes that will be handled in the next step.
This is an example of a request containing all required fields for face with `"id": 0` (zero):
```jsonc title="Request"
{
"id_image": "67762577...", // IMAGE_ID
"id_face": "0", // FACE_ID as string
"prompt": "{\"Country\":\"Austria\"}"
}
```
For more information on the usage of the `prompt` parameter, see [Prompts](#prompts).
***
Notification objects returned by this endpoint will inform you of the end of a generation on our servers.
We recommend sending a request every 3 seconds at most, to not incur in rate limiting.
```jsonc title="Request"
{
"name_list": ["new_generation"]
}
```
After the process is complete, you might see a similar response:
```jsonc title="Response"
{
"notifications_list": [
{
"data": {
"f": 0, // FACE_ID
"g": 0, // GENERATION_ID [!code highlight]
"id_image": "67762577...", // IMAGE_ID
"link": "https://...",
},
"name": "new_generation",
},
]
}
```
Where `link` points to a cropped thumbnail of the face with the new face you requested in the previous step.
The `generation_id` field is a unique identifier for the process that was started in the previous step and has now ended.
If you start another anonymization, the result will have a different `generation_id`. This ID is needed in the next step.
***
When you're satisfied with a new face from the previous step, you can request the server to place the newly generated face on the original image you started with.
```jsonc title="Request"
{
"id_image": "67762577...", // IMAGE_ID
"id_face": "0", // FACE_ID as string
"id_generation": "2" // GENERATION_ID [!code highlight]
}
```
## Uploading an image [#uploading-an-image]
All target images are deleted automatically after 24 hours of being uploaded. This action cannot be undone.
You can upload any image containing **up to 30 people**. Their faces will be automatically detected by the API.
Please note that we only support the following formats: **WEBP**, **JPEG** and **PNG**.
The response will also contain some prompts detected in the image. You can use these prompts as a base for later requests.
```python lineNumbers
import json
import requests
api_url = "https://api.piktid.com/api"
access_token = "your_access_token"
target_path = "path_to_image"
options = {
"flag_hair": True,
"flag_sync": True,
"mode": "random"
}
def get_prompts(response, face_id):
return filter(
lambda r: r["f"] == face_id,
response.get("face_description_list")
)
with open(target_path, "rb") as target:
response = requests.post(
api_url + "/upload_pro",
headers={"Authorization": "Bearer " + access_token},
data={"options": json.dumps(options)},
files={"file": target},
).json()
image_id = response.get("image_id")
prompts = get_prompts(response, 0)
```
### Options [#options]
The `options` parameter in the request body must be formatted as a JSON-encoded string. It can contain the following parameters:
| Parameter | Values | Description |
| ----------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
| `flag_hair` | `true{:json}` or `false{:json}` | If `false{:json}`, the server will carefully avoid drawing over the subject's hair. |
| `flag_sync` | `true{:json}` or `false{:json}` | If `false{:json}`, the server will not run the detection step but only upload the image and create an `image_id`. |
| `mode` | `"random"{:json}` or `"keep"{:json}` | If set to `"keep"{:json}`, changes the expression on the selected faces. See [Change expression](/docs/v1/change-expression) for that. |
## Generating a new face [#generating-a-new-face]
Starting a face generation process is easy, but make sure to handle it right.
Everything is done asynchronously and the endpoint will return a success code if the process was started successfully.
```python lineNumbers
import json
import requests
api_url = "https://api.piktid.com/api"
access_token = "your_access_token"
image_id = "67762577..."
prompts = {
0: {
"Country": "Japan",
"Emotion": "serious",
"Eyes": "intelligent",
},
1: {
"Gender": "female",
"Glasses": "eyeglasses",
"LightPosition": "frontal",
"Light": "natural",
"Lips": "full",
}
}
for face_id, prompt in prompts.items():
requests.post(
api_url + "/ask_random_face",
headers={"Authorization": "Bearer " + access_token},
json={
"id_image": image_id,
"id_face": face_id,
"flag_sync": False,
"prompt": json.dumps(prompt)
},
)
```
For more information on the usage of the `prompt` parameter, see [Prompts](#prompts).
### Prompts [#prompts]
Face generation prompts are defined as a group of key-value pairs, where:
* the key is a facial feature
* the value is an expression or physical state of the key
To better explain this concept, here are the currently defined prompts as programming language type definitions:
```python
from dataclasses import dataclass
from typing import Literal
@dataclass
class Prompt:
Age: Literal[
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99
]
Country: Literal[
"Afghanistan",
"Albania",
"Algeria",
"Andorra",
"Angola",
"Antigua",
"Argentina",
"Armenia",
"Australia",
"Austria",
"Azerbaijan",
"Bahamas",
"Bahrain",
"Bangladesh",
"Barbados",
"Belarus",
"Belgium",
"Belize",
"Benin",
"Bhutan",
"Bolivia",
"Bosnia Herzegovina",
"Botswana",
"Brazil",
"Brunei",
"Bulgaria",
"Burkina",
"Burundi",
"Cambodia",
"Cameroon",
"Canada",
"Cape Verde",
"Central African Rep",
"Chad",
"Chile",
"China",
"Colombia",
"Comoros",
"Congo",
"Costa Rica",
"Croatia",
"Cuba",
"Cyprus",
"Czech Republic",
"Denmark",
"Djibouti",
"Dominica",
"Dominican Republic",
"East Timor",
"Ecuador",
"Egypt",
"El Salvador",
"Equatorial Guinea",
"Eritrea",
"Estonia",
"Ethiopia",
"Fiji",
"Finland",
"France",
"Gabon",
"Gambia",
"Georgia",
"Germany",
"Ghana",
"Greece",
"Grenada",
"Guatemala",
"Guinea",
"Guinea-Bissau",
"Guyana",
"Haiti",
"Honduras",
"Hungary",
"Iceland",
"India",
"Indonesia",
"Iran",
"Iraq",
"Ireland",
"Israel",
"Italy",
"Ivory Coast",
"Jamaica",
"Japan",
"Jordan",
"Kazakhstan",
"Kenya",
"Kiribati",
"Korea North",
"Korea South",
"Kosovo",
"Kuwait",
"Kyrgyzstan",
"Laos",
"Latvia",
"Lebanon",
"Lesotho",
"Liberia",
"Libya",
"Liechtenstein",
"Lithuania",
"Luxembourg",
"Macedonia",
"Madagascar",
"Malawi",
"Malaysia",
"Maldives",
"Mali",
"Malta",
"Marshall Islands",
"Mauritania",
"Mauritius",
"Mexico",
"Micronesia",
"Moldova",
"Monaco",
"Mongolia",
"Montenegro",
"Morocco",
"Mozambique",
"Myanmar",
"Namibia",
"Nauru",
"Nepal",
"Netherlands",
"New Zealand",
"Nicaragua",
"Niger",
"Nigeria",
"Norway",
"Oman",
"Pakistan",
"Palau",
"Panama",
"Papua New Guinea",
"Paraguay",
"Peru",
"Philippines",
"Poland",
"Portugal",
"Qatar",
"Romania",
"Russian Federation",
"Rwanda",
"St Lucia",
"Saint Vincent",
"Samoa",
"San Marino",
"Sao Tome & Principe",
"Saudi Arabia",
"Senegal",
"Serbia",
"Seychelles",
"Sierra Leone",
"Singapore",
"Slovakia",
"Slovenia",
"Solomon Islands",
"Somalia",
"South Africa",
"South Sudan",
"Spain",
"Sri Lanka",
"Sudan",
"Suriname",
"Swaziland",
"Sweden",
"Switzerland",
"Syria",
"Taiwan",
"Tajikistan",
"Tanzania",
"Thailand",
"Togo",
"Tonga",
"Trinidad & Tobago",
"Tunisia",
"Turkey",
"Turkmenistan",
"Tuvalu",
"Uganda",
"Ukraine",
"United Arab Emirates",
"United Kingdom",
"United States",
"Uruguay",
"Uzbekistan",
"Vanuatu",
"Vatican City",
"Venezuela",
"Vietnam",
"Yemen",
"Zambia",
"Zimbabwe",
]
Emotion: Literal[
"affectionate",
"agitated",
"amused",
"angry",
"annoyed",
"anxious",
"astonished",
"astounded",
"caring",
"comforted",
"compassionate",
"confident",
"confused",
"content",
"delighted",
"depressed",
"despairing",
"devoted",
"disgusted",
"dumbfounded",
"elated",
"empathetic",
"enamored",
"enraged",
"excited",
"fearful",
"furious",
"grateful",
"grinning",
"gloomy",
"happy",
"heartbroken",
"hopeless",
"indignant",
"irritated",
"isolated",
"joyful",
"lonely",
"lost",
"loved",
"melancholic",
"miserable",
"motivated",
"nauseated",
"nervous",
"offended",
"panicked",
"passionate",
"perplexed",
"pleased",
"rejected",
"relieved",
"repulsed",
"resentful",
"sad",
"scared",
"serious",
"shocked",
"speechless",
"stressed",
"surprised",
"sympathetic",
"tender",
"terrified",
"tense",
"warm",
"worried",
]
Expression: Literal[
"amazed",
"amused",
"angry",
"astonished",
"awe-struck",
"appalled",
"bewildered",
"bored",
"calm",
"cheerful",
"confused",
"content",
"curious",
"dejected",
"delighted",
"distraught",
"drowsy",
"dumbfounded",
"ecstatic",
"elated",
"exhausted",
"frustrated",
"furious",
"gloomy",
"grinning",
"happy",
"heartbroken",
"impressed",
"indignant",
"interested",
"intrigued",
"irritated",
"jovial",
"laughing",
"merry",
"miserable",
"nauseated",
"offended",
"peaceful",
"perplexed",
"pleased",
"radiant",
"relaxed",
"repulsed",
"resentful",
"sad",
"serene",
"shocked",
"sleepy",
"smiling",
"sorrowful",
"speechless",
"surprised",
"thrilled",
"tired",
"unhappy",
"upset",
"weary",
"cry",
"disgusted",
"fearful",
"pathetic",
"suspicious",
]
Eyes: Literal[
"adorable",
"alluring",
"amusing",
"animated",
"astute",
"attractive",
"beautiful",
"blissful",
"brilliant",
"captivating",
"celestial",
"charismatic",
"charming",
"cheerful",
"clever",
"closed",
"compassionate",
"confident",
"constructive",
"creative",
"cute",
"curious",
"delightful",
"delicate",
"divine",
"dynamic",
"effective",
"elegant",
"energetic",
"enchanting",
"endearing",
"enlightened",
"entertaining",
"enthusiastic",
"ethereal",
"expressive",
"expert",
"fascinating",
"friendly",
"funny",
"genius",
"genuine",
"gifted",
"glistening",
"gorgeous",
"graceful",
"happy",
"heavenly",
"hopeful",
"honest",
"hypnotic",
"imaginative",
"innovative",
"inquisitive",
"insightful",
"intelligent",
"intense",
"inviting",
"joyful",
"keen",
"kind",
"lively",
"lovely",
"loving",
"luminous",
"magical",
"masterful",
"mesmerizing",
"merry",
"magnetic",
"motivating",
"mysterious",
"observant",
"open",
"optimistic",
"passionate",
"penetrating",
"perceptive",
"playful",
"positive",
"productive",
"proficient",
"quick-witted",
"radiant",
"refined",
"seductive",
"sharp",
"sharp-witted",
"shining",
"silky",
"skillful",
"smart",
"smooth",
"soft",
"sophisticated",
"soulful",
"sparkling",
"spiritual",
"stunning",
"subtle",
"successful",
"supportive",
"sympathetic",
"talented",
"tender",
"transcendent",
"translucent",
"trustworthy",
"uplifting",
"velvety",
"vibrant",
"visionary",
"warm",
"welcoming",
"wise",
"witty",
"wonderful",
"wink_left",
"wink_right",
]
EyesColor: Literal[
"blue",
"light-blue",
"blue-gray",
"brown",
"green",
"hazel",
"gray",
"amber",
"black",
]
EyesShape: Literal[
"almond-shaped",
"round",
"hooded",
"upturned",
"downturned",
"monolid",
"deep-set",
"wide-set",
"close-set",
"protruding",
]
Eyelashes: Literal[
"long",
"short",
"curly",
"straight",
"dense",
"sparse",
"shiny",
"matte",
"black",
"brown",
"blonde",
"rough",
"damaged",
]
Eyebrows: Literal[
"arched",
"low",
"thick",
"thin",
"long",
"short",
"straight",
"curved",
"dense",
"sparse",
]
Face: Literal[
"asymmetrical",
"balanced",
"bulbous",
"diamond-shaped",
"disproportionate",
"graceful",
"heart-shaped",
"oval",
"rectangular",
"round",
"square",
"symmetrical",
]
FacialHair: Literal[
"beard",
"mustache",
"none",
]
Gaze: Literal[
"left",
"left-up",
"left-down",
"right",
"right-up",
"right-down",
"straight",
"straight-up",
"straight-down",
]
Gender: Literal[
"female",
"male",
"none",
]
Glasses: Literal[
"eyeglasses",
"sunglasses",
]
LightPosition: Literal[
"behind",
"frontal",
"left",
"right",
"top",
"bottom",
]
Light: Literal[
"soft",
"dynamic",
"white",
"blue",
"red",
"green",
"neon",
"dramatic",
"studio",
"city",
"warm",
"dark",
"natural",
"vivid",
]
Lips: Literal[
"bow-shaped",
"cupid's bow",
"downturned",
"full",
"fuller upper",
"fuller lower",
"thin",
"plump",
"wide",
"narrow",
"straight",
"rounded",
"heart-shaped",
"tapered",
]
Hair: Literal[
"bun",
"bald",
"coarse",
"dated",
"dull",
"elegant",
"fine",
"flat",
"flowing",
"frizzy",
"greasy",
"kempt",
"messy",
"polished",
"silky",
"sleek",
"sophisticated",
"sparse",
"straggly",
"stringy",
"textured",
"thick",
"trendy",
"unkempt",
"unruly",
"voluminous",
"youthful",
]
HairType: Literal[
"curly",
"coily",
"kinky",
"straight",
"wavy",
]
HairColor: Literal[
"ashy",
"auburn",
"black",
"blonde",
"caramel",
"chestnut",
"copper",
"gray",
"golden",
"platinum",
"red",
"strawberry",
"white",
]
HairLength: Literal[
"short",
"medium",
"long",
"pixie",
"bob",
]
Makeup: Literal[
"flawless",
"natural",
"matte",
"tantalizing",
"porcelain",
"radiant",
"warm",
"cool",
"sparkling",
"vivid",
]
Mouth: Literal[
"arguing",
"babbling",
"beaming",
"bickering",
"biting",
"breathing",
"browsing",
"calm",
"caring",
"chatting",
"chewing",
"chuckling",
"closed",
"confused",
"content",
"crying",
"debating",
"discussing",
"distressed",
"drowsy",
"exhausted",
"frowning",
"gasping",
"gargling",
"giggling",
"gnawing",
"gossiping",
"grinning",
"grimacing",
"groaning",
"hissing",
"half-open",
"intrigued",
"jabbering",
"laughing",
"licking",
"lisping",
"lips-pointed",
"mouthing",
"moaning",
"munching",
"mumbling",
"nibbling",
"open",
"pouting",
"prattling",
"relaxed",
"screaming",
"serene",
"shouting",
"sighing",
"singing",
"sleepy",
"slurring",
"smiling",
"smirking",
"slobbering",
"stammering",
"stressed",
"stuttering",
"sucking",
"surprised",
"talking",
"tense",
"tired",
"weary",
"whimpering",
"whispering",
"whistling",
]
Nose: Literal[
"big",
"bumpy",
"button",
"crooked",
"cute",
"delicate",
"elegant",
"graceful",
"hooked",
"large",
"long",
"perfect",
"petite",
"prominent",
"refined",
"shapely",
"straight",
"wide",
]
Region: Literal[
"North America",
"South America",
"Central America",
"East America",
"West America",
"North Asia",
"South Asia",
"Central Asia",
"East Asia",
"West Asia",
"North Europe",
"South Europe",
"Central Europe",
"East Europe",
"West Europe",
"North Africa",
"South Africa",
"Central Africa",
"East Africa",
"West Africa",
"Middle East",
"Australia",
"Oceania",
"Caribbean",
]
Skin: Literal[
"clear",
"dark",
"dull",
"dehydrated",
"fair",
"flushed",
"flawless",
"freckled",
"flowing",
"hydrated",
"light",
"matte",
"natural",
"oily",
"pale",
"pale-white",
"radiant",
"rough",
"silky",
"smooth",
"soft",
"spotty",
"supple",
"sweaty",
"wrinkled",
"tanned",
"taut",
]
Smile: Literal[
"bright",
"radiant",
"infectious",
"joyful",
"beaming",
"cheery",
"blissful",
"enchanting",
"captivating",
"charming",
"delightful",
"enthralling",
"exquisite",
"fetching",
"gorgeous",
"heavenly",
"lovely",
"magical",
"mesmerizing",
"pretty",
"ravishing",
"splendid",
"stunning",
"sublime",
"superb",
"sweet",
"wonderful",
"adorable",
"attractive",
"beautiful",
"bewitching",
"cute",
"divine",
"elegant",
"fascinating",
"graceful",
"irresistible",
"marvelous",
"seductive",
"sensational",
"sensual",
"tantalizing",
"tender",
"vibrant",
"alluring",
"amiable",
"beguiling",
"comforting",
"endearing",
"heartwarming",
"likeable",
"magnetic",
"personable",
"winning",
]
Teeth: Literal[
"aligned",
"beaming",
"blinding",
"braced",
"bright",
"broken",
"chewy",
"chipped",
"clean",
"clear",
"crunchy",
"crystalline",
"crooked",
"crowned",
"dazzling",
"decayed",
"discolored",
"elastic",
"fixed",
"flawless",
"gapped",
"gleaming",
"glassy",
"immaculate",
"ivory",
"juicy",
"luminous",
"lustrous",
"lucid",
"misaligned",
"missing",
"orthodontic",
"pearlescent",
"perfect",
"plump",
"polished",
"radiant",
"resplendent",
"rubbery",
"satiny",
"shimmering",
"shiny",
"sheer",
"silky",
"sleek",
"smile",
"smooth",
"soft",
"spongy",
"spotless",
"sparkling",
"stained",
"straight",
"supple",
"tender",
"translucent",
"velvety",
"white",
"yellowed",
]
```
```ts
type Prompt = Partial<{
Age:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99
Country:
| "Afghanistan"
| "Albania"
| "Algeria"
| "Andorra"
| "Angola"
| "Antigua"
| "Argentina"
| "Armenia"
| "Australia"
| "Austria"
| "Azerbaijan"
| "Bahamas"
| "Bahrain"
| "Bangladesh"
| "Barbados"
| "Belarus"
| "Belgium"
| "Belize"
| "Benin"
| "Bhutan"
| "Bolivia"
| "Bosnia Herzegovina"
| "Botswana"
| "Brazil"
| "Brunei"
| "Bulgaria"
| "Burkina"
| "Burundi"
| "Cambodia"
| "Cameroon"
| "Canada"
| "Cape Verde"
| "Central African Rep"
| "Chad"
| "Chile"
| "China"
| "Colombia"
| "Comoros"
| "Congo"
| "Costa Rica"
| "Croatia"
| "Cuba"
| "Cyprus"
| "Czech Republic"
| "Denmark"
| "Djibouti"
| "Dominica"
| "Dominican Republic"
| "East Timor"
| "Ecuador"
| "Egypt"
| "El Salvador"
| "Equatorial Guinea"
| "Eritrea"
| "Estonia"
| "Ethiopia"
| "Fiji"
| "Finland"
| "France"
| "Gabon"
| "Gambia"
| "Georgia"
| "Germany"
| "Ghana"
| "Greece"
| "Grenada"
| "Guatemala"
| "Guinea"
| "Guinea-Bissau"
| "Guyana"
| "Haiti"
| "Honduras"
| "Hungary"
| "Iceland"
| "India"
| "Indonesia"
| "Iran"
| "Iraq"
| "Ireland"
| "Israel"
| "Italy"
| "Ivory Coast"
| "Jamaica"
| "Japan"
| "Jordan"
| "Kazakhstan"
| "Kenya"
| "Kiribati"
| "Korea North"
| "Korea South"
| "Kosovo"
| "Kuwait"
| "Kyrgyzstan"
| "Laos"
| "Latvia"
| "Lebanon"
| "Lesotho"
| "Liberia"
| "Libya"
| "Liechtenstein"
| "Lithuania"
| "Luxembourg"
| "Macedonia"
| "Madagascar"
| "Malawi"
| "Malaysia"
| "Maldives"
| "Mali"
| "Malta"
| "Marshall Islands"
| "Mauritania"
| "Mauritius"
| "Mexico"
| "Micronesia"
| "Moldova"
| "Monaco"
| "Mongolia"
| "Montenegro"
| "Morocco"
| "Mozambique"
| "Myanmar"
| "Namibia"
| "Nauru"
| "Nepal"
| "Netherlands"
| "New Zealand"
| "Nicaragua"
| "Niger"
| "Nigeria"
| "Norway"
| "Oman"
| "Pakistan"
| "Palau"
| "Panama"
| "Papua New Guinea"
| "Paraguay"
| "Peru"
| "Philippines"
| "Poland"
| "Portugal"
| "Qatar"
| "Romania"
| "Russian Federation"
| "Rwanda"
| "St Lucia"
| "Saint Vincent"
| "Samoa"
| "San Marino"
| "Sao Tome & Principe"
| "Saudi Arabia"
| "Senegal"
| "Serbia"
| "Seychelles"
| "Sierra Leone"
| "Singapore"
| "Slovakia"
| "Slovenia"
| "Solomon Islands"
| "Somalia"
| "South Africa"
| "South Sudan"
| "Spain"
| "Sri Lanka"
| "Sudan"
| "Suriname"
| "Swaziland"
| "Sweden"
| "Switzerland"
| "Syria"
| "Taiwan"
| "Tajikistan"
| "Tanzania"
| "Thailand"
| "Togo"
| "Tonga"
| "Trinidad & Tobago"
| "Tunisia"
| "Turkey"
| "Turkmenistan"
| "Tuvalu"
| "Uganda"
| "Ukraine"
| "United Arab Emirates"
| "United Kingdom"
| "United States"
| "Uruguay"
| "Uzbekistan"
| "Vanuatu"
| "Vatican City"
| "Venezuela"
| "Vietnam"
| "Yemen"
| "Zambia"
| "Zimbabwe"
Emotion:
| "affectionate"
| "agitated"
| "amused"
| "angry"
| "annoyed"
| "anxious"
| "astonished"
| "astounded"
| "caring"
| "comforted"
| "compassionate"
| "confident"
| "confused"
| "content"
| "delighted"
| "depressed"
| "despairing"
| "devoted"
| "disgusted"
| "dumbfounded"
| "elated"
| "empathetic"
| "enamored"
| "enraged"
| "excited"
| "fearful"
| "furious"
| "grateful"
| "grinning"
| "gloomy"
| "happy"
| "heartbroken"
| "hopeless"
| "indignant"
| "irritated"
| "isolated"
| "joyful"
| "lonely"
| "lost"
| "loved"
| "melancholic"
| "miserable"
| "motivated"
| "nauseated"
| "nervous"
| "offended"
| "panicked"
| "passionate"
| "perplexed"
| "pleased"
| "rejected"
| "relieved"
| "repulsed"
| "resentful"
| "sad"
| "scared"
| "serious"
| "shocked"
| "speechless"
| "stressed"
| "surprised"
| "sympathetic"
| "tender"
| "terrified"
| "tense"
| "warm"
| "worried"
Expression:
| "amazed"
| "amused"
| "angry"
| "astonished"
| "awe-struck"
| "appalled"
| "bewildered"
| "bored"
| "calm"
| "cheerful"
| "confused"
| "content"
| "curious"
| "dejected"
| "delighted"
| "distraught"
| "drowsy"
| "dumbfounded"
| "ecstatic"
| "elated"
| "exhausted"
| "frustrated"
| "furious"
| "gloomy"
| "grinning"
| "happy"
| "heartbroken"
| "impressed"
| "indignant"
| "interested"
| "intrigued"
| "irritated"
| "jovial"
| "laughing"
| "merry"
| "miserable"
| "nauseated"
| "offended"
| "peaceful"
| "perplexed"
| "pleased"
| "radiant"
| "relaxed"
| "repulsed"
| "resentful"
| "sad"
| "serene"
| "shocked"
| "sleepy"
| "smiling"
| "sorrowful"
| "speechless"
| "surprised"
| "thrilled"
| "tired"
| "unhappy"
| "upset"
| "weary"
| "cry"
| "disgusted"
| "fearful"
| "pathetic"
| "suspicious"
Eyes:
| "adorable"
| "alluring"
| "amusing"
| "animated"
| "astute"
| "attractive"
| "beautiful"
| "blissful"
| "brilliant"
| "captivating"
| "celestial"
| "charismatic"
| "charming"
| "cheerful"
| "clever"
| "closed"
| "compassionate"
| "confident"
| "constructive"
| "creative"
| "cute"
| "curious"
| "delightful"
| "delicate"
| "divine"
| "dynamic"
| "effective"
| "elegant"
| "energetic"
| "enchanting"
| "endearing"
| "enlightened"
| "entertaining"
| "enthusiastic"
| "ethereal"
| "expressive"
| "expert"
| "fascinating"
| "friendly"
| "funny"
| "genius"
| "genuine"
| "gifted"
| "glistening"
| "gorgeous"
| "graceful"
| "happy"
| "heavenly"
| "hopeful"
| "honest"
| "hypnotic"
| "imaginative"
| "innovative"
| "inquisitive"
| "insightful"
| "intelligent"
| "intense"
| "inviting"
| "joyful"
| "keen"
| "kind"
| "lively"
| "lovely"
| "loving"
| "luminous"
| "magical"
| "masterful"
| "mesmerizing"
| "merry"
| "magnetic"
| "motivating"
| "mysterious"
| "observant"
| "open"
| "optimistic"
| "passionate"
| "penetrating"
| "perceptive"
| "playful"
| "positive"
| "productive"
| "proficient"
| "quick-witted"
| "radiant"
| "refined"
| "seductive"
| "sharp"
| "sharp-witted"
| "shining"
| "silky"
| "skillful"
| "smart"
| "smooth"
| "soft"
| "sophisticated"
| "soulful"
| "sparkling"
| "spiritual"
| "stunning"
| "subtle"
| "successful"
| "supportive"
| "sympathetic"
| "talented"
| "tender"
| "transcendent"
| "translucent"
| "trustworthy"
| "uplifting"
| "velvety"
| "vibrant"
| "visionary"
| "warm"
| "welcoming"
| "wise"
| "witty"
| "wonderful"
| "wink_left"
| "wink_right"
EyesColor:
| "blue"
| "light-blue"
| "blue-gray"
| "brown"
| "green"
| "hazel"
| "gray"
| "amber"
| "black"
EyesShape:
| "almond-shaped"
| "round"
| "hooded"
| "upturned"
| "downturned"
| "monolid"
| "deep-set"
| "wide-set"
| "close-set"
| "protruding"
Eyelashes:
| "long"
| "short"
| "curly"
| "straight"
| "dense"
| "sparse"
| "shiny"
| "matte"
| "black"
| "brown"
| "blonde"
| "rough"
| "damaged"
Eyebrows:
| "arched"
| "low"
| "thick"
| "thin"
| "long"
| "short"
| "straight"
| "curved"
| "dense"
| "sparse"
Face:
| "asymmetrical"
| "balanced"
| "bulbous"
| "diamond-shaped"
| "disproportionate"
| "graceful"
| "heart-shaped"
| "oval"
| "rectangular"
| "round"
| "square"
| "symmetrical"
FacialHair:
| "beard"
| "mustache"
| "none"
Gaze:
| "left"
| "left-up"
| "left-down"
| "right"
| "right-up"
| "right-down"
| "straight"
| "straight-up"
| "straight-down"
Gender:
| "female"
| "male"
| "none"
Glasses:
| "eyeglasses"
| "sunglasses"
LightPosition:
| "behind"
| "frontal"
| "left"
| "right"
| "top"
| "bottom"
Light:
| "soft"
| "dynamic"
| "white"
| "blue"
| "red"
| "green"
| "neon"
| "dramatic"
| "studio"
| "city"
| "warm"
| "dark"
| "natural"
| "vivid"
Lips:
| "bow-shaped"
| "cupid's bow"
| "downturned"
| "full"
| "fuller upper"
| "fuller lower"
| "thin"
| "plump"
| "wide"
| "narrow"
| "straight"
| "rounded"
| "heart-shaped"
| "tapered"
Hair:
| "bun"
| "bald"
| "coarse"
| "dated"
| "dull"
| "elegant"
| "fine"
| "flat"
| "flowing"
| "frizzy"
| "greasy"
| "kempt"
| "messy"
| "polished"
| "silky"
| "sleek"
| "sophisticated"
| "sparse"
| "straggly"
| "stringy"
| "textured"
| "thick"
| "trendy"
| "unkempt"
| "unruly"
| "voluminous"
| "youthful"
HairType:
| "curly"
| "coily"
| "kinky"
| "straight"
| "wavy"
HairColor:
| "ashy"
| "auburn"
| "black"
| "blonde"
| "caramel"
| "chestnut"
| "copper"
| "gray"
| "golden"
| "platinum"
| "red"
| "strawberry"
| "white"
HairLength:
| "short"
| "medium"
| "long"
| "pixie"
| "bob"
Makeup:
| "flawless"
| "natural"
| "matte"
| "tantalizing"
| "porcelain"
| "radiant"
| "warm"
| "cool"
| "sparkling"
| "vivid"
Mouth:
| "arguing"
| "babbling"
| "beaming"
| "bickering"
| "biting"
| "breathing"
| "browsing"
| "calm"
| "caring"
| "chatting"
| "chewing"
| "chuckling"
| "closed"
| "confused"
| "content"
| "crying"
| "debating"
| "discussing"
| "distressed"
| "drowsy"
| "exhausted"
| "frowning"
| "gasping"
| "gargling"
| "giggling"
| "gnawing"
| "gossiping"
| "grinning"
| "grimacing"
| "groaning"
| "hissing"
| "half-open"
| "intrigued"
| "jabbering"
| "laughing"
| "licking"
| "lisping"
| "lips-pointed"
| "mouthing"
| "moaning"
| "munching"
| "mumbling"
| "nibbling"
| "open"
| "pouting"
| "prattling"
| "relaxed"
| "screaming"
| "serene"
| "shouting"
| "sighing"
| "singing"
| "sleepy"
| "slurring"
| "smiling"
| "smirking"
| "slobbering"
| "stammering"
| "stressed"
| "stuttering"
| "sucking"
| "surprised"
| "talking"
| "tense"
| "tired"
| "weary"
| "whimpering"
| "whispering"
| "whistling"
Nose:
| "big"
| "bumpy"
| "button"
| "crooked"
| "cute"
| "delicate"
| "elegant"
| "graceful"
| "hooked"
| "large"
| "long"
| "perfect"
| "petite"
| "prominent"
| "refined"
| "shapely"
| "straight"
| "wide"
Region:
| "North America"
| "South America"
| "Central America"
| "East America"
| "West America"
| "North Asia"
| "South Asia"
| "Central Asia"
| "East Asia"
| "West Asia"
| "North Europe"
| "South Europe"
| "Central Europe"
| "East Europe"
| "West Europe"
| "North Africa"
| "South Africa"
| "Central Africa"
| "East Africa"
| "West Africa"
| "Middle East"
| "Australia"
| "Oceania"
| "Caribbean"
Skin:
| "clear"
| "dark"
| "dull"
| "dehydrated"
| "fair"
| "flushed"
| "flawless"
| "freckled"
| "flowing"
| "hydrated"
| "light"
| "matte"
| "natural"
| "oily"
| "pale"
| "pale-white"
| "radiant"
| "rough"
| "silky"
| "smooth"
| "soft"
| "spotty"
| "supple"
| "sweaty"
| "wrinkled"
| "tanned"
| "taut"
Smile:
| "bright"
| "radiant"
| "infectious"
| "joyful"
| "beaming"
| "cheery"
| "blissful"
| "enchanting"
| "captivating"
| "charming"
| "delightful"
| "enthralling"
| "exquisite"
| "fetching"
| "gorgeous"
| "heavenly"
| "lovely"
| "magical"
| "mesmerizing"
| "pretty"
| "ravishing"
| "splendid"
| "stunning"
| "sublime"
| "superb"
| "sweet"
| "wonderful"
| "adorable"
| "attractive"
| "beautiful"
| "bewitching"
| "cute"
| "divine"
| "elegant"
| "fascinating"
| "graceful"
| "irresistible"
| "marvelous"
| "seductive"
| "sensational"
| "sensual"
| "tantalizing"
| "tender"
| "vibrant"
| "alluring"
| "amiable"
| "beguiling"
| "comforting"
| "endearing"
| "heartwarming"
| "likeable"
| "magnetic"
| "personable"
| "winning"
Teeth:
| "aligned"
| "beaming"
| "blinding"
| "braced"
| "bright"
| "broken"
| "chewy"
| "chipped"
| "clean"
| "clear"
| "crunchy"
| "crystalline"
| "crooked"
| "crowned"
| "dazzling"
| "decayed"
| "discolored"
| "elastic"
| "fixed"
| "flawless"
| "gapped"
| "gleaming"
| "glassy"
| "immaculate"
| "ivory"
| "juicy"
| "luminous"
| "lustrous"
| "lucid"
| "misaligned"
| "missing"
| "orthodontic"
| "pearlescent"
| "perfect"
| "plump"
| "polished"
| "radiant"
| "resplendent"
| "rubbery"
| "satiny"
| "shimmering"
| "shiny"
| "sheer"
| "silky"
| "sleek"
| "smile"
| "smooth"
| "soft"
| "spongy"
| "spotless"
| "sparkling"
| "stained"
| "straight"
| "supple"
| "tender"
| "translucent"
| "velvety"
| "white"
| "yellowed"
}>
```
In both languages, the following are valid prompt objects:
```json
{
"Age": 40,
"Country": "Japan",
"Emotion": "serious",
"Eyes": "intelligent",
"Eyebrows": "thick",
"Hair": "sleek",
"Gaze": "straight"
}
```
```json
{
"EyesColor": "green",
"EyesShape": "almond-shaped",
"Face": "oval",
"FacialHair": "none",
"Gender": "female",
"Glasses": "eyeglasses",
"LightPosition": "frontal",
"Light": "natural",
"Lips": "full",
"HairType": "wavy",
"HairColor": "chestnut",
"HairLength": "long"
}
```
## Waiting for a generated face [#waiting-for-a-generated-face]
Notifications are automatically deleted after exactly **10 minutes** (600 seconds) from their creation.
Results are shared to clients via "notifications". Clients are expected to poll for notifications and handle them in the allowed time limit before they are automatically dismissed by the API.
```python lineNumbers
import time
import requests
from pprint import pprint
api_url = "https://api.piktid.com/api"
access_token = "your_access_token"
image_id = "67762577..."
def process_notifications(response, image_id):
for notification in response.get("notifications_list", []):
if notification["name"] != "new_generation":
return None, None
if notification["data"]["id_image"] != image_id:
return None, None
return notification["data"], notification["id"]
return None, None
def delete_notification(notification_id):
requests.delete(
api_url + "/notification/delete_json",
headers={"Authorization": "Bearer " + access_token},
json={"id": notification_id},
)
# Loops for 300 seconds, even though generating faces generally
# takes much less time.
for _ in range(300):
response = requests.post(
api_url + "/notification_by_name_json",
headers={"Authorization": "Bearer " + access_token},
json={"name_list": "new_generation,error"},
).json()
data, notification_id = process_notifications(response, image_id)
if data is not None:
# Delete the notification to avoid reading it again
delete_notification(notification_id)
print(f"Face with ID {data['f']} finished processing!")
pprint(data)
break
time.sleep(1)
```
Remember that the results you just received are only relative to a single face and the response does not contain the final image!
## Assembling the final image [#assembling-the-final-image]
All generated images are deleted automatically after 24 hours of being created. This action cannot be undone. Make
sure to download them!
To assemble the final image, you need to "pick" faces from the ones you generated so far (remember `generation_id`?).
Each request picks a single face, so you will need multiple requests to fully assemble the final image.
```python lineNumbers
import json
import requests
api_url = "https://api.piktid.com/api"
access_token = "your_access_token"
image_id = "67762577..."
# Keys are FACE_ID, values are GENERATION_ID
picked_generations = {
0: "2",
1: "1"
}
final_link = None
for face_id, generation_id in picked_generations.items():
response = requests.post(
api_url + "/pick_face2",
headers={"Authorization": "Bearer " + access_token},
json={
"id_image": image_id,
"id_face": face_id,
"id_generation": generation_id,
# "flag_watermark": 0
},
).json()
final_link = response["links"]["l"]
print("Finished processing: ", final_link)
```
users can also disable watermark application on the final image.
# Authentication
URL: /docs/v1/auth
Applies to API version: v1
Description: Authenticate your requests to PiktID APIs
The authentication flow for this API is based on *access* and *refresh* tokens.
Most endpoints in this API are authenticated with the access token, passed in the `Authorization` header, using the `Bearer` scheme.
For added security in single-page applications, the refresh token is also returned in a secure cookie.
## Quick start [#quick-start]
Create a new PiktID account to access apps and APIs
You will first need to manually sign up to use our services.
It's fine if you just want to try out the APIs, as free users get 10 credits and an API token.
***
Tokens can be generated at any time using Basic HTTP authentication, with the email and password used to sign up.
## Generating tokens [#generating-tokens]
Both access and refresh are valid for 30 days (by default) from the time they are issued.
Token pairs can be generated using your email and password as [Basic Authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization#basic_authentication_2).
```python lineNumbers
import requests
api_url = "https://api.piktid.com/api"
EMAIL = "your email"
PASSWORD = "your password"
response = requests.post(
api_url + "/tokens",
data={},
auth=(EMAIL, PASSWORD),
).json()
access_token = response["access_token"]
refresh_token = response["refresh_token"]
```
The freshly generated access token can then be used to authenticate API requests with the `Authentication: Bearer ` header.
## Refreshing tokens [#refreshing-tokens]
Using a refresh token more than once is considered a possible attack and will cause all existing tokens for the user to be revoked immediately as a mitigation measure.
When the access token expires you can refresh it using the associated refresh token:
```python lineNumbers
response = requests.put(
api_url + "/tokens",
json={
"access_token": access_token,
"refresh_token": refresh_token
},
).json()
access_token = response["access_token"]
refresh_token = response["refresh_token"]
```
Remember to store the new refresh token, as the old one is now invalid.
# Change expression
URL: /docs/v1/change-expression
Applies to API version: v1
Description: Change facial expressions with our advanced AI model
You will need an API token to send HTTP requests. See [Authentication](/docs/v1/auth) for instructions.
## Quick start [#quick-start]
Firstly, you'll need to upload the image you want to process. These parameters must be supplied as **multipart form data**:
| Parameter | Example | Description |
| --------- | -------------------------------------------------------------------- | ------------------------------------------------------------------ |
| `file` | `@file.png` | The image file as binary data. |
| `options` | `"{\"flag_hair\":true,\"flag_sync\":true,\"mode\":\"keep\"}"{:json}` | Extra options for the generation process. See [Options](#options). |
Please note that we only support the following formats: **WEBP**, **JPEG** and **PNG**.
Note down these fields from the response:
```jsonc title="Response"
{
"image_id": "67762577...", // IMAGE_ID [!code highlight]
"face_description_list": [
{
"a": { "Age": 25, "Gender": "male" }, // [!code highlight]
"f": 0 // FACE_ID [!code highlight]
}
],
"faces": {
"number_of_faces": 2,
"coordinates_list": [
{
"id": 0 // FACE_ID [!code highlight]
// ...
},
{
"id": 1 // FACE_ID [!code highlight]
// ...
}
]
}
}
```
For more information about the data contained in `coordinates_list` and how to process it, see [Coordinates](/docs/v1/concepts/coordinates).
To understand how to use the `image_id` parameter, also see [Image ID](/docs/v1/concepts/image-id).
You can also find out how to use the objects in `face_description_list` in the [Prompts](#prompts) section.
***
To change the expression on a face, you will have to call the endpoint **once for each face** you want to modify.
This will start asynchronous processes that will be handled in the next step.
This is an example of request containing all required fields for the face with `"id": 0` (zero):
```jsonc title="Request"
{
"id_image": "67762577...", // IMAGE_ID
"id_face": "0", // FACE_ID as string
"prompt": "{\"Expression\":\"happy\"}"
}
```
For more information on the usage of the `prompt` parameter, see [Prompts](#prompts).
***
Notification objects returned by this endpoint will inform you of the end of a generation on our servers.
We recommend sending a request every 3 seconds at most, to not incur in rate limiting.
```jsonc title="Request"
{
"name_list": ["new_generation"]
}
```
After the process is complete, you might see a similar response:
```jsonc title="Response"
{
"notifications_list": [
{
"data": {
"f": 0, // FACE_ID
"g": 0, // GENERATION_ID [!code highlight]
"id_image": "67762577...", // IMAGE_ID
"link": "https://...",
},
"name": "new_generation",
},
]
}
```
Where `link` points to a cropped thumbnail of the face with the expression you requested in the previous step.
The `generation_id` field is a unique identifier for the process that was started in the previous step and has now ended.
If you start another expression change, the result will have a different `generation_id`. This ID is needed in the next step.
***
When you're satisfied with a new expression from the previous step, you can request the server to place the newly generated expression on the original image you started with.
```jsonc title="Request"
{
"id_image": "67762577...", // IMAGE_ID
"id_face": "0", // FACE_ID as string
"id_generation": "2" // GENERATION_ID [!code highlight]
}
```
## Uploading an image [#uploading-an-image]
All target images are deleted automatically after 24 hours of being uploaded. This action cannot be undone.
You can upload any image containing **up to 30 people**. Their faces will be automatically detected by the API.
Please note that we only support the following formats: **WEBP**, **JPEG** and **PNG**.
```python lineNumbers
import json
import requests
api_url = "https://api.piktid.com/api"
access_token = "your_access_token"
target_path = "path_to_image"
options = {
"flag_hair": True,
"flag_sync": True,
"mode": "keep"
}
with open(target_path, "rb") as target:
response = requests.post(
api_url + "/upload_pro",
headers={"Authorization": "Bearer " + access_token},
files={"file": target},
data={"options": json.dumps(options)},
).json()
image_id = response.get("image_id")
```
### Options [#options]
The `options` parameter in the request body must be formatted as a JSON-encoded string. It can contain the following parameters:
| Parameter | Values | Description |
| ----------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `flag_hair` | `true{:json}` or `false{:json}` | If `false{:json}`, the server will carefully avoid drawing over the subject's hair. |
| `flag_sync` | `true{:json}` or `false{:json}` | If `false{:json}`, the server will not run the detection step but only upload the image and create an `image_id`. |
| `mode` | `"keep"{:json}` or `"random"{:json}` | If set to `"random"{:json}`, generates a new face instead of just changing the expression. See [Anonymize](/docs/v1/anonymize) for that. |
## Changing an expression [#changing-an-expression]
Starting an expression change process is easy, but make sure to handle it right.
Everything is done asynchronously and the endpoint will return a success code if the process was started successfully.
```python lineNumbers
import json
import requests
api_url = "https://api.piktid.com/api"
access_token = "your_access_token"
image_id = "67762577..."
expressions = {
0: {"Expression": "happy"},
1: {"Eyes": "wink_right"}
}
for face_id, prompt in expressions.items():
requests.post(
api_url + "/ask_new_expression",
headers={"Authorization": "Bearer " + access_token},
json={
"id_image": image_id,
"id_face": face_id,
"flag_sync": False,
"prompt": json.dumps(prompt)
},
)
```
For more information on the usage of the `prompt` parameter, see [Prompts](#prompts).
### Prompts [#prompts]
Expression prompts are defined as a **single** key-value pair, where:
* the key is a facial feature
* the value is an expression or physical state of the key
To better explain this concepts, here are the currently defined prompts as programming language type definitions:
```python
from dataclasses import dataclass
from typing import Literal
@dataclass
class Prompt:
Expression: Literal[
"happy",
"angry",
"astonished",
"cry",
"disgusted",
"fearful",
"pathetic",
"surprised",
"suspicious",
"neutral",
"sad"
]
Eyes: Literal[
"closed",
"open",
"wink_left",
"wink_right"
]
Gaze: Literal[
"left",
"right",
"up",
"down"
]
```
```ts
type Prompt = Partial<{
Expression:
| "happy"
| "angry"
| "astonished"
| "cry"
| "disgusted"
| "fearful"
| "pathetic"
| "surprised"
| "suspicious"
| "neutral"
| "sad"
Eyes:
| "closed"
| "open"
| "wink_left"
| "wink_right"
Gaze:
| "left"
| "right"
| "up"
| "down"
}>
```
In both languages, the following are valid prompt objects:
```json
{
"Expression": "happy"
}
```
```json
{
"Eyes": "wink_left"
}
```
## Waiting for an expression change [#waiting-for-an-expression-change]
Notifications are automatically deleted after exactly **10 minutes** (600 seconds) from their creation.
Results are shared to clients via "notifications". Clients are expected to poll for notifications and handle them in the allowed time limit before they are automatically dismissed by the API.
```python lineNumbers
import time
import requests
api_url = "https://api.piktid.com/api"
access_token = "your_access_token"
image_id = "67762577..."
def process_notifications(response, image_id):
for notification in response.get("notifications_list", []):
if notification["name"] != "new_generation":
return None, None
if notification["data"]["id_image"] != image_id:
return None, None
return notification["data"], notification["id"]
return None, None
def delete_notification(notification_id):
requests.delete(
api_url + "/notification/delete_json",
headers={"Authorization": "Bearer " + access_token},
json={"id": notification_id},
)
count = 0
# Loops for 300 seconds, even though changing expressions generally
# takes much less time.
for _ in range(300):
response = requests.post(
api_url + "/notification_by_name_json",
headers={"Authorization": "Bearer " + access_token},
json={"name_list": "new_generation,error"},
).json()
data, notification_id = process_notifications(response, image_id)
if data is not None:
# Delete the notification to avoid reading it again
delete_notification(notification_id)
print(f"Face with ID {data['f']} finished processing!")
print(data["link"])
count += 1
if count == len(expressions):
break
time.sleep(1)
```
Remember that the results you just received are only relative to a single face and the response does not contain the final image!
## Assembling the final image [#assembling-the-final-image]
All generated images are deleted automatically after 24 hours of being created. This action cannot be undone. Make
sure to download them!
To assemble the final image, you need to "pick" faces from the ones you generated so far (remember `generation_id`?).
Each request picks a single face, so you will need multiple requests to fully assemble the final image.
The following examples assumes that:
* the face with `face_id` equals to 0 has been regenerated twice and generation 2 is chosen for the final image
* the face with `face_id` equals to 1 has been regenerated once and generation 1 is chosen for the final image
```python lineNumbers
import json
import requests
api_url = "https://api.piktid.com/api"
access_token = "your_access_token"
image_id = "67762577..."
# Keys are FACE_ID, values are GENERATION_ID
picked_generations = {
0: "2",
1: "1"
}
final_link = None
for face_id, generation_id in picked_generations.items():
response = requests.post(
api_url + "/pick_face2",
headers={"Authorization": "Bearer " + access_token},
json={
"id_image": image_id,
"id_face": face_id,
"id_generation": generation_id,
# "flag_watermark": 0
},
).json()
final_link = response["links"]["l"]
print("Finished processing: ", final_link)
```
users can also disable watermark application on the final image.
# Create image
URL: /docs/v1/create-image
Applies to API version: v1
Description: Generate images from a prompt
You will need an API token to send HTTP requests. See [Authentication](/docs/v1/auth) for instructions.
## Quick start [#quick-start]
To generate pictures, you will have to call the endpoint **once for each prompt** you want to use. Each one will generate a new picture.
This will start asynchronous processes that will be handled in the next step.
This is an example of request containing all required fields:
```jsonc title="Request"
{
"prompt": "a picture of a cat",
"aspect_ratio": "16:9"
}
```
For more information on generation options, see [Generation options](#generation-options).
***
Notification objects returned by this endpoint will inform you of the end of a generation on our servers.
We recommend sending a request every 3 seconds at most, to not incur in rate limiting.
```jsonc title="Request"
{
"name_list": ["create"]
}
```
After the process is complete, you might see a similar response:
```jsonc title="Response"
{
"links": [{
"l": "https://..." // Download link [!code highlight]
// ...
}]
}
```
Where `"l"` points to a fully ready, generated image based on the prompt and options you specified in the previous step.
## Generating a new picture [#generating-a-new-picture]
Our API can generate a single picture with a single prompt at a time, but parallel processing is allowed and recommended.
To generate multiple pictures, you just have to send multiple requests.
```python lineNumbers
import json
import requests
api_url = "https://api.piktid.com/api"
access_token = "your_access_token"
# This is explained in more detail in the "Waiting for results" section
image_id = None
prompts = [
"Half-body image of a 25-year-old White man, trimmed beard, wearing a casual leather jacket, relaxed pose, plain grey background.",
"Half-body image of a 70-year-old African woman, colorful traditional dress and headwrap, elegant pose, flat studio white background."
]
aspect_ratio = "16:9"
options= {
"prompt_enhancement_flag": True,
"seed": None
}
for prompt in prompts:
data = requests.post(
api_url + "/create/new",
headers={"Authorization": "Bearer " + access_token},
json={
"id_image": image_id,
"prompt": prompt,
"aspect_ratio": aspect_ratio,
"options": json.dumps(options)
},
).json()
```
### Generation options [#generation-options]
The `aspect_ratio` parameter controls the aspect ratio of the generated images. It must be one of the following strings:
* `"1:2"{:json}`
* `"9:16"{:json}`
* `"2:3"{:json}`
* `"3:4"{:json}`
* `"1:1"{:json}`
* `"4:3"{:json}`
* `"3:2"{:json}`
* `"16:9"{:json}`
* `"2:1"{:json}`
The `image_id` parameter is not required, but can be passed so all subsequent generations will be appended to the history (see [`/history`](/docs/v1/history/post)) of the given image ID.
The first notification for a generated image will contain a valid image ID that can be used for the following generations.
If `image_id` is left undefined, all new generated images will have a different image ID. To learn more about image IDs, see [Image ID](/docs/v1/concepts/image-id).
The `options` parameter in the request body must be formatted as a JSON-encoded string. It can contain the following parameters:
| Parameter | Values | Description |
| ------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `prompt_enhancement_flag` | Boolean | Turns on (true) or off (false) the prompt enhancer. It is responsible for making simple prompts generate higher quality images. |
| `seed` | Integer number | Seed used for random number generation. Two requests with the same seed will have the same result. If undefined, it will be randomized server-side. |
## Waiting for results [#waiting-for-results]
Notifications are automatically deleted after exactly **10 minutes** (600 seconds) from their creation.
Results are shared to clients via "notifications". Clients are expected to poll for notifications and handle them in the allowed time limit before they are automatically dismissed by the API.
```python lineNumbers
import time
import requests
from pprint import pprint
api_url = "https://api.piktid.com/api"
access_token = "your_access_token"
image_id = "67762577..."
def process_notifications(response):
for notification in response.get("notifications_list", []):
if notification["name"] != "create":
return None, None
return notification["data"], notification["id"]
return None, None
def delete_notification(notification_id):
requests.delete(
api_url + "/notification/delete_json",
headers={"Authorization": "Bearer " + access_token},
json={"id": notification_id},
)
count = 0
# Loops for 10 minutes, even though generation generally
# takes less time.
for _ in range(600):
response = requests.post(
api_url + "/notification_by_name_json",
headers={"Authorization": "Bearer " + access_token},
json={"name_list": "create,error"},
).json()
data, notification_id = process_notifications(response)
if data is not None:
# Delete the notification to avoid reading it again
delete_notification(notification_id)
print(f"Picture generated! Image ID: {data['id_image']}")
link = data["links"][0]
# The "l" field contains a static link to the final image
print(link["l"])
count += 1
if count == len(prompts):
break
time.sleep(1)
```
users can also disable watermark application on the final image.
# Edit background
URL: /docs/v1/edit-background
Applies to API version: v1
Description: Change a photo's background and lighting
You will need an API token to send HTTP requests. See [Authentication](/docs/v1/auth) for instructions.
## Quick start [#quick-start]
Firstly, you'll need to upload the image you want to process. These parameters must be supplied as **multipart form data**:
| Parameter | Example | Description |
| --------- | ----------- | ------------------------------ |
| `file` | `@file.png` | The image file as binary data. |
Please note that we only support the following formats: **WEBP**, **JPEG** and **PNG**.
Note down this field from the response:
```jsonc title="Response"
{
"image_id": "67762577...", // IMAGE_ID [!code highlight]
}
```
To understand how to use the `image_id` parameter, also see [Image ID](/docs/v1/concepts/image-id).
***
To generate environments (background and lighting), you will have to call the endpoint **once for each keyword/prompt** you want to use. Each one will generate a new picture.
This will start asynchronous processes that will be handled in the next step.
This is an example of request containing all required fields:
```jsonc title="Request"
{
"id_image": "67762577...", // IMAGE_ID
"keyword": "{\"indoor_spaces\":\"Office\"}", // KEYWORD
"prompt": ""
}
```
For more information on the usage of the `keyword` parameter, see [Keywords](#keywords).
***
Notification objects returned by this endpoint will inform you of the end of a generation on our servers.
We recommend sending a request every 3 seconds at most, to not incur in rate limiting.
```jsonc title="Request"
{
"name_list": ["edit_background"]
}
```
After the process is complete, you might see a similar response:
```jsonc title="Response"
{
"links": [{
"l": "https://..." // Download link [!code highlight]
// ...
}]
}
```
Where `"l"` points to a fully ready, modified version of the original image with the background and lighting changed to the keyword you specified in the previous step.
## Uploading an image [#uploading-an-image]
All target images are deleted automatically after 24 hours of being uploaded. This action cannot be undone.
You can upload any image containing any subject (or no subject at all).
Please note that we only support the following formats: **WEBP**, **JPEG** and **PNG**.
```python lineNumbers
import json
import requests
api_url = "https://api.piktid.com/api"
access_token = "your_access_token"
target_path = "path_to_image"
with open(target_path, "rb") as target:
response = requests.post(
api_url + "/edit/target",
headers={"Authorization": "Bearer " + access_token},
files={
"file": target
},
).json()
image_id = response.get("id_image")
```
## Generating a new background [#generating-a-new-background]
Our API can generate a single background with a single keyword at a time, but parallel processing is allowed and recommended.
To generate multiple backgrounds, you just have to send multiple requests.
```python lineNumbers
import json
import requests
api_url = "https://api.piktid.com/api"
access_token = "your_access_token"
image_id = "67762577..."
keywords = [
{"nature_landscapes": "Beach"},
{"outdoor_spaces": "Rooftop"}
]
options= {
"relight_strength": 1.0
}
for keyword in keywords:
data = requests.post(
api_url + "/edit/background",
headers={"Authorization": "Bearer " + access_token},
json={
"id_image": image_id,
"keyword": json.dumps(keyword),
"prompt": "",
"options": json.dumps(options)
},
).json()
```
### Generation options [#generation-options]
The `options` parameter in the request body must be formatted as a JSON-encoded string. It can contain the following parameters:
| Parameter | Values | Description |
| ------------------ | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `relight_strength` | Number from `0.0{:json}` to `1.0{:json}` | Controls the amount of light/shadow generation on the main subject. A higher number will replace the lighting more and more, while zero will turn off the feature entirely. |
| `seed` | Integer number | Seed used for random number generation. Two requests with the same seed will have the same result. If undefined, it will be randomized server-side. |
### Keywords [#keywords]
A keyword defined as a **single** key-value pair, where:
* the key is a generic group (for example "special\_occasions")
* the value is an expression of the key (for example "Christmas")
To better explain this concepts, here are the currently defined prompts as programming language type definitions:
```python
from dataclasses import dataclass
from typing import Literal
@dataclass
class Prompt:
indoor_spaces: Literal[
"Studio",
"Living Room",
"Kitchen",
"Bathroom",
"Bedroom",
"Office",
"Cafe",
"Gym",
"Library",
]
outdoor_spaces: Literal[
"Street",
"Rooftop",
"Pool",
"Courtyard",
"Balcony",
"Skyscraper",
]
weather_effects: Literal[
"Rain",
"Fog",
"Sunset",
"Sunrise",
"Fire",
"Water",
"Snow",
]
nature_landscapes: Literal[
"Beach",
"Forest",
"Mountains",
"Desert",
"Lake",
"Garden",
"Tropical",
"Sky",
]
seasonal: Literal[
"Spring",
"Summer",
"Autumn",
"Winter",
]
special_occasions: Literal[
"Christmas",
"Halloween",
"Easter",
"Wedding",
"Birthday",
]
architectural: Literal[
"Modern",
"Industrial",
"Classic",
"Futuristic",
]
professional_business: Literal[
"Corporate",
"Medical",
"Conference",
]
transportation: Literal[
"Car",
"Airplane",
"Train",
]
materials_textures: Literal[
"Silk",
"Wood",
"Marble",
"Stone",
"Metal",
"Leather",
"Paper",
"Gold",
]
artistic_stylized: Literal[
"Paint",
"Graffiti",
"Abstract",
"Vintage",
"Minimalist",
]
colors_gradients: Literal[
"Solid",
"Gradients",
"Ombre",
]
entertainment: Literal[
"Cinema",
"Concert",
"Night",
]
florals: Literal[
"Roses",
"Lavender",
"Flowers",
"Leaves",
]
```
```ts
type Prompt = Partial<{
indoor_spaces:
| "Studio"
| "Living Room"
| "Kitchen"
| "Bathroom"
| "Bedroom"
| "Office"
| "Cafe"
| "Gym"
| "Library"
outdoor_spaces:
| "Street"
| "Rooftop"
| "Pool"
| "Courtyard"
| "Balcony"
| "Skyscraper"
weather_effects:
| "Rain"
| "Fog"
| "Sunset"
| "Sunrise"
| "Fire"
| "Water"
| "Snow"
nature_landscapes:
| "Beach"
| "Forest"
| "Mountains"
| "Desert"
| "Lake"
| "Garden"
| "Tropical"
| "Sky"
seasonal:
| "Spring"
| "Summer"
| "Autumn"
| "Winter"
special_occasions:
| "Christmas"
| "Halloween"
| "Easter"
| "Wedding"
| "Birthday"
architectural:
| "Modern"
| "Industrial"
| "Classic"
| "Futuristic"
professional_business:
| "Corporate"
| "Medical"
| "Conference"
transportation:
| "Car"
| "Airplane"
| "Train"
materials_textures:
| "Silk"
| "Wood"
| "Marble"
| "Stone"
| "Metal"
| "Leather"
| "Paper"
| "Gold"
artistic_stylized:
| "Paint"
| "Graffiti"
| "Abstract"
| "Vintage"
| "Minimalist"
colors_gradients:
| "Solid"
| "Gradients"
| "Ombre"
entertainment:
| "Cinema"
| "Concert"
| "Night"
florals:
| "Roses"
| "Lavender"
| "Flowers"
| "Leaves"
}>
```
In both languages, the following are valid keyword objects:
```json
{
"architectural": "Modern",
}
```
```json
{
"weather_effects": "Rain",
}
```
## Waiting for results [#waiting-for-results]
Notifications are automatically deleted after exactly **10 minutes** (600 seconds) from their creation.
Results are shared to clients via "notifications". Clients are expected to poll for notifications and handle them in the allowed time limit before they are automatically dismissed by the API.
```python lineNumbers
import time
import requests
from pprint import pprint
api_url = "https://api.piktid.com/api"
access_token = "your_access_token"
image_id = "67762577..."
def process_notifications(response, image_id):
for notification in response.get("notifications_list", []):
if notification["name"] != "edit_background:
return None, None
if notification["data"]["address"] != image_id:
return None, None
return notification["data"], notification["id"]
return None, None
def delete_notification(notification_id):
requests.delete(
api_url + "/notification/delete_json",
headers={"Authorization": "Bearer " + access_token},
json={"id": notification_id},
)
count = 0
# Loops for 10 minutes, even though generation generally
# takes less time.
for _ in range(600):
response = requests.post(
api_url + "/notification_by_name_json",
headers={"Authorization": "Bearer " + access_token},
json={"name_list": "edit_background,error"},
).json()
data, notification_id = process_notifications(response, image_id)
if data is not None:
# Delete the notification to avoid reading it again
delete_notification(notification_id)
print(
f"Background generated! Keyword: {data['keyword']}"
)
link = data["links"][0]
# The "l" field contains a static link to the final image
print(link["l"])
count += 1
if count == len(keywords):
break
time.sleep(1)
```
users can also disable watermark application on the final image.
# Magic expand
URL: /docs/v1/expand
Applies to API version: v1
Description: Augment images by generating outside borders
You will need an API token to send HTTP requests. See [Authentication](/docs/v1/auth) for instructions.
## Quick start [#quick-start]
Firstly, you'll need to upload the image you want to process. These parameters must be supplied as **multipart form data**:
| Parameter | Example | Description |
| --------- | ----------- | ------------------------------ |
| `file` | `@file.png` | The image file as binary data. |
Please note that we only support the following formats: **WEBP**, **JPEG** and **PNG**.
Note down these fields from the response:
```jsonc title="Response"
{
"image_id": "67762577...", // IMAGE_ID [!code highlight]
}
```
To understand how to use the `image_id` parameter, see [Image ID](/docs/v1/concepts/image-id).
***
This is an example of request containing all required fields to expand a picture:
```jsonc title="Request"
{
"id_image": "67762577...", // IMAGE_ID
"top": 100,
"bottom": 0,
"left": -25,
"right": 10,
}
```
For more information about what the side numbers represent, see [Side offsets](#side-offsets)
***
Notification objects returned by this endpoint will inform you of the end of a generation on our servers.
We recommend sending a request every 3 seconds at most, to not incur in rate limiting.
```jsonc title="Request"
{
"name_list": ["expand_generate"]
}
```
After the process is complete, you might see a similar response:
```jsonc title="Response"
{
"notifications_list": [
{
"data": {
"id_image": "67762577...", // IMAGE_ID
"links": [{
"l": "https://..." // Download link [!code highlight]
// ...
}],
},
"name": "expand_generate",
},
]
}
```
Where `"l"` points to a fully ready, modified version of the newly expanded/cropped image as you requested in the previous step.
## Uploading an image [#uploading-an-image]
All target images are deleted automatically after 24 hours of being uploaded. This action cannot be undone.
You can upload any image that's at least 64 by 64 pixels and at most 4096 by 4096 pixels.
Please note that we only support the following formats: **WEBP**, **JPEG** and **PNG**.
```python lineNumbers
import json
import requests
api_url = "https://api.piktid.com/api"
access_token = "your_access_token"
target_path = "path_to_image"
with open(target_path, "rb") as target:
response = requests.post(
api_url + "/expand/target",
headers={"Authorization": "Bearer " + access_token},
files={
"file": target
},
).json()
image_id = response.get("id_image")
```
## Expanding an image [#expanding-an-image]
```python lineNumbers
import json
import requests
api_url = "https://api.piktid.com/api"
access_token = "your_access_token"
image_id = "67762577..."
offsets = {
"top": 100,
"bottom": 0,
"left": -25,
"right": 10,
}
requests.post(
api_url + "/edit/generate",
headers={"Authorization": "Bearer " + access_token},
json={
"id_image": image_id,
} | offsets,
)
```
### Side offsets [#side-offsets]
To control image expansion and cropping, side offsets must be provided in the request.
Each side will be:
* **expanded** by the given offset if the offset is a **positive** number
* **cropped** by the given offset if the offset is a **negative** number
As an example, the following offsets will be applied to an image that's **900** pixels wide and **600** pixels tall:
```python
offsets = {
"top": 100,
"bottom": 0,
"left": -25,
"right": 10,
}
```
The resulting image will be:
* `900` (original) + `100` (top) + `0` (bottom) = `1000` pixels tall
* `600` (original) - `25` (left) + `10` (right) = `585` pixels wide
## Waiting for results [#waiting-for-results]
Notifications are automatically deleted after exactly **10 minutes** (600 seconds) from their creation.
Results are shared to clients via "notifications". Clients are expected to poll for notifications and handle them in the allowed time limit before they are automatically dismissed by the API.
```python lineNumbers
import time
import requests
from pprint import pprint
api_url = "https://api.piktid.com/api"
access_token = "your_access_token"
image_id = "67762577..."
def process_notifications(response, image_id):
for notification in response.get("notifications_list", []):
if notification["name"] != "expand_generate":
return None, None
if notification["data"]["address"] != image_id:
return None, None
return notification["data"], notification["id"]
return None, None
def delete_notification(notification_id):
requests.delete(
api_url + "/notification/delete_json",
headers={"Authorization": "Bearer " + access_token},
json={"id": notification_id},
)
# Loops for 10 minutes, even though generating people generally
# takes less time.
for _ in range(600):
response = requests.post(
api_url + "/notification_by_name_json",
headers={"Authorization": "Bearer " + access_token},
json={
"name_list": "expand_generate,error",
"id_image": image_id,
# Task IDs (returned by /generate) can optionally be used to filter notifications
# "id_task": ["..."],
},
).json()
data, notification_id = process_notifications(response, image_id)
if data is not None:
# Delete the notification to avoid reading it again
delete_notification(notification_id)
print(f"Generation finished!")
link = data["links"][0]
# The "l" field contains a static link to the final image
print(link["l"])
break
time.sleep(1)
```
users can also disable watermark application on the final image.
# Generate person
URL: /docs/v1/generate-person
Applies to API version: v1
Description: Transform people with our advanced AI model
You will need an API token to send HTTP requests. See [Authentication](/docs/v1/auth) for instructions.
## Quick start [#quick-start]
Firstly, you'll need to upload the image you want to process. These parameters must be supplied as **multipart form data**:
| Parameter | Example | Description |
| --------- | ----------- | ------------------------------ |
| `file` | `@file.png` | The image file as binary data. |
Please note that we only support the following formats: **WEBP**, **JPEG** and **PNG**.
Note down these fields from the response:
```jsonc title="Response"
{
"image_id": "67762577...", // IMAGE_ID [!code highlight]
"num_persons": 2,
"coordinates_list": [
{
"id": 0 // PERSON_ID [!code highlight]
"approve": true, // Usable if true otherwise too blurry
// ...
},
{
"id": 1 // PERSON_ID [!code highlight]
"approve": true, // Usable if true otherwise too blurry
// ...
}
]
}
```
For more information about the data contained in `coordinates_list` and how to process it, see [Coordinates](/docs/v1/concepts/coordinates).
To understand how to use the `image_id` parameter, also see [Image ID](/docs/v1/concepts/image-id).
***
To generate a person, you will have to call the endpoint:
* **once for each person** you want to replace in the original image
* **once for each keyword** you want to use for a person
This will start asynchronous processes that will be handled in the next step.
This is an example of request containing all required fields for the person with `"id": 0` (zero):
```jsonc title="Request"
{
"id_image": "67762577...", // IMAGE_ID
"id_person": "0", // PERSON_ID as string
"keyword": "{\"Location\":\"Oceania\"}"
}
```
For more information on the usage of the `keyword` parameter, see [Keywords](#keywords).
***
Notification objects returned by this endpoint will inform you of the end of a generation on our servers.
We recommend sending a request every 3 seconds at most, to not incur in rate limiting.
```jsonc title="Request"
{
"name_list": ["edit_generate"]
}
```
After the process is complete, you might see a similar response:
```jsonc title="Response"
{
"notifications_list": [
{
"data": {
"id_person": 0, // PERSON_ID [!code highlight]
"address": "67762577...", // IMAGE_ID
"links": [{
"l": "https://..." // Download link [!code highlight]
// ...
}],
},
"name": "edit_generate",
},
]
}
```
Where `"l"` points to a fully ready, modified version of the original image with the specified person replaced as you requested in the previous step.
## Uploading an image [#uploading-an-image]
All target images are deleted automatically after 24 hours of being uploaded. This action cannot be undone.
You can upload any image containing **up to 30 people**. Their faces will be automatically detected by the API.
Please note that we only support the following formats: **WEBP**, **JPEG** and **PNG**.
```python lineNumbers
import json
import requests
api_url = "https://api.piktid.com/api"
access_token = "your_access_token"
target_path = "path_to_image"
with open(target_path, "rb") as target:
response = requests.post(
api_url + "/edit/target",
headers={"Authorization": "Bearer " + access_token},
files={
"file": target
},
).json()
image_id = response.get("id_image")
```
## Generating a person [#generating-a-person]
Our API can generate a single person with a single keyword at a time, but parallel processing is allowed and recommended.
To generate multiple people at once, or a single person with multiple keywords, you just have to send multiple requests.
```python lineNumbers
import json
import requests
api_url = "https://api.piktid.com/api"
access_token = "your_access_token"
image_id = "67762577..."
person_id = 0
keywords = [
{"Location": "East Asia"},
{"Location": "South America"}
]
keyword = keywords[0]
requests.post(
api_url + "/edit/generate",
headers={"Authorization": "Bearer " + access_token},
json={
"id_image": image_id,
"id_person": person_id,
"keyword": json.dumps(keyword),
"category": "person"
},
)
# A 15 second timeout is encouraged due to internal API limitations.
# See below for explanation.
time.sleep(15)
for keyword in keywords[1:]:
requests.post(
api_url + "/edit/generate",
headers={"Authorization": "Bearer " + access_token},
json={
"id_image": image_id,
"id_person": person_id,
"keyword": json.dumps(keyword),
"category": "person"
},
)
```
The first call to `/edit/generate` creates internal files and data (for example masks for a single person).
This allows all subsequent requests to get a head start because of the already existing data.
If all requests are made in parallel, each one has to generate the initial data, thus taking a lot longer than sending them in a staggered way.
### Keywords [#keywords]
A keyword defined as a **single** key-value pair, where:
* the key is a generic group (for example "Location")
* the value is an expression or physical state of the key (for example "South America")
To better explain this concepts, here are the currently defined prompts as programming language type definitions:
```python
from dataclasses import dataclass
from typing import Literal
@dataclass
class Prompt:
Location: Literal[
"Africa",
"East Asia",
"Middle East",
"North America",
"North Europe",
"Oceania",
"South America",
"South Europe",
]
```
```ts
type Prompt = Partial<{
Location:
| "Africa"
| "East Asia"
| "Middle East"
| "North America"
| "North Europe"
| "Oceania"
| "South America"
| "South Europe"
}>
```
In both languages, the following are valid prompt objects:
```json
{
"Location": "North America",
}
```
```json
{
"Location": "Middle East",
}
```
## Waiting for results [#waiting-for-results]
Notifications are automatically deleted after exactly **10 minutes** (600 seconds) from their creation.
Results are shared to clients via "notifications". Clients are expected to poll for notifications and handle them in the allowed time limit before they are automatically dismissed by the API.
```python lineNumbers
import time
import requests
from pprint import pprint
api_url = "https://api.piktid.com/api"
access_token = "your_access_token"
image_id = "67762577..."
def process_notifications(response, image_id):
for notification in response.get("notifications_list", []):
if notification["name"] != "edit_generate":
return None, None
if notification["data"]["address"] != image_id:
return None, None
return notification["data"], notification["id"]
return None, None
def delete_notification(notification_id):
requests.delete(
api_url + "/notification/delete_json",
headers={"Authorization": "Bearer " + access_token},
json={"id": notification_id},
)
count = 0
# Loops for 10 minutes, even though generating people generally
# takes less time.
for _ in range(600):
response = requests.post(
api_url + "/notification_by_name_json",
headers={"Authorization": "Bearer " + access_token},
json={"name_list": "edit_generate,error"},
).json()
data, notification_id = process_notifications(response, image_id)
if data is not None:
# Delete the notification to avoid reading it again
delete_notification(notification_id)
print(
f"Person with ID {data['id_person']} generated! Keyword: {data['keyword']}"
)
link = data["links"][0]
# The "l" field contains a static link to the final image
print(link["l"])
count += 1
if count == len(keywords):
break
time.sleep(1)
```
users can also disable watermark application on the final image.
# Introduction
URL: /docs/v1
Applies to API version: v1
Description: General information about the APIs
Welcome to the documentation for the PiktID API!
## Getting started [#getting-started]
To use the APIs, you will need to sign up first. After confirming your email address, you can proceed to the authentication overview, where you'll learn how to authenticate your HTTP requests with our APIs.
Create a new PiktID account to access apps and APIs
## Using the APIs [#using-the-apis]
PiktID provides the following APIs for image editing and annotation:
# Swap
URL: /docs/v1/swap
Applies to API version: v1
Description: Remove face identities and replace them with user-supplied ones
You will need an API token to send HTTP requests. See [Authentication](/docs/v1/auth) for instructions.
## Quick start [#quick-start]
Firstly, you'll need to upload a target image. This is the "background" image where your faces/identities will be placed upon.
Please note that we only support the following formats: **WEBP**, **JPEG** and **PNG**.
You can also supply a URL pointing to an image on the web, without uploading the image yourself.
See [Uploading a target image](#uploading-a-target-image) for more information.
Note down these fields from the response:
```jsonc title="Response"
{
"image_id": "67762577...", // IMAGE_ID [!code highlight]
"faces": {
"approved_faces": [1, 1], // [!code highlight]
"coordinates_list": [
{
"id": 0 // FACE_ID [!code highlight]
// ...
},
{
"id": 1 // FACE_ID [!code highlight]
// ...
}
]
}
}
```
For more information about the data contained in `coordinates_list` and how to process it, see [Coordinates](/docs/v1/concepts/coordinates).
To understand how to use the `image_id` parameter, see [Image ID](/docs/v1/concepts/image-id).
You can also specify some advanced settings for extra control (such as hair replacement). For that, see [Upload options](#upload-options).
***
You may then need to upload one or more to be swapped onto the target image. Keep in mind that you can only upload one face at a time, so it's fine to call the endpoint several times.
Note the following field in the response:
```jsonc title="Response"
{
"identity_name": "1W38bSrE..." // IDENTITY_ID [!code highlight]
}
```
You can also reuse identities that are already stored in your profile. More on this in [the detailed section](#using-identities) later.
***
To create a swapped image, you will have to call the endpoint **once for each face** you want to swap.
This will start asynchronous processes that will be handled the next step.
This is an example of request containing all required fields for face with `"id": 0` (zero):
```jsonc title="Request"
{
"identity_name": "1W38bSrE...", // IDENTITY_ID
"id_image": "67762577...", // IMAGE_ID
"id_face": "0", // FACE_ID as string
"options": "{\"flag_replace_and_download\":true}"
}
```
Note that the `flag_replace_and_download` bit **is required**. This flag allows to skip the [pick process](/docs/v1/anonymize#assembling-the-final-image) and directly download the final image.
You can also specify some advanced settings for extra control (such as similarity and seed). For that, see [Generation options](#generation-options).
***
Notification objects returned by this endpoint will inform you of the end of a swap process on our servers.
We recommend sending a request every 3 seconds at most, to not incur in rate limiting.
```jsonc title="Request"
{
"name_list": ["download"]
}
```
After the process is complete, you might see a similar response:
```jsonc title="Response"
{
"notifications_list": [
{
"data": {
"f": 0, // FACE_ID
"id_image": "67762577...", // IMAGE_ID
"link": "https://...", // [!code highlight]
"link_hd": "https://...", // [!code highlight]
},
"name": "download",
},
]
}
```
Where `link` and `link_hd` point to the final image with face `data.f` swapped, as well as all previously swapped faces.
Faces **are not** swapped in order, so you should always check `data.f` and count that face as done while keeping track of other faces.
## Uploading a target image [#uploading-a-target-image]
All target images are deleted automatically after 24 hours of being uploaded. This action cannot be undone.
You can upload any image containing **up to 30 people**. Their faces will be automatically detected by the API.
Please note that we only support the following formats: **WEBP**, **JPEG** and **PNG**.
```python lineNumbers
import requests
api_url = "https://api.piktid.com/api"
access_token = "your_access_token"
target_path = "path_to_image"
options = {
# Check the "Options" section for more information
"flag_hair": True,
}
with open(target_path, "rb") as target:
response = requests.post(
api_url + "/consistent_identities/upload_target",
headers={"Authorization": "Bearer " + access_token},
data={"options": json.dumps(options)},
files={
"file": target
},
).json()
image_id = response.get("image_id")
print("Created image ID: ", image_id)
```
Alternatively, you can also supply a full static URL pointing to an image and our API will fetch it for you and process it in the same way as an uploaded file.png
```python lineNumbers
import requests
api_url = "https://api.piktid.com/api"
access_token = "your_access_token"
target_url = "http://example.com/image.jpg"
options = {
# Check the "Options" section for more information
"flag_hair": True,
}
response = requests.post(
api_url + "/consistent_identities/upload_target",
headers={"Authorization": "Bearer " + access_token},
data={
"url": target_url,
"options": json.dumps(options),
},
).json()
image_id = response.get("image_id")
print("Created image ID: ", image_id)
```
Remember to check the `approved_faces` bitmask! If a face with `FACE_ID` equals to a number N is valid, its corresponding value in `approved_faces[N]` will be equals to `1`.
A face is considered invalid if it is detected but it also is too distorted or too low resolution to ensure a quality swap result.
In this example, the API marked the faces with ID `1` and `3` as invalid. The `coordinates_list` will only contain data for faces with ID `0` and `2`.
```jsonc title="Response"
{
"image_id": "67762577...", // IMAGE_ID
"faces": {
"approved_faces": [1, 0, 1, 0], // [!code highlight]
"coordinates_list": [ /* ... */ ],
// ...
},
// ...
}
```
For more information about the data contained in `coordinates_list` and how to process it, see [Coordinates](/docs/v1/concepts/coordinates).
### Upload options [#upload-options]
The `options` parameter in the request body must be formatted as a JSON-encoded string. It can contain the following parameters:
| Parameter | Values | Description |
| ----------- | ------------------------------- | ------------------------------------------------------------------- |
| `flag_hair` | `true{:json}` or `false{:json}` | If `true{:json}`, the model will also draw over the subject's hair. |
## Using identities [#using-identities]
To swap a face, you don't necessarily need to upload a new identity. Existing identities can be listed using the gallery endpoint.
```jsonc title="Response"
{
"data": [
{
"f": "1W38bSrE...", // IDENTITY_ID [!code highlight]
"l": "https://...", // thumbnail
"t": "2026-01-20T17:56:41.037429",
// ...
},
],
}
```
You can still upload a fresh face with the appropriate endpoint:
```python lineNumbers
import requests
api_url = "https://api.piktid.com/api"
access_token = "your_access_token"
face_path = "path_to_picture"
with open(face_path, "rb") as face:
response = requests.post(
api_url + "/consistent_identities/upload_face",
headers={"Authorization": "Bearer " + access_token},
files={
"file": face,
},
).json()
face_id = response.get("identity_name")
print(face_id)
```
## Starting a swap [#starting-a-swap]
Swap processes are always identified by the `IMAGE_ID`. That's why you're able to (and you must) send a request for each face you want swapped.
```python lineNumbers
import json
import requests
api_url = "https://api.piktid.com/api"
access_token = "your_access_token"
image_id = "67762577..."
options = {
"flag_replace_and_download": True,
# You can include your custom watermark here (PRO only)
# "watermark_url": "https://...",
}
swaps = {
0: "1W38bSrE...",
2: "4We79yLh...",
}
for face_id, identity_id in swaps.items():
response = requests.post(
api_url + "/consistent_identities/generate",
headers={"Authorization": "Bearer " + access_token},
json={
"identity_name": identity_id, # IDENTITY_ID
"id_image": image_id, # IMAGE_ID
"id_face": face_id, # FACE_ID
"options": json.dumps(options),
},
).json()
```
In this example the `swaps: dict[int, str]{:python}` dictionary contains pairs associating detected `FACE_ID`s (from the target upload response) to existing `IDENTITY_ID`s.
Retrieval of existing identities is described in detail in [Using identities](#using-identities).
### Generation options [#generation-options]
The `options` parameter in the request body must be formatted as a JSON-encoded string. It can contain the following parameters:
| Parameter | Values | Description |
| ----------------- | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `prompt_strength` | Number from `0.0{:json}` to `1.0{:json}` | A lower number will make the output image more similar to the source/original image. A higher number will make the output more similar to the target identity. In the web app, it is named "Similarity". |
| `transfer_hair` | Boolean | If set True, the source hairstyle will be transferred to the target. It works with a strength greater or equal than 0.5. |
| `seed` | Integer number | Seed used for random number generation. Two requests with the same seed will have the same result. If undefined, it will be randomized server-side. |
## Fetching results [#fetching-results]
Notifications are automatically deleted after exactly **10 minutes** (600 seconds) from their creation.
Results are shared to clients via "notifications". Clients are expected to poll for notifications and handle them in the allowed time limit before they are automatically dismissed by the API.
```python lineNumbers
import time
import requests
api_url = "https://api.piktid.com/api"
access_token = "your_access_token"
image_id = "67762577..."
def process_notifications(response, image_id):
for notification in response.get("notifications_list", []):
if notification["name"] != "download":
return None, None
if notification["data"]["id_image"] != image_id:
return None, None
return notification["data"], notification["id"]
return None, None
def delete_notification(notification_id):
requests.delete(
api_url + "/notification/delete_json",
headers={"Authorization": "Bearer " + access_token},
json={"id": notification_id},
)
# Loops for 300 seconds, even though swapping faces generally
# takes much less time.
for _ in range(300):
response = requests.post(
api_url + "/consistent_identities/notification/read",
headers={"Authorization": "Bearer " + access_token},
json={"name_list": "download,error"},
).json()
data, notification_id = process_notifications(response, image_id)
if data is not None:
# Delete the notification to avoid reading it again
delete_notification(notification_id)
print(f"Face with ID {data['f']} finished processing!")
print(data["l"])
break
time.sleep(1)
```
Remember that you can always get all the images you have generated so far with the "gallery" endpoint:
All generated images are deleted automatically after 24 hours of being created. This action cannot be undone. Make sure to download them!
```python lineNumbers
import json
import requests
from pprint import pprint
api_url = "https://api.piktid.com/api"
access_token = "your_access_token"
response = requests.post(
api_url + "/consistent_identities/gallery",
headers={"Authorization": "Bearer " + access_token},
).json()
images = response["data"]
pprint(images)
```
# Upscale
URL: /docs/v1/upscale
Applies to API version: v1
Description: Increase image resolution without compromising facial features
You will need an API token to send HTTP requests. See [Authentication](/docs/v1/auth) for instructions.
## Quick start [#quick-start]
Firstly, you'll need to upload the image you want to process. These parameters must be supplied as **multipart form data**:
| Parameter | Example | Description |
| --------- | ----------- | ---------------------------- |
| `file` | `@file.png` | The image file as form data. |
Please note that we only support the following formats: **WEBP**, **JPEG** and **PNG**.
Note down these fields from the response:
```jsonc title="Response"
{
"id_image": "67762577...", // IMAGE_ID [!code highlight]
"id_project": "67762577...", // PROJECT_ID [!code highlight]
}
```
***
To upscale an image, you just have to call the provided endpoint.
This will start asynchronous processes that will be handled in the next step.
This is an example of request containing all required fields to upscale the uploaded image to 4 times its original size:
```jsonc title="Request"
{
"id_image": "67762577...", // IMAGE_ID
"id_project": "67762577...", // PROJECT_ID
"scale_factor": "4",
"output_format": "PNG",
"mode": "s_upscaler",
"face_enhancer": true
}
```
```jsonc title="Response"
{
"eta": 60.00,
"height": 512,
"width": 512,
"height_o": 2048.0,
"width_o": 2048.0,
"required_credits": 0.25
}
```
For more examples and options for the upscaling process, see [Upscaling an image](#upscaling-an-image).
You can optionally make the API calculate time and credit estimates without starting a process. For that, see [Estimating time and credits](#estimating-time-and-credits).
***
Notification objects returned by this endpoint will inform you of the end of a process on our servers.
We recommend sending a request every 3 seconds at most, to not incur in rate limiting.
```jsonc title="Request"
{
"name_list": ["superid"]
}
```
After the process is complete, you might see a similar response:
```jsonc title="Response"
{
"notifications_list": [
{
"data": {
"id_image": "67762577...", // IMAGE_ID
"id_project": "67762577...", // PROJECT_ID
"link": {
"l": "https://...", // [!code highlight]
},
},
"name": "superid",
},
]
}
```
Where `l` points to the upscaling result image.
We recommend checking `id_image` and `id_project` when handling notifications, to ensure the correct link is used in case of parallel running processes.
## Uploading a target image [#uploading-a-target-image]
All target images are deleted automatically after 24 hours of being uploaded. This action cannot be undone.
You can upload any image that satisfies all following constraints:
* it must be encoded in one the following formats: **WEBP**, **JPEG** or **PNG**.
* it must be **larger** than 64 by 64 pixels
* the output image must be **smaller** than 8192 by 8192 pixels
```python lineNumbers
import requests
from pprint import pprint
api_url = "https://api.piktid.com/api"
access_token = "your_access_token"
target_path = "path_to_image"
with open(target_path, "rb") as target:
response = requests.post(
api_url + "/superid/upload",
headers={"Authorization": "Bearer " + access_token},
files={
"file": target,
},
).json()
pprint(response)
```
## Upscaling an image [#upscaling-an-image]
There are currently two operating modes for SuperID:
* [**Super** mode](/docs/v1/superid/v2/post): optimized for maximum quality and detail.
* [**Fast** mode](/docs/v1/superid_fast/post): designed for quick processing with slightly reduced precision.
To start an upscaling process, all that's needed is a call to the desired endpoint:
```python lineNumbers
import requests
from pprint import pprint
api_url = "https://api.piktid.com/api"
access_token = "your_access_token"
image_id = "67762577...", # IMAGE_ID
project_id = "67762577...", # PROJECT_ID
# Default params for upscaler (s_upscaler) mode, can be omitted from request
params = {
"prompt": "",
"creativity": 7,
"fractality": 3,
"fidelity": 5,
"denoise": 1,
}
response = requests.post(
api_url + "/superid/v2",
headers={"Authorization": "Bearer " + access_token},
json={
"superid_type": "s_upscaler",
"id_project": project_id,
"id_image": image_id,
"scale_factor": "4",
"output_format": "PNG",
"face_enhancer": True,
} | params,
).json()
pprint(response)
```
The response will contain an estimation of the time and credits needed for the process that has just been started.
```ts twoslash title="Response"
interface Response {
/** Estimated processing time in seconds */
eta: number
/** Original height of the input image in pixels */
height: number
/** Original width of the input image in pixels */
width: number
/** Height of the upscaled image in pixels */
height_o: number
/** Width of the upscaled image in pixels */
width_o: number
/** Credits that will be consumed by the process, can be fractions of a credit */
required_credits: number
}
const obj: Response =
// ---cut-before---
{
eta: 60.00,
height: 512,
width: 512,
height_o: 2048.0,
width_o: 2048.0,
required_credits: 0.25
}
```
### Enhancing and upscaling [#enhancing-and-upscaling]
Using the upscaler in "enhance" mode will let our model be more creative with the input image, sacrificing fidelity with more richness in details.
```python lineNumbers
import requests
from pprint import pprint
api_url = "https://api.piktid.com/api"
access_token = "your_access_token"
image_id = "67762577...", # IMAGE_ID
project_id = "67762577...", # PROJECT_ID
# Default params for enhancer (s_enhancer) mode, can be omitted from request
params = {
"prompt": "",
"creativity": 9,
"fractality": 5,
"fidelity": 3,
"denoise": 1,
}
response = requests.post(
api_url + "/superid/v2",
headers={"Authorization": "Bearer " + access_token},
json={
"superid_type": "s_enhancer",
"id_project": project_id,
"id_image": image_id,
"scale_factor": "4",
"output_format": "PNG",
"face_enhancer": False,
} | params,
).json()
pprint(response)
```
The response will contain an estimation of the time and credits needed for the process that has just been started.
```ts twoslash title="Response"
interface Response {
/** Estimated processing time in seconds */
eta: number
/** Original height of the input image in pixels */
height: number
/** Original width of the input image in pixels */
width: number
/** Height of the upscaled image in pixels */
height_o: number
/** Width of the upscaled image in pixels */
width_o: number
/** Credits that will be consumed by the process, can be fractions of a credit */
required_credits: number
}
const obj: Response =
// ---cut-before---
{
eta: 60.00,
height: 512,
width: 512,
height_o: 2048.0,
width_o: 2048.0,
required_credits: 0.25
}
```
## Waiting for results [#waiting-for-results]
Notifications are automatically deleted after exactly **10 minutes** (600 seconds) from their creation.
Results are shared to clients via "notifications". Clients are expected to poll for notifications and handle them in the allowed time limit before they are automatically dismissed by the API.
```python lineNumbers
import time
import requests
from pprint import pprint
api_url = "https://api.piktid.com/api"
access_token = "your_access_token"
project_id = "67762577..."
def process_notifications(response, project_id):
for notification in response.get("notifications_list", []):
if notification["name"] != "superid":
return None, None
if notification["data"]["id_project"] != project_id:
return None, None
return notification["data"], notification["id"]
return None, None
def delete_notification(notification_id):
requests.delete(
api_url + "/notification/delete_json",
headers={"Authorization": "Bearer " + access_token},
json={"id": notification_id},
)
# Loops for 20 minutes, even though upscaling images generally
# takes less time.
for _ in range(1200):
response = requests.post(
api_url + "/notification_by_name_json",
headers={"Authorization": "Bearer " + access_token},
json={"name_list": "superid,error"},
).json()
data, notification_id = process_notifications(response, image_id)
if data is not None:
# Delete the notification to avoid reading it again
delete_notification(notification_id)
print(f"Project with ID {data['id_project']} finished upscaling!")
pprint(data["link"])
break
time.sleep(1)
```
## Estimating time and credits [#estimating-time-and-credits]
You can always ask our API to estimate both credits and time required in advance, without having to start an upscaling process.
```python lineNumbers
import requests
from pprint import pp as print
api_url = "https://api.piktid.com/api"
access_token = "your_access_token"
id_image = "67762577...", # IMAGE_ID
id_project = "67762577...", # PROJECT_ID
response = requests.post(
api_url + "/superid_get_info",
headers={"Authorization": "Bearer " + access_token},
json={
"id_project": id_project,
"id_image": id_image,
"scale_factor": "4",
"output_format": "PNG",
"options": "{\"face_enhancer\": true}"
},
).json()
print(response)
```
The response will contain an estimation of the time and credits needed for the process that has just been started.
```ts twoslash title="Response"
interface Response {
/** Estimated processing time in seconds */
eta: number
/** Original height of the input image in pixels */
height: number
/** Original width of the input image in pixels */
width: number
/** Height of the upscaled image in pixels */
height_o: number
/** Width of the upscaled image in pixels */
width_o: number
/** Credits that will be consumed by the process, can be fractions of a credit */
required_credits: number
}
const obj: Response =
// ---cut-before---
{
eta: 60.00,
height: 512,
width: 512,
height_o: 2048.0,
width_o: 2048.0,
required_credits: 0.25
}
```
## Resuming a past project [#resuming-a-past-project]
You can retrieve the original image used in a past project whenever you wish, by using the provided endpoint:
```python lineNumbers
import requests
from pprint import pprint
api_url = "https://api.piktid.com/api"
access_token = "your_access_token"
id_project = "67762577...", # PROJECT_ID
response = requests.post(
api_url + "/retrieve_superid_project",
headers={"Authorization": "Bearer " + access_token},
json={
"id_project": id_project
},
).json()
pprint(response)
```
# Authentication
URL: /docs/v2/auth
Applies to API version: v2
Description: Authenticate your requests to the APIs
The authentication flow for this API is based on simple *access* tokens, passed in the `Authorization` header, using the `Bearer` scheme.
API tokens must be manually generated from the profile dashboard. Their expiration can be modified up to 4 years from the time they are issued.
It is not possible to refresh an API token. When it expires, you will need to generate a new one.
## Quick start [#quick-start]
Create a new account to access apps and APIs
You will first need to manually sign up to use our services.
If you need **pilot** or **enterprise** access to the `v2` APIs, [reach out to us](https://on-model.com/contact).
We will provision an account for you or your enterprise.
***
Manage your API tokens from the profile dashboard
API tokens can be generated at any time from [the profile dashboard](https://app.on-model.com/profile?tab=tokens).
# Create an identity
URL: /docs/v2/create-identity
Applies to API version: v2
Description: Generate a new identity from a brief or a reference image and promote it into your library
You will need an API token to send HTTP requests. See [Authentication](/docs/v2/auth) for instructions.
This guide walks through generating a new identity end-to-end: describe the model you want, dispatch a creation job, pick the draft you like, and promote it into a permanent identity in your library.
## Quick start [#quick-start]
Submit a creation job with one or more structured instructions (or a free-form prompt). Each instruction can fan out into multiple variations. The endpoint returns a `job_id` you will use to track draft events. See [Submitting a creation job](#submitting-a-creation-job) for details.
***
Track notifications using SSE or webhooks with the `job_id`. When the job completes, fetch the draft `image_result_id` values from the job results endpoint. See [Tracking drafts](#tracking-drafts) for details.
***
Promote the draft you want into a full identity. The API returns the new `identity_code` and starts preprocessing. See [Promoting a draft](#promoting-a-draft) for details.
***
Track `identity_preprocessing` notifications with SSE or webhooks until preprocessing is complete. The new identity is then ready to use in Model Swap, Flat-to-Model, or any other job. See [Tracking preprocessing](#tracking-preprocessing) for details.
## Building a brief [#building-a-brief]
A creation job takes a list of `instructions`. Each instruction describes one conceptual face and produces 1–8 draft variations. Instructions can use structured field groups, a free-form `prompt`, or both. When `prompt` is set, it overrides the auto-built prompt and the structured fields are still passed through for downstream metadata.
The user-facing groups are:
| Group | Sub-fields |
| ------------ | ---------------------------------------------------------------------------------------------- |
| `appearance` | `gender`, `age`, `ethnicity`, `skin`, `build`, `bust`, `chest`, `size`, `height`, `expression` |
| `face` | `eyes`, `eyebrows`, `nose`, `lips`, `smile`, `face_shape`, `facial_hair`, `makeup`, `marks` |
| `hair` | `color`, `length`, `style` |
Every field is optional. Leave one blank and the generator picks something sensible. Provide either at least one structured field or a non-empty `prompt`.
### Chest and bust [#chest-and-bust]
`build` describes overall physique ("slim", "athletic", "curvy"); `bust` and `chest` describe the chest specifically, which is what drives fit for swimwear, lingerie and knitwear. Pass a short noun phrase:
* `bust` — `"flat chest"`, `"small bust"`, `"medium bust"`, `"full bust"`, `"very full bust"`
* `chest` — `"narrow chest"`, `"average chest"`, `"broad chest"`, `"barrel chest"`, `"defined pecs"`
The two are gender-scoped in the wizard (`bust` for female, `chest` for male, both for non-binary), but the API accepts either for any adult identity — send whichever describes your subject.
Both fields are **adults only**. They are dropped from the generation prompt for any instruction that reads as a minor: `appearance.age` below 18, or an `appearance.gender` of `girl`, `boy` or `teen`. The request still succeeds; the field is simply ignored. The same rule applies to preset extraction, which never returns them for a subject that appears under 18.
Camera angle, framing, lighting, outfit, and background are standardized by the platform on every Create Identity job. The output is always a clean front-facing portrait, so every identity in your library shares the same visual treatment. The API accepts `outfit`, `style`, `scene`, and `camera` groups for forward compatibility, but values in these groups are ignored.
### Starting from a reference image [#starting-from-a-reference-image]
To build a brief from an existing photo, mirror the **From Image** flow in the wizard: upload the reference via [`POST /upload`](/docs/v2/model-swap#uploading-images), then call [`POST /preset/extract-from-image`](/docs/v2/presets/extract_preset_from_image__post) with `type: "identity_creation"` to get back an `instruction_data` object. Wrap that in `{"instructions": [], "options": {...}}` and submit to `/identity/create` as usual. Preset extraction charges credits separately from the creation job.
### Advanced: input\_assets in expert prompts [#advanced-input_assets-in-expert-prompts]
When using a free-form `prompt`, you can attach up to **3** reference images via `input_assets` so the generator can riff on a visual cue alongside the text. References must already be uploaded via [`POST /upload`](/docs/v2/model-swap#uploading-images); pass each one as `{"file_id": ""}`.
`input_assets` is not supported when `options.model` is `orbita`. Drop them or switch model.
## Submitting a creation job [#submitting-a-creation-job]
```python lineNumbers
import requests
api_url = "https://v2.api.piktid.com"
access_token = "your_access_token"
response = requests.post(
api_url + "/identity/create",
headers={"Authorization": "Bearer " + access_token},
json={
"instructions": [
{
"appearance": {
"gender": "female",
"age": 28,
"ethnicity": "North European",
"skin": "light",
"build": "slim",
"bust": "medium bust",
"expression": "confident and relaxed",
},
"face": {
"eyes": "pale blue",
"eyebrows": "natural arched",
"marks": "freckles",
},
"hair": {
"color": "ash blonde",
"length": "long",
"style": "wavy",
},
"num_variations": 3,
"options": {"ar": "3:4", "size": "2K", "format": "jpg"},
}
],
"options": {"model": "auto"},
"name": "Spring catalog model",
},
).json()
job_id = response["job_id"]
print(f"Creation job dispatched: {job_id}")
```
```jsonc title="Response"
{
"job_id": "job_abc123...", // JOB_ID [!code highlight]
"status": "pending",
"num_instructions": 1,
"total_variations": 3,
"message": "Identity creation job dispatched: 1 instruction(s), 3 total variation(s)"
}
```
### Job-level options [#job-level-options]
Fields inside the top-level `options` object that apply to every instruction in the job.
| Parameter | Type | Default | Description |
| ------------------ | -------------------------------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `model` | `"auto"` \| `"nano_banana_2"` \| `"nano_banana_pro"` \| `"seedream"` \| `"seedream_5_pro"` \| `"orbita"` | `"auto"` | Generation engine. `auto` lets the platform pick. `nano_banana_2` is Google's faster image model; `nano_banana_pro` is Google's higher-quality image model. `seedream` and `seedream_5_pro` are ByteDance's Seedream 4.5 and Seedream 5.0 Pro; both accept reference images. `orbita` has tighter constraints (see [Limits and validation](#limits-and-validation)). |
| `add_ai_watermark` | boolean | `false` | Bake an "AI-generated" disclosure watermark into every output (irreversible). **Enterprise-only**; silently ignored on other tiers. |
### Instruction-level options [#instruction-level-options]
Fields inside each instruction's `options` object.
| Parameter | Type | Default | Description |
| --------- | ------------------------------------------------------------------------------------------------------------- | ------- | ---------------------------------------------------- |
| `size` | `"1K"` \| `"2K"` \| `"4K"` | `"2K"` | Output resolution. `orbita` supports `1K` only. |
| `ar` | `"1:1"` \| `"2:3"` \| `"3:4"` \| `"4:5"` \| `"5:4"` \| `"3:2"` \| `"4:3"` \| `"9:16"` \| `"16:9"` \| `"21:9"` | `"3:4"` | Aspect ratio. `orbita` rejects `4:5`, `5:4`, `21:9`. |
| `format` | `"jpg"` \| `"png"` | `"jpg"` | Output file format. |
### Expert mode: free-form prompt [#expert-mode-free-form-prompt]
Set `prompt` on an instruction to send a single paragraph to the generator verbatim. The structured fields are still persisted as metadata, but they no longer drive the prompt.
```jsonc title="Expert prompt instruction"
{
"instructions": [
{
"prompt": "A 31-year-old woman of mixed South Asian and North European heritage, long jet-black hair parted in the middle and twisted into a loose low bun, deep brown eyes with faint amber flecks, a small gold septum ring, warm olive skin with a scatter of small beauty marks along the jawline. Natural minimal makeup, calm focused expression, gaze slightly off-camera.", // [!code highlight]
"num_variations": 2,
"options": {"ar": "3:4", "size": "2K", "format": "jpg"}
}
]
}
```
### Expert prompt with a reference image [#expert-prompt-with-a-reference-image]
Pair `prompt` with `input_assets` when you want the generator to use a visual cue alongside text direction.
```jsonc title="Expert prompt + reference"
{
"instructions": [
{
"prompt": "A model styled in the same vibe as the reference, three-quarter turn, calm focused expression.", // [!code highlight]
"input_assets": [{"file_id": "img_xyz..."}], // [!code highlight]
"num_variations": 2,
"options": {"ar": "3:4", "size": "2K", "format": "jpg"}
}
]
}
```
## Tracking drafts [#tracking-drafts]
Identity creation uses the same notifications surface as Model Swap and Flat-to-Model. Use SSE or webhooks with `id_task: ` until you receive a `completed` notification, then fetch the draft list.
```python lineNumbers
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
if notification["name"] == "completed":
break
if notification["name"] == "error":
raise RuntimeError("Identity creation failed")
# Fetch draft results
results = requests.get(
api_url + f"/jobs/{job_id}/results",
headers={"Authorization": "Bearer " + access_token},
).json()
for draft in results["results"]:
print(f"Draft #{draft['image_result_id']}: {draft['url']}")
```
```python lineNumbers
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"] == "completed":
print("Creation job completed")
elif notification["name"] == "error":
print("Identity creation failed")
return "", 204
app.run(port=8000)
```
```jsonc title="Job results response"
{
"job_id": "job_abc123...",
"job_type": "identity_creation",
"status": "completed",
"results": [
{
"image_index": 0,
"image_result_id": 12345, // IMAGE_RESULT_ID [!code highlight]
"url": "https://...", // Signed URL of the draft image
"status": "completed"
},
{
"image_index": 0,
"image_result_id": 12346,
"url": "https://...",
"status": "completed"
},
{
"image_index": 0,
"image_result_id": 12347,
"url": "https://...",
"status": "completed"
}
]
}
```
Drafts do not consume an identity slot and are not visible in the [identity list](/docs/v2/concepts/identities) until you promote one.
## Promoting a draft [#promoting-a-draft]
Pick the draft you want to keep and call `POST /identity/promote-generated` with its `image_result_id`. This is the step that consumes a 50-credit preprocessing charge and an identity slot.
```python lineNumbers
import requests
api_url = "https://v2.api.piktid.com"
access_token = "your_access_token"
response = requests.post(
api_url + "/identity/promote-generated",
headers={"Authorization": "Bearer " + access_token},
json={
"image_result_id": 12346,
"name": "Spring catalog model", # Optional; falls back to the job's name
},
).json()
identity_code = response["identity_code"]
preprocessing_job_id = response["preprocessing_job_id"]
print(f"Promoted to identity {identity_code}; preprocessing {preprocessing_job_id}")
```
```jsonc title="Response"
{
"identity_code": "abc1234567xy", // IDENTITY_CODE [!code highlight]
"preprocessing_job_id": "job_pre456...", // PREPROCESSING_JOB_ID [!code highlight]
"status": "pending",
"message": "Identity promoted; preprocessing dispatched."
}
```
If you supply an explicit `name` that already exists for one of your identities, the API returns `409`. If you omit `name`, the promotion falls back to the creation job's `name` and auto-suffixes ("Spring catalog model", "Spring catalog model 2", ...) so promoting multiple drafts from the same job does not collide.
Calling `promote-generated` twice on the same `image_result_id` returns `409` with the existing `identity_code`. If the previous promotion's preprocessing failed, the API hides the failed identity and lets you retry.
## Tracking preprocessing [#tracking-preprocessing]
Promotion dispatches a second job (`identity_preprocessing`). Track that job via notifications until preprocessing is complete.
```python lineNumbers
import json
import requests
api_url = "https://v2.api.piktid.com"
access_token = "your_access_token"
preprocessing_job_id = "job_pre456..."
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", {})
if notification["name"] != "identity_preprocessing":
continue
task_id = data.get("id_task") or data.get("job_id")
if task_id != preprocessing_job_id:
continue
status = data.get("status")
if status == "completed":
print("Identity preprocessing completed")
break
if status == "failed":
raise RuntimeError(data.get("error_message", "Preprocessing failed"))
```
```python lineNumbers
import hashlib
import hmac
import requests
from flask import Flask, abort, request
api_url = "https://v2.api.piktid.com"
access_token = "your_access_token"
preprocessing_job_id = "job_pre456..."
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", {})
if notification.get("name") != "identity_preprocessing":
return "", 204
task_id = data.get("id_task") or data.get("job_id")
if task_id != preprocessing_job_id:
return "", 204
status = data.get("status")
if status == "completed":
print("Identity preprocessing completed")
elif status == "failed":
print(f"Preprocessing failed: {data.get('error_message')}")
return "", 204
app.run(port=8000)
```
Use [`DELETE /notifications/{id}`](/docs/v2/notifications/notifications_delete__delete) after processing events so they are not replayed on reconnect.
Once preprocessing is `completed`, pass `identity_code` to any Model Swap or Flat-to-Model job exactly as you would for an uploaded identity.
## Limits and validation [#limits-and-validation]
| Limit | Value |
| -------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| Instructions per job | 5 |
| Variations per instruction | 1–8 |
| Reference images per instruction | 3 (not supported on `orbita`) |
| Concurrent jobs (non-enterprise) | 5 (shared with Model Swap and Flat-to-Model) |
| Identity slots (non-enterprise) | 50 |
| Allowed sizes | `1K`, `2K`, `4K` (`orbita`: `1K` only) |
| Allowed aspect ratios | `1:1`, `2:3`, `3:4`, `4:5`, `5:4`, `3:2`, `4:3`, `9:16`, `16:9`, `21:9` (`orbita` rejects `4:5`, `5:4`, `21:9`) |
| Allowed formats | `jpg`, `png` |
Each instruction must provide either a non-empty `prompt` or at least one populated structured field; otherwise the request is rejected with `400`.
## Error handling [#error-handling]
| Status | Cause |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `400` | Validation error (no instructions, >5 instructions, bad aspect ratio/size/format, missing reference image, empty instruction). Also returned when the prompt mentions a real person, in which case the response includes the blocked term. |
| `400` | Promotion: draft is not from an `identity_creation` job, or its status is not `completed`, or the face-detection step does not find exactly one face. |
| `402` | Insufficient credits. Response includes `required_credits`, `in_progress_credits`, and `user_credits`. |
| `403` | Output size exceeds the user's tier policy. |
| `404` | Promotion: the `image_result_id` does not exist or is not owned by the caller. |
| `409` | Promotion: draft already promoted (response includes the existing `identity_code`), or explicit `name` collides with an existing identity. |
| `429` | Rate limit hit, or the user is at the concurrent-jobs cap (non-enterprise) or identity-slot cap (non-enterprise). |
| `503` | The `IDENTITY_CREATION_ENABLED` feature flag is off. |
# Create packshot
URL: /docs/v2/create-packshot
Applies to API version: v2
Description: Generate clean product packshots from raw garment photos
You will need an API token to send HTTP requests. See [Authentication](/docs/v2/auth) for instructions.
## Quick start [#quick-start]
Create a project to organize your images. The `project_id` will be used in subsequent requests. See [Creating a project](#creating-a-project) for details.
***
Upload one or more garment-bearing images (front, back, side, detail shots) 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](#uploading-images) for details.
The `upload_url` is only valid for a limited time. Upload the image immediately after receiving the response.
***
Start a Create Packshot job by providing the list of uploaded file IDs and one or more instructions. Each instruction produces one output image (or `num_variations` outputs). No identity is required: packshots are product-only. See [Starting a job](#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](#tracking-progress) for details.
Unlike Flat-lay-to-on-model and Model Swap, Create Packshot does **not** require an identity. Outputs are product-only (no model). If you need a model wearing the garment, use [Flat-lay-to-on-model](/docs/v2/flat-lay) instead.
## Creating a project [#creating-a-project]
```python lineNumbers
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-packshot-project"},
).json()
project_id = response["project_id"]
project_name = response["project_name"]
```
```jsonc title="Response"
{
"project_id": "abc123...", // PROJECT_ID [!code highlight]
"project_name": "my-packshot-project"
}
```
## Uploading images [#uploading-images]
Upload all garment photos that the packshot job will consume: hanger shots, mannequin shots, flat-lays, on-model photos, even phone snaps. The AI uses every photo to reconstruct the garment, so more angles produce sharper packshots. You can upload between 1 and 10 images per job.
The `upload_url` is only valid for a limited time. Upload the image immediately after receiving the response.
```python lineNumbers
import requests
api_url = "https://v2.api.piktid.com"
access_token = "your_access_token"
project_name = "my-packshot-project"
image_path = "path/to/blazer-front.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": "blazer-front.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}")
```
```jsonc title="Response"
{
"upload_url": "https://s3...", // Pre-signed PUT URL
"download_url": "https://...",
"project_id": "abc123...",
"project_name": "my-packshot-project",
"file_id": "img_001...", // FILE_ID [!code highlight]
"filename": "blazer-front.jpg",
"content_type": "image/jpeg"
}
```
## Starting a job [#starting-a-job]
A Create Packshot job takes `N` garment images plus `M` instructions and produces `M` output packshots (or `Σ(num_variations)` if any instruction requests more than one variation):
* Each instruction produces one output by default; set `num_variations` (1-8) to fan out a single instruction into multiple variations.
* Stack instructions to cover multiple catalog surfaces in one job (e.g., one ghost-mannequin set for your PDP plus one flat-lay set for editorial), all from the same input photos.
* Instructions run in parallel.
**Credit cost:** 3 credits per output at `1K`, 5 at `2K`, 10 at `4K`, billed per output (matches Flat-lay-to-on-model pricing).
```python lineNumbers
import requests
api_url = "https://v2.api.piktid.com"
access_token = "your_access_token"
project_id = "abc123..."
file_ids = ["img_001...", "img_002..."] # Front + back of the same garment
# Define instructions - each produces one output (or num_variations outputs)
instructions = [
{
"style": "ghost_mannequin",
"background": "white studio",
"framing": "tall_3_4",
"angle": "three_quarter",
"shadow": "contact",
"num_variations": 3,
"options": {
"size": "2K",
"ar": "3:4",
"format": "jpg",
},
},
{
"style": "flat_lay",
"surface": "linen",
"shadow": "natural",
"options": {
"size": "2K",
"ar": "1:1",
"format": "jpg",
},
},
]
response = requests.post(
api_url + "/create-packshot",
headers={"Authorization": "Bearer " + access_token},
json={
"project_id": project_id,
"images": file_ids,
"instructions": instructions,
"post_process": False, # Optional: enable post-processing
"options": {
"model": "auto", # Optional: see "Generation options" below
"use_anchor": False, # Optional: opt-in cohesion across variations
},
},
).json()
job_id = response["job_id"]
total_outputs = response["total_outputs"]
print(f"Job started: {job_id} ({total_outputs} outputs)")
```
```jsonc title="Response"
{
"job_id": "job_abc123...", // JOB_ID [!code highlight]
"status": "pending",
"message": "Job created successfully",
"total_outputs": 4 // Sum of num_variations across instructions [!code highlight]
}
```
### Instruction parameters [#instruction-parameters]
Each instruction can contain the following parameters. Only `style` is required; everything else has sensible defaults derived from the chosen style.
| Parameter | Type | Required | Description |
| ---------------- | ----------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `style` | enum | yes | Packshot style. One of `flat_lay`, `ghost_mannequin`, `marketing_ready`, `white_cutout`, `free`. See [Style values](#style-values). |
| `background` | string \| int \| object | no | Background description (e.g. `"white studio"`), Design Value index, or `{text, image}` object. Style-default if omitted. |
| `composition` | string \| int \| object | no | Composition descriptor (e.g. `"single garment centered"`, `"folded on shelf"`). |
| `props` | string \| int \| object | no | Props for the scene. Only meaningful for `marketing_ready`; ignored for other styles. |
| `surface` | string \| int \| object | no | Surface the garment lies on (e.g. `"matte concrete"`, `"linen"`). Only meaningful for `flat_lay`; ignored for other styles. |
| `color_palette` | string \| int \| object | no | Color scheme descriptor. Most useful for `marketing_ready`. |
| `lighting` | string \| int \| object | no | Lighting descriptor. Also accepts a structured object with optional `direction`, `quality`, `complexity` sub-fields. |
| `framing` | enum | no | How the garment fills the canvas. One of `square_packshot`, `tall_3_4`, `wide_4_3`, `full_frame`. |
| `angle` | enum | no | View angle. One of `front`, `three_quarter`, `side`, `back_three_quarter`, `back`, `top_down`, `bottom`, `detail_macro`. `top_down` is forced for `flat_lay`. |
| `presentation` | enum | no | How the product is held up. One of `auto`, `flat`, `folded`, `stacked`, `hanger`, `mannequin`, `ghost_mannequin`, `draped`, `floating`. See [Presentation](#presentation). |
| `shadow` | enum | no | Shadow treatment. One of `auto`, `none`, `contact`, `soft_drop`, `natural`. Style-specific defaults apply when omitted. |
| `prompt` | string | no | Free-form prompt overlay. When empty, the engine builds one automatically from `style` and the structured fields above. |
| `seed` | integer | no | Reproducibility seed for this instruction. |
| `num_variations` | integer (1-8) | no | Number of output variations to generate from this instruction. Defaults to `1`. |
| `preset_name` | string | no | Metadata only. Name of the preset this instruction came from, surfaced in the UI. |
| `category_names` | string\[] | no | Metadata only. Categories the preset belongs to. |
| `options` | object | no | Per-instruction output options. See [Output options](#output-options). |
### Style values [#style-values]
The `style` field controls the look of the packshot and the defaults applied to the structured fields. Pick the style that matches the catalog surface you're producing for.
| Style | Description | Best for |
| ----------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
| `flat_lay` | Top-down view of the garment laid on a clean surface. `angle` is forced to `top_down`. | Editorial product grids, lookbooks |
| `ghost_mannequin` | Invisible mannequin: the garment hovers in 3D as if worn, no body visible. | Most fashion PDPs |
| `marketing_ready` | Editorial scene with props, lighting, and atmosphere. `props` and `color_palette` carry weight here. | Campaign imagery, hero banners |
| `white_cutout` | Pure white seamless background with sharp edges and minimal contact shadow. | Marketplaces (Amazon, Zalando), feeds |
| `free` | Unconstrained. The engine interprets the structured fields and `prompt` without applying a style preset. | Custom looks that do not fit the four above |
### Presentation [#presentation]
`style` describes the **scene**; `presentation` describes **how the product is physically held up**. They are independent — a hanger shot on pure white is `style: "white_cutout"` with `presentation: "hanger"`.
| Value | What you get |
| ----------------- | ----------------------------------------------------------------------------------------------- |
| `auto` | The style decides. This is the default and the behaviour of every request that omits the field. |
| `flat` | Lying flat on the surface, unsupported. |
| `folded` | Neatly folded into a rectangle with squared edges. |
| `stacked` | Several folded copies stacked on one another. |
| `hanger` | Hanging from a plain, unbranded hanger. |
| `mannequin` | On a **visible** headless, featureless dress form. |
| `ghost_mannequin` | Holding a worn 3D shape with no visible support. |
| `draped` | Draped over a simple form with a natural fall. |
| `floating` | Suspended in space with no visible support. |
Not every style can honour every value. A style that owns its presentation wins, and the incompatible value is ignored rather than rejected:
| Style | Accepts | Notes |
| ----------------- | --------------------------- | ------------------------------------- |
| `flat_lay` | `flat`, `folded`, `stacked` | Anything else falls back to `flat`. |
| `ghost_mannequin` | `ghost_mannequin` | Locked — the style is a presentation. |
| `marketing_ready` | any | |
| `white_cutout` | any | |
| `free` | any | |
Outputs are still product-only whatever you pick. `mannequin` means a featureless dress form, never a person — the "no model, no body parts, no skin" guarantee is never relaxed.
```jsonc title="Hanger shot on a pure white marketplace background"
{
"style": "white_cutout",
"presentation": "hanger", // [!code highlight]
"angle": "side", // [!code highlight]
"options": { "size": "2K", "ar": "1:1", "format": "png" }
}
```
### Output options [#output-options]
The `options` object inside each instruction can contain:
| Parameter | Values | Description |
| --------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `size` | `"1K"`, `"2K"`, `"4K"` | Output image resolution. Drives credit cost: 3 / 5 / 10 credits per output. |
| `ar` | `"1:1"`, `"3:4"`, `"4:3"`, `"9:16"`, `"16:9"` | Aspect ratio. |
| `format` | `"jpg"`, `"png"` | Output file format. |
| `width` | integer (256-7000) | Custom output width. Must be set together with `height`. Aspect ratio must be between 1:4 and 4:1. Requires the `OUTPUT_CUSTOM_DIMENSIONS` policy on your account. |
| `height` | integer (256-7000) | Custom output height. Must be set together with `width`. |
| `seed` | integer | Random seed for this output. Prefer the top-level `seed` field on the instruction. |
### Generation options [#generation-options]
Top-level fields inside the request's `options` object that control *how* the batch is generated (as opposed to per-instruction styling).
| Parameter | Type | Default | Description |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `model` | `"auto"` \| `"nano_banana_2"` \| `"nano_banana_pro"` \| `"seedream"` \| `"seedream_5_pro"` \| `"gpt_image"` \| `"gpt_image_2_5"` | `"auto"` | Which engine generates outputs. `auto` runs the default engine (Google's Nano Banana 2) with a safety fallback if content is refused. `nano_banana_2` is Google's faster image model; `nano_banana_pro` is Google's higher-quality image model; `gpt_image` and `gpt_image_2_5` are OpenAI's GPT Image 2 and GPT Image 2.5; both render legible garment lettering, and 2.5 is markedly faster. `seedream` and `seedream_5_pro` are ByteDance's Seedream 4.5 and Seedream 5.0 Pro; both accept reference images. Specifying an engine disables the fallback. |
| `use_anchor` | boolean | `false` | When `true`, the engine pins one instruction as the canonical look reference and aligns every output to it. Defaults to `false` for Create Packshot. |
| `anchor_index` | integer | `0` | Which instruction (zero-indexed into `instructions`) is used as the anchor reference when `use_anchor` is `true`. Must satisfy `0 <= anchor_index < len(instructions)`. |
```jsonc title="Request with generation options"
{
"project_id": "abc123...",
"images": ["img_001...", "img_002..."],
"instructions": [/* ... */],
"options": {
"model": "nano_banana_2", // [!code highlight]
"use_anchor": true, // [!code highlight]
"anchor_index": 0 // [!code highlight]
}
}
```
### Force flat background [#force-flat-background]
Marketplace and white-cutout feeds usually require a perfectly uniform background, but generation engines don't always render a solid color cleanly — they can drift slightly, add a subtle gradient, or treat a plain studio as a textured scene. When your instruction's `background` is a solid hex color (e.g. `"#FFFFFF"`), set `force_flat_background` to `true` inside the top-level `options` object to lock the output background to exactly that color. This requires `post_process: true`, and has no effect when the background is a descriptive surface or scene rather than a solid color.
| Parameter | Type | Default | Description |
| ----------------------- | ------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `force_flat_background` | boolean | `false` | Force the output background to the exact hex color given in the instruction's `background` field. Applies only when `background` is a solid hex color and `post_process` is enabled. |
```jsonc title="Request with forced flat background"
{
"project_id": "abc123...",
"images": ["img_001...", "img_002..."],
"post_process": true,
"instructions": [
{
"style": "white_cutout",
"background": "#FFFFFF" // [!code highlight]
}
],
"options": {
"force_flat_background": true // [!code highlight]
}
}
```
### Per-image annotations [#per-image-annotations]
You can provide optional notes for individual images to highlight context the AI might otherwise miss: which view the photo shows, fabric peculiarities, lining details to preserve, or which input represents the canonical "hero" angle.
The `images` field accepts two formats:
| Format | Example | Description |
| --------- | --------------------------------------------- | ----------------------------------------------- |
| 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.
```jsonc title="Request with per-image annotations"
{
"project_id": "abc123...",
"images": [
{"file_id": "img_001...", "note": "front view, hanger removed in post"}, // [!code highlight]
{"file_id": "img_002...", "note": "back view, same garment"}, // [!code highlight]
{"file_id": "img_003...", "note": "detail of printed lining"} // [!code highlight]
],
"instructions": [/* ... */]
}
```
**Example notes:**
* `"front view"` / `"back view"` / `"three-quarter angle"`
* `"detail of stitching"` or `"close-up of buttons"`
* `"shows printed inner lining, preserve in output"`
* `"phone photo, ignore the wrinkled bedsheet background"`
* `"hero angle, prioritize this look"`
### Post-processing [#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.
```jsonc title="Request with post-processing"
{
"project_id": "abc123...",
"images": ["img_001...", "img_002..."],
"instructions": [/* ... */],
"post_process": true // [!code highlight]
}
```
## Tracking progress [#tracking-progress]
Use either SSE or webhooks to receive notifications for job updates.
```python lineNumbers
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"))
```
```python lineNumbers
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)
```
```jsonc title="Response"
[
{
"id": 12345,
"name": "batch_edit", // Notification type [!code highlight]
"timestamp": 1702819200.0,
"data": { // Job-specific data [!code highlight]
"id_task": "job_abc123...",
"status": "completed",
"total_images": 4,
"processed_images": 4
}
}
]
```
Use [`DELETE /notifications/{id}`](/docs/v2/notifications/notifications_delete__delete) after processing events so they are not replayed on reconnect.
## Retrieving results [#retrieving-results]
Once the job is complete, retrieve the processed images.
```python lineNumbers
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']}")
```
```jsonc title="Response"
{
"job_id": "job_abc123...",
"job_type": "create_packshot",
"status": "completed",
"results": [
{
"image_index": 0,
"group_index": 0,
"output": {
"full_size": "https://...", // Result image URL [!code highlight]
"thumbnail": "https://..."
},
"model_used": "nano_banana_2", // Engine that produced this output [!code highlight]
"status": "completed"
},
{
"image_index": 1,
"group_index": 0,
"output": {
"full_size": "https://...",
"thumbnail": "https://..."
},
"model_used": "nano_banana_2",
"status": "completed"
}
],
"summary": {
// Job statistics
}
}
```
### Bulk download [#bulk-download]
For bulk downloads, generate a temporary download URL that packages all results into a ZIP file.
```python lineNumbers
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)
```
```jsonc title="Response"
{
"download_url": "https://v2.api.piktid.com/download/token123...",
"expires": "2024-12-17T11:00:00Z" // URL expiration time [!code highlight]
}
```
## Checking job status [#checking-job-status]
You can also check the job status directly without waiting for notifications.
```python lineNumbers
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']}")
```
```jsonc title="Response"
{
"job_id": "job_abc123...",
"job_type": "create_packshot",
"status": "processing",
"progress": 50.0,
"total_images": 4,
"processed_images": 2,
"should_post_process": false,
"post_processing_status": null,
"created_at": "2024-12-17T10:00:00Z",
"updated_at": "2024-12-17T10:05:00Z"
}
```
## Error handling [#error-handling]
Jobs may fail due to various reasons. Check the `error_message` field in the job status or results.
```python lineNumbers
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']}")
```
### Common errors at job creation [#common-errors-at-job-creation]
`POST /create-packshot` returns the following errors before the job is queued:
| HTTP | Meaning |
| ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400 | Missing or invalid image, invalid `anchor_index`, invalid custom dimensions, unsupported `model`. |
| 402 | Insufficient credits. Response body includes `required_credits`, `in_progress_credits`, `user_credits`. |
| 403 | Output size exceeds your plan's policy, or you do not have access to the target project. |
| 404 | Project not found. |
| 429 | Concurrent job limit reached. Non-enterprise accounts cap at 5 active jobs across model-swap, flat-2-model, and create-packshot combined. The endpoint is also rate-limited to 5 requests per minute per user. |
```jsonc title="402 Insufficient credits response"
{
"error": "Insufficient credits to create job",
"required_credits": 20.0,
"in_progress_credits": 5.0,
"user_credits": 12.0
}
```
# Detail repair
URL: /docs/v2/detail-repair
Applies to API version: v2
Description: Regenerate a masked logo, text, or small detail and blend only that region back
You will need an API token to send HTTP requests. See [Authentication](/docs/v2/auth) for instructions.
Detail Repair regenerates **only a masked region** of an image — a garbled logo, distorted text, a small artifact — and composites just that region back onto the original, so everything outside the mask stays untouched. Optional reference shots act as ground truth so the fix reproduces the real detail instead of inventing one.
It also doubles as a general-purpose **masked inpainting** tool: with `category: "general"` you can drive any edit inside the mask (hairstyle, garment color, object removal, …) from a text instruction. See [General-purpose inpainting](#general-purpose-inpainting).
It is built for programmatic pipelines: upload the image to fix, (optionally) a few reference shots, and a **binary mask image**, then start a job. Each job returns `num_candidates` variations.
## Quick start [#quick-start]
Create a project to organize your images. The `project_id` is used in later requests.
***
Upload the **image to fix**, any **reference shots**, and your **mask image** — each via the two-step pre-signed upload. Collect every `file_id`.
***
Start a detail repair job with the image, the mask, and (optionally) the references.
***
Track progress via SSE or webhooks; stop on a terminal status, then fetch the results.
## Uploading images [#uploading-images]
Every image (the one to fix, the references, and the mask) is uploaded the same way: request a pre-signed URL, then PUT the bytes. See [Uploading images](#uploading-images).
```python lineNumbers
import requests
api_url = "https://v2.api.piktid.com"
access_token = "your_access_token"
project_name = "my-detail-repair-project"
def upload(path, filename):
r = requests.post(
api_url + "/upload",
headers={"Authorization": "Bearer " + access_token},
json={"project_name": project_name, "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["project_id"], r["file_id"]
project_id, image_id = upload("product.jpg", "product.jpg") # the image to fix
_, reference_id = upload("reference.jpg", "reference.jpg") # ground-truth shot (optional)
_, mask_id = upload("mask.png", "mask.png") # binary mask (see below)
```
## Providing the mask [#providing-the-mask]
The mask tells the engine **which region to regenerate**. There are two ways to supply it; **a binary mask image is the recommended path for API pipelines**.
### Binary mask image (recommended) [#binary-mask-image-recommended]
Upload a black-and-white PNG where **white (255) marks the area to repair** and black is left untouched, then pass its `file_id` as `mask_image`.
The mask must be the **same pixel dimensions as the image to fix** (it is resized to match if it differs). The repair follows the **exact shape** of the white pixels — not just their bounding rectangle.
```python
# A mask you produced however you like — segmentation model, OpenCV threshold,
# a hand-drawn PNG, etc. White = repair, black = keep.
_, mask_id = upload("mask.png", "mask.png")
# ... pass mask_image=mask_id when starting the job (see below).
```
### Bounding-box rectangle [#bounding-box-rectangle]
For a coarse fix you can skip the mask image and send a normalized rectangle instead — `mask.bbox_norm = [x, y, w, h]` in `0..1` coordinates (origin top-left). The **whole rectangle** is regenerated and blended back.
```python
mask = {"bbox_norm": [0.66, 0.30, 0.08, 0.05]} # x, y, width, height (fractions of the image)
```
Provide **either** `mask_image` **or** `mask.bbox_norm` (at least one is required). A binary mask gives a pixel-precise repair; a bbox regenerates the whole rectangle. The app's brush-`strokes` format is also accepted but is intended for the UI — prefer `mask_image` for integrations.
## Starting a job [#starting-a-job]
```python lineNumbers
import requests
api_url = "https://v2.api.piktid.com"
access_token = "your_access_token"
response = requests.post(
api_url + "/detail-repair",
headers={"Authorization": "Bearer " + access_token},
json={
"project_id": project_id,
"image": image_id, # the image to fix
"mask_image": mask_id, # binary mask (white = repair area) [!code highlight]
"references": [reference_id], # optional ground-truth shots (max 10)
"num_candidates": 3, # 1-4 variations
"category": "logo", # logo | text | artifact | ...
"reviewer_note": "Fix the wordmark to match the reference exactly.",
"model": "auto",
},
).json()
job_id = response["job_id"]
print(f"Job started: {job_id}")
```
```jsonc title="Response"
{
"job_id": "job_abc123...", // JOB_ID [!code highlight]
"status": "pending",
"message": "Detail repair job created and queued"
}
```
### Request parameters [#request-parameters]
| Field | Type | Default | Description |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `project_id` | string | — | Project key the images belong to. **Required.** |
| `image` | string | — | `file_id` of the image to fix. **Required.** |
| `mask_image` | string | `null` | `file_id` of a binary mask (white = repair). Provide this **or** `mask`. |
| `mask` | object | `null` | `{ "bbox_norm": [x, y, w, h] }` rectangle in `0..1`. Provide this **or** `mask_image`. |
| `references` | string\[] | `[]` | `file_id`s of reference shots where the detail is clearly visible. Max **10**. Strongly recommended — without them the engine reconstructs from context and may approximate the detail. |
| `num_candidates` | integer | `1` | Variations to generate, `1`–`4`. Each is billed separately. |
| `category` | string | `"logo"` | Prompt family. `logo` (default) optimizes for logos / brand marks; `general` switches to general-purpose inpainting for any masked edit (see below). Free-form — other values (`text`, `artifact`, …) also use the logo-optimized prompt. |
| `reviewer_note` | string | `null` | Targeted instruction, e.g. *"fix the first letter only"*. |
| `image_notes` | string\[] | `[]` | Brand/style hints forwarded to the prompt (e.g. font, casing). |
| `model` | `"auto"` \| `"nano_banana_2"` \| `"nano_banana_pro"` \| `"seedream"` \| `"seedream_5_pro"` \| `"gpt_image"` \| `"gpt_image_2_5"` | `"auto"` | Which engine generates the fix. `auto` runs the default engine with a safety fallback. `seedream` and `seedream_5_pro` are ByteDance's Seedream 4.5 and Seedream 5.0 Pro; both accept reference images. `gpt_image` and `gpt_image_2_5` are OpenAI's GPT Image 2 and GPT Image 2.5; both render legible garment lettering, and 2.5 is markedly faster. Specifying an engine disables the fallback. |
| `add_ai_watermark` | boolean | `false` | Bake an "AI-generated" disclosure mark into each output. Ignored (forced on) for free-tier accounts. |
### Advanced controls (enterprise) [#advanced-controls-enterprise]
Enterprise/admin accounts can tune *how* the region is regenerated. Other tiers always use the defaults below.
| Field | Type | Default | Description |
| ---------------- | ------ | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mode` | string | `"guided-blend"` | Generation strategy. Three techniques — `guided` (recommended, highest fidelity), `annotated`, and `marked` — each available with or without a `-blend` variant. The `-blend` variants (default) keep the change tightly confined to the masked detail for a seamless result; the non-blend variants regenerate the region more freely. If `guided` isn't ideal for a given detail, `annotated` and `marked` are alternative strategies to try. Six values: `marked`, `marked-blend`, `annotated`, `annotated-blend`, `guided`, `guided-blend`. |
| `context_factor` | number | `3.0` | How much surrounding context the engine considers when regenerating the detail. Clamped to `1.5`–`10.0`. Lower keeps the focus tight on the detail; higher takes in more of the surroundings. |
## General-purpose inpainting [#general-purpose-inpainting]
Detail Repair isn't only for logos. Set `category: "general"` to switch to a
general-purpose inpainting prompt for **any** masked edit — change a hairstyle,
recolor a garment, swap a detail, remove an object — while everything outside the
mask stays untouched. The edit is driven by your **`reviewer_note`** (the
instruction); `references`, when supplied, should show the **desired result**.
```python
# Recolor the jacket inside the mask, guided by the instruction.
response = requests.post(
api_url + "/detail-repair",
headers={"Authorization": "Bearer " + access_token},
json={
"project_id": project_id,
"image": image_id,
"mask_image": mask_id, # the region to edit
"category": "general", # general-purpose inpainting [!code highlight]
"reviewer_note": "Make the jacket forest green; keep the texture and folds.",
"references": [], # optional — show the target look if you have one
"num_candidates": 3,
},
).json()
```
In general mode the **instruction is the spec** — be specific about what changes
and what must stay the same. With no instruction, the engine simply regenerates the
marked area to be clean and coherent with its surroundings. Everything else (engine
selection, candidates, the quality check) works exactly as in logo mode.
## Credits [#credits]
Detail Repair costs a flat **3 credits per variation**, regardless of resolution (the output is preserved at its original size; processing is capped at 2K internally). A 3-candidate job costs **9 credits**, charged once on completion. Failed candidates are not charged. See [Credits](/docs/v2/concepts/credits).
## Tracking progress [#tracking-progress]
Track the job with the notifications stream (SSE) or a webhook, filtering on your `job_id` and stopping on a terminal status (`completed`, `failed`, `aborted`). See [Notifications](/docs/v2/concepts/notifications) and [Webhooks](/docs/v2/concepts/webhooks).
## Retrieving results [#retrieving-results]
Once the job is `completed`, fetch its results like any other job (see [Jobs](/docs/v2/concepts/jobs)). Each output is a standard image result with the repaired region composited onto the original.
**Quality check.** Each output carries an automatic signal. If the engine can't confirm the fix lines up cleanly with the original, the output is flagged (`needs_review`, with a reason such as `scale_drift`, `framing_shift`, or `no_reference` when the job ran without references). Treat a flagged output as "compare against the original before using" — a handy signal for ranking candidates.
## Error handling [#error-handling]
| Status | Meaning |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400` | Missing/invalid input — no mask provided (`mask_image` and `mask` both absent), unknown `image`/`mask_image`/reference `file_id`, or an unreadable mask. |
| `402` | Insufficient credits for the requested number of candidates. |
| `403` | A subject was blocked by the content filter. |
| `404` | Project not found. |
See [Errors](/docs/v2/concepts/errors) for the full error model.
# Flat lay on model
URL: /docs/v2/flat-lay
Applies to API version: v2
Description: Virtual try-on from flat lay images with a custom model
You will need an API token to send HTTP requests. See [Authentication](/docs/v2/auth) for instructions.
## Quick start [#quick-start]
Create a project to organize your images. The `project_id` will be used in subsequent requests. See [Creating a project](#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](#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](#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](#tracking-progress) for details.
## Uploading identities [#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.
```python lineNumbers
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}")
```
```jsonc title="Response"
{
"identity_code": "id_xyz...", // IDENTITY_CODE [!code highlight]
"name": "Model A",
"face_detected": true,
"success": true,
// ...
}
```
You can list existing identities using:
## Creating a project [#creating-a-project]
```python lineNumbers
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"]
```
```jsonc title="Response"
{
"project_id": "abc123...", // PROJECT_ID [!code highlight]
"project_name": "my-flat-lay-project"
}
```
## Uploading images [#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.
```python lineNumbers
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}")
```
```jsonc title="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 [!code highlight]
"filename": "sku-shirt.jpg",
"content_type": "image/jpeg"
}
```
## Starting a job [#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
```python lineNumbers
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)")
```
```jsonc title="Response"
{
"job_id": "job_abc123...", // JOB_ID [!code highlight]
"status": "pending",
"message": "Job created successfully",
"total_outputs": 2 // Number of output images [!code highlight]
}
```
### Instruction parameters [#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.
| Parameter | Type | Description |
| ---------------- | ----------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `pose` | string \| int \| object | Pose descriptor (e.g. `"standing front-facing"`, `"walking, mid-stride"`). |
| `expression` | string \| int \| object | Facial expression descriptor (e.g. `"neutral"`, `"smiling"`, `"confident"`). |
| `mood` | string \| int \| object | Overall mood / atmosphere descriptor (e.g. `"editorial"`, `"casual"`). |
| `camera` | string \| int \| object | Camera descriptor. Plain text or a structured object with optional `framing`, `angle`, `lens`, `aperture` sub-fields. |
| `lighting` | string \| int \| object | Lighting descriptor. Plain text or a structured object with optional `direction`, `quality`, `complexity` sub-fields. |
| `background` | string \| int \| object | Background descriptor (e.g. `"white studio"`, `"gym rooftop at golden hour"`). |
| `style` | string \| int \| object | Visual style descriptor. |
| `color_palette` | string \| int \| object | Color scheme descriptor. |
| `prompt` | string | Free-form prompt overlay. When provided, the engine combines it with the structured fields above. |
| `seed` | integer | Reproducibility seed for this instruction. Top-level seed is preferred over `options.seed`. |
| `num_variations` | integer (1-8) | Number of output variations to generate from this instruction. Defaults to `1`. |
| `angle` | string \| int \| object | **Deprecated.** Use `camera.angle` instead. Kept for backward compatibility. |
| `preset_name` | string | Metadata only. Name of the preset this instruction came from, surfaced in the UI. |
| `category_names` | string\[] | Metadata only. Categories the preset belongs to. |
| `options` | object | Per-instruction output options. See [Output options](#output-options). |
### Output options [#output-options]
The `options` object within each instruction can contain:
| Parameter | Values | Description |
| --------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `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. |
| `width` | integer (256-7000) | 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`. |
| `height` | integer (256-7000) | Custom output height. Must be set together with `width`. |
| `seed` | integer | Random seed for this output. Prefer the top-level `seed` field on the instruction. |
### Generation options [#generation-options]
Top-level fields inside the request's `options` object that control *how* the batch is generated (as opposed to per-instruction styling).
| Parameter | Type | Default | Description |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `model` | `"auto"` \| `"nano_banana_2"` \| `"nano_banana_pro"` \| `"seedream"` \| `"seedream_5_pro"` \| `"gpt_image"` \| `"gpt_image_2_5"` | `"auto"` | Which engine generates outputs. `auto` runs the default engine (Google's Nano Banana 2) with a safety fallback if content is refused. `nano_banana_2` is Google's faster image model; `nano_banana_pro` is Google's higher-quality image model; `gpt_image` and `gpt_image_2_5` are OpenAI's GPT Image 2 and GPT Image 2.5; both render legible garment lettering, and 2.5 is markedly faster. `seedream` and `seedream_5_pro` are ByteDance's Seedream 4.5 and Seedream 5.0 Pro; both accept reference images. Specifying an engine disables the fallback. |
| `use_anchor` | boolean | `true` | Keeps 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_index` | integer | `0` | Which instruction (zero-indexed into `instructions`) is used as the anchor reference when `use_anchor` is `true`. Must satisfy `0 <= anchor_index < len(instructions)`. |
```jsonc title="Request with generation options"
{
"identity_code": "id_xyz...",
"project_id": "abc123...",
"images": ["sku_001...", "sku_002..."],
"instructions": [/* ... */],
"options": {
"model": "nano_banana_2", // [!code highlight]
"use_anchor": false // [!code highlight]
}
}
```
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 [#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.
```jsonc title="Request with AI disclosure"
{
"identity_code": "id_xyz...",
"project_id": "abc123...",
"images": ["sku_001...", "sku_002..."],
"instructions": [/* ... */],
"options": {
"add_ai_watermark": true // [!code highlight]
}
}
```
### Barefoot mode [#barefoot-mode]
By default, flat-to-model expects a complete outfit (top, bottom, and footwear) and renders the model wearing shoes. If your SKUs don't include footwear, or you want the model rendered barefoot, set `barefoot` to `true` inside the top-level `options` object. When enabled, footwear is dropped from the wardrobe description and the model is rendered without shoes.
| Parameter | Type | Default | Description |
| ---------- | ------- | ------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `barefoot` | boolean | `false` | Render the model without footwear. Footwear images/notes are ignored. Only top and bottom are required (no shoe image needed). |
```jsonc title="Request with barefoot mode"
{
"identity_code": "id_xyz...",
"project_id": "abc123...",
"images": ["sku_top...", "sku_bottom..."],
"instructions": [/* ... */],
"options": {
"barefoot": true // [!code highlight]
}
}
```
### No top [#no-top]
Swimwear, underwear, lingerie and base layers are complete looks on their own. By default
flat-to-model styles a garment for any category you don't upload, which for these products
means an invented t-shirt over the item you are selling. Set `no_top` to `true` inside the
top-level `options` object to render the model with no upper garment.
What gets rendered depends on the identity, and on whether the garments are a swim or
underwear set:
| Identity | Swim / underwear set | Any other look |
| --------------------------------------- | ----------------------------------------------------------------------------------------- | -------------------------------------------- |
| Child or teenage | A full, age-appropriate swim top covering the chest and torso, matching the lower garment | A plain, age-appropriate t-shirt or vest top |
| Male-presenting adult | Bare chest, no top garment | Bare chest, no top garment |
| Female-presenting adult | A matching bikini top or bralette in the same colour and fabric as the lower garment | A plain bra or bralette in a neutral tone |
| Adult whose gender cannot be determined | Treated as female-presenting | Treated as female-presenting |
The minor row is applied first: a minor is always dressed, whatever the request asked for. For
a minor on a non-swim look the result is close to what you would get without the flag at all,
which is intended.
The swim / non-swim split matters. Asking for "a top matching the lower garment" is right for a
bikini bottom and wrong for jeans, where it would return a denim shirt, so on a general look the
garment is named directly instead.
As with `barefoot` and footwear, any top images you upload are ignored while the flag is set.
You often won't need the flag at all. When the uploaded garments are recognisably swimwear or
underwear, flat-to-model applies the same behaviour on its own — including rendering the look
barefoot. Use `no_top` when you want the guarantee, or for looks that aren't swim or underwear
(a shirtless model in jeans, for example).
| Parameter | Type | Default | Description |
| --------- | ------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `no_top` | boolean | `false` | Render the model with no upper garment. Top images are ignored, and no top is invented. Combine with `barefoot` for a full swim or underwear look. |
```jsonc title="Request with no top"
{
"identity_code": "id_xyz...",
"project_id": "abc123...",
"images": ["sku_briefs..."],
"instructions": [/* ... */],
"options": {
"no_top": true, // [!code highlight]
"barefoot": true
}
}
```
### Force flat background [#force-flat-background]
Marketplaces and product feeds often require a perfectly uniform background, but generation engines don't always render a solid color cleanly — they can drift slightly, add a subtle gradient, or treat a plain studio as a textured scene. When your instruction's `background` is a solid hex color (e.g. `"#EDEDED"`), set `force_flat_background` to `true` inside the top-level `options` object to lock the output background to exactly that color. This requires `post_process: true`, and has no effect when the background is a descriptive scene rather than a solid color.
| Parameter | Type | Default | Description |
| ----------------------- | ------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `force_flat_background` | boolean | `false` | Force the output background to the exact hex color given in the instruction's `background` field. Applies only when `background` is a solid hex color and `post_process` is enabled. |
```jsonc title="Request with forced flat background"
{
"identity_code": "id_xyz...",
"project_id": "abc123...",
"images": ["sku_top...", "sku_bottom..."],
"post_process": true,
"instructions": [
{
"background": "#EDEDED", // [!code highlight]
"pose": "standing front-facing"
}
],
"options": {
"force_flat_background": true // [!code highlight]
}
}
```
### Per-image annotations [#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:
| Format | Example | Description |
| --------- | --------------------------------------------- | ----------------------------------------------- |
| 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.
```jsonc title="Request with per-image annotations"
{
"identity_code": "id_xyz...",
"project_id": "abc123...",
"images": [
{"file_id": "sku_001...", "note": "tucked into pants"}, // [!code highlight]
{"file_id": "sku_002...", "note": "slim fit, cuffed at ankles"}, // [!code highlight]
{"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 [#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.
```jsonc title="Request with post-processing"
{
"identity_code": "id_xyz...",
"project_id": "abc123...",
"images": ["sku_001...", "sku_002..."],
"instructions": [/* ... */],
"post_process": true // [!code highlight]
}
```
## Tracking progress [#tracking-progress]
Use either SSE or webhooks to receive notifications for job updates.
```python lineNumbers
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"))
```
```python lineNumbers
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)
```
```jsonc title="Response"
[
{
"id": 12345,
"name": "batch_edit", // Notification type [!code highlight]
"timestamp": 1702819200.0,
"data": { // Job-specific data [!code highlight]
"id_task": "job_abc123...",
"status": "completed",
"total_images": 2,
"processed_images": 2
}
}
]
```
Use [`DELETE /notifications/{id}`](/docs/v2/notifications/notifications_delete__delete) after processing events so they are not replayed on reconnect.
## Retrieving results [#retrieving-results]
Once the job is complete, retrieve the processed images.
```python lineNumbers
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']}")
```
```jsonc title="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 [!code highlight]
"thumbnail": "https://..."
},
"model_used": "nano_banana_2", // Engine that produced this output [!code highlight]
"status": "completed"
},
{
"image_index": 1,
"group_index": 0,
"output": {
"full_size": "https://...",
"thumbnail": "https://..."
},
"model_used": "seedream",
"status": "completed"
}
],
"summary": {
// Job statistics
}
}
```
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.
```python
for result in response["results"]:
print(f"Image {result['image_index']}: model_used = {result.get('model_used')}")
```
### Bulk download [#bulk-download]
For bulk downloads, generate a temporary download URL that packages all results into a ZIP file.
```python lineNumbers
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)
```
```jsonc title="Response"
{
"download_url": "https://v2.api.piktid.com/download/token123...",
"expires": "2024-12-17T11:00:00Z" // URL expiration time [!code highlight]
}
```
## Checking job status [#checking-job-status]
You can also check the job status directly without waiting for notifications.
```python lineNumbers
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']}")
```
```jsonc title="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 [#error-handling]
Jobs may fail due to various reasons. Check the `error_message` field in the job status or results.
```python lineNumbers
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']}")
```
# Garment recolor
URL: /docs/v2/garment-recolor
Applies to API version: v2
Description: Recolor one garment consistently across a set of product images
You will need an API token to send HTTP requests. See [Authentication](/docs/v2/auth) for instructions.
## Quick start [#quick-start]
Create a project to organize your images. The `project_id` will be used in subsequent requests. See [Creating a project](#creating-a-project) for details.
***
Upload the product images that contain the garment to recolor (and, optionally, a colour/pattern reference image). 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](#uploading-images) for details.
The `upload_url` is only valid for a limited time. Upload the image immediately after receiving the response.
***
Start a Garment Recolor job with the list of uploaded file IDs and a recolor target (a reference image, a colour/pattern text, an instruction, or any combination). Each input image yields `num_variations` outputs. No identity is required: Garment Recolor is product-only. See [Starting a job](#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](#tracking-progress) for details.
Like Create Packshot, Garment Recolor does **not** require an identity. It recolors the garment in your existing product images and keeps everything else untouched. If you need a model wearing the garment, use [Flat-lay-to-on-model](/docs/v2/flat-lay) or [Model Swap](/docs/v2/model-swap) instead.
## Creating a project [#creating-a-project]
```python lineNumbers
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-recolor-project"},
).json()
project_id = response["project_id"]
project_name = response["project_name"]
```
```jsonc title="Response"
{
"project_id": "abc123...", // PROJECT_ID [!code highlight]
"project_name": "my-recolor-project"
}
```
## Uploading images [#uploading-images]
Upload the product images that show the garment you want to recolor. For a consistent result, all images should feature the **same** garment (different poses, angles, or models are fine). You can upload between 1 and 10 images per job.
If you want to recolor toward a reference image (a colour swatch or a patterned fabric), upload it the same way and keep its `file_id` for the `reference_image` field in the next step.
The `upload_url` is only valid for a limited time. Upload the image immediately after receiving the response.
```python lineNumbers
import requests
api_url = "https://v2.api.piktid.com"
access_token = "your_access_token"
project_name = "my-recolor-project"
image_path = "path/to/tshirt-front.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": "tshirt-front.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}")
```
```jsonc title="Response"
{
"upload_url": "https://s3...", // Pre-signed PUT URL
"download_url": "https://...",
"project_id": "abc123...",
"project_name": "my-recolor-project",
"file_id": "img_001...", // FILE_ID [!code highlight]
"filename": "tshirt-front.jpg",
"content_type": "image/jpeg"
}
```
## Starting a job [#starting-a-job]
A Garment Recolor job takes `N` product images, recolors one garment across all of them, and produces `N × num_variations` outputs:
* Every input image is recolored, so the same garment looks the same across the whole set.
* Set `num_variations` (1-8) to generate multiple variations per input image.
* You define the recolor target with a `reference_image`, a `reference_text` (colour or pattern), an `instruction`, or any combination. **At least one is required.**
**Credit cost:** 3 credits per output at `1024`, 5 at `2048`, 10 at `4096`, billed per output.
```python lineNumbers
import requests
api_url = "https://v2.api.piktid.com"
access_token = "your_access_token"
project_id = "abc123..."
file_ids = ["img_001...", "img_002...", "img_003..."] # Same garment across several shots
reference_image = "img_ref..." # Optional: an uploaded colour/pattern swatch
response = requests.post(
api_url + "/garment-recolor",
headers={"Authorization": "Bearer " + access_token},
json={
"project_id": project_id,
"images": file_ids,
"garment_label": "t-shirt", # Optional: auto-detected when omitted
"reference_image": reference_image, # Recolor target. Or use reference_text / instruction
"num_variations": 2,
"model": "auto", # Optional: see "Generation options" below
"processing_size": 2048, # Optional: 1024 | 2048 | 4096
"post_process": True, # Optional: finishing pass (on by default)
"use_anchor": True, # Optional: keep the set consistent (on by default)
},
).json()
job_id = response["job_id"]
total_outputs = response["total_outputs"]
print(f"Job started: {job_id} ({total_outputs} outputs)")
```
```jsonc title="Response"
{
"job_id": "job_abc123...", // JOB_ID [!code highlight]
"status": "pending",
"message": "Garment recolor job created and queued",
"total_outputs": 6 // len(images) × num_variations [!code highlight]
}
```
### Recolor target [#recolor-target]
Tell the job which garment to recolor and what to recolor it to. You must provide **at least one** of `reference_image`, `reference_text`, or `instruction`. They can be combined — for example, a reference image to set the colour and an instruction to refine it.
| Parameter | Type | Required | Description |
| ----------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------- |
| `garment_label` | string | no | Which garment to recolor, e.g. `"t-shirt"`, `"jacket"`, `"hat"`. Automatically detected when omitted. |
| `reference_image` | string | no\* | File ID of a colour/pattern reference image to recolor toward. Upload it like any other image. |
| `reference_text` | string | no\* | A colour or pattern described as text, e.g. a hex code `"#2E5A3B"` or a colour name like `"forest green"`. |
| `instruction` | string | no\* | Free-form recolor instruction, e.g. `"make it forest green"`. |
\* At least one of `reference_image`, `reference_text`, or `instruction` must be provided.
### Output options [#output-options]
| Parameter | Type | Default | Description |
| ----------------- | -------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------- |
| `num_variations` | integer (1-8) | `1` | Number of variations to generate per input image. Total outputs = `len(images) × num_variations`. |
| `processing_size` | `1024` \| `2048` \| `4096` | `2048` | Output resolution in pixels. Drives credit cost: 3 / 5 / 10 credits per output. Larger sizes cost more and take longer. |
### Generation options [#generation-options]
| Parameter | Type | Default | Description |
| ------------ | -------------------------------------------------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `model` | `"auto"` \| `"nano_banana_2"` \| `"nano_banana_pro"` \| `"seedream"` \| `"seedream_5_pro"` \| `"gpt_image"` \| `"gpt_image_2_5"` | `"auto"` | Which engine generates outputs. `auto` runs the default engine (Google's Nano Banana 2) with a safety fallback if content is refused. `nano_banana_2` is Google's faster image model; `nano_banana_pro` is Google's higher-quality image model; `gpt_image` and `gpt_image_2_5` are OpenAI's GPT Image 2 and GPT Image 2.5; both render legible garment lettering, and 2.5 is markedly faster. `seedream` and `seedream_5_pro` are ByteDance's Seedream 4.5 and Seedream 5.0 Pro; both accept reference images. Specifying an engine disables the fallback. |
| `use_anchor` | boolean | `true` | Keeps the recolored garment visually consistent and cohesive across every output in the set. Only has an effect when the job produces more than one output. Defaults to on. |
```jsonc title="Request with generation options"
{
"project_id": "abc123...",
"images": ["img_001...", "img_002..."],
"instruction": "make it forest green",
"model": "nano_banana_2", // [!code highlight]
"use_anchor": true // [!code highlight]
}
```
### AI-generated disclosure watermark [#ai-generated-disclosure-watermark]
Set `add_ai_watermark` to `true` to bake a small "AI-generated" disclosure mark into the bottom-right corner of each output. This is irreversible, as it becomes part of the clean output file.
| Parameter | Type | Default | Description |
| ------------------ | ------- | ------- | ----------------------------------------------------------------------- |
| `add_ai_watermark` | boolean | `false` | Bake an "AI-generated" disclosure mark into each output (irreversible). |
```jsonc title="Request with AI watermark"
{
"project_id": "abc123...",
"images": ["img_001...", "img_002..."],
"instruction": "make it forest green",
"add_ai_watermark": true // [!code highlight]
}
```
### Post-processing [#post-processing]
`post_process` is enabled by default and applies a finishing pass that keeps the recolored garment tones consistent across the whole set. Set it to `false` to skip that step. When enabled, the job status includes `post_processing_status` to track this additional step.
```jsonc title="Request without post-processing"
{
"project_id": "abc123...",
"images": ["img_001...", "img_002..."],
"instruction": "make it forest green",
"post_process": false // [!code highlight]
}
```
## Tracking progress [#tracking-progress]
Use either SSE or webhooks to receive notifications for job updates.
```python lineNumbers
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"))
```
```python lineNumbers
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)
```
```jsonc title="Response"
[
{
"id": 12345,
"name": "batch_edit", // Notification type [!code highlight]
"timestamp": 1702819200.0,
"data": { // Job-specific data [!code highlight]
"id_task": "job_abc123...",
"status": "completed",
"total_images": 6,
"processed_images": 6
}
}
]
```
Use [`DELETE /notifications/{id}`](/docs/v2/notifications/notifications_delete__delete) after processing events so they are not replayed on reconnect.
## Retrieving results [#retrieving-results]
Once the job is complete, retrieve the recolored images.
```python lineNumbers
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']}")
```
```jsonc title="Response"
{
"job_id": "job_abc123...",
"job_type": "garment_recolor",
"status": "completed",
"results": [
{
"image_index": 0,
"group_index": 0,
"output": {
"full_size": "https://...", // Result image URL [!code highlight]
"thumbnail": "https://..."
},
"model_used": "nano_banana_2", // Engine that produced this output [!code highlight]
"status": "completed"
},
{
"image_index": 1,
"group_index": 0,
"output": {
"full_size": "https://...",
"thumbnail": "https://..."
},
"model_used": "nano_banana_2",
"status": "completed"
}
],
"summary": {
// Job statistics
}
}
```
### Bulk download [#bulk-download]
For bulk downloads, generate a temporary download URL that packages all results into a ZIP file.
```python lineNumbers
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)
```
```jsonc title="Response"
{
"download_url": "https://v2.api.piktid.com/download/token123...",
"expires": "2024-12-17T11:00:00Z" // URL expiration time [!code highlight]
}
```
## Checking job status [#checking-job-status]
You can also check the job status directly without waiting for notifications.
```python lineNumbers
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']}")
```
```jsonc title="Response"
{
"job_id": "job_abc123...",
"job_type": "garment_recolor",
"status": "processing",
"progress": 50.0,
"total_images": 6,
"processed_images": 3,
"should_post_process": true,
"post_processing_status": null,
"created_at": "2024-12-17T10:00:00Z",
"updated_at": "2024-12-17T10:05:00Z"
}
```
## Error handling [#error-handling]
Jobs may fail due to various reasons. Check the `error_message` field in the job status or results.
```python lineNumbers
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']}")
```
### Common errors at job creation [#common-errors-at-job-creation]
`POST /garment-recolor` returns the following errors before the job is queued:
| HTTP | Meaning |
| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400 | No recolor target (provide at least one of `reference_image`, `reference_text`, `instruction`), unsupported `model`, or a missing/invalid image or reference image. |
| 402 | Insufficient credits. Response body includes `required_credits`, `in_progress_credits`, `user_credits`. |
| 403 | Output size exceeds your plan's policy, or you do not have access to the target project. |
| 404 | Project not found. |
| 429 | Concurrent job limit reached. Non-enterprise accounts cap at 5 active jobs across model-swap, flat-2-model, create-packshot, and garment-recolor combined. The endpoint is also rate-limited to 5 requests per minute per user. |
```jsonc title="402 Insufficient credits response"
{
"error": "Insufficient credits",
"required_credits": 30.0,
"in_progress_credits": 5.0,
"user_credits": 12.0
}
```
# Image playground
URL: /docs/v2/image-playground
Applies to API version: v2
Description: 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](/docs/v2/auth) 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](/docs/v2/model-swap), [Flat lay on model](/docs/v2/flat-lay), [Create packshot](/docs/v2/create-packshot), or [Garment recolor](/docs/v2/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 [#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](#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](#starting-a-job).
***
Track progress with the notifications stream (SSE) or a webhook, and stop when your image is ready. See [Tracking progress](#tracking-progress).
***
Fetch the generated image. See [Retrieving results](#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 [#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`.
```python lineNumbers
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 [#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 [#generate-from-a-prompt]
```python lineNumbers
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}")
```
```jsonc title="Response"
{
"job_id": "job_abc123...", // JOB_ID [!code highlight]
"conversation_id": "job_abc123...", // same value as job_id [!code highlight]
"status": "processing"
}
```
### Edit or compose existing images [#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.
```python lineNumbers
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], # [!code highlight]
"size": "2K",
},
).json()
```
### Use a saved identity [#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`](/docs/v2/identities/list_identities__get).
```python lineNumbers
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"], # [!code highlight]
"size": "2K",
},
).json()
```
### Request parameters [#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_id`s 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"` \| `"gpt_image_2_5"` \| `"seedream"` \| `"seedream_5_pro"` | `"auto"` | Generation engine. `auto` runs the default engine with a safety fallback. `seedream` and `seedream_5_pro` are ByteDance's Seedream 4.5 and Seedream 5.0 Pro; both accept reference images. `gpt_image` and `gpt_image_2_5` are OpenAI's GPT Image 2 and GPT Image 2.5; both render legible garment lettering, and 2.5 is markedly faster. Specifying an engine disables the fallback. |
| `size` | `"1K"` \| `"2K"` \| `"4K"` | `"2K"` | Quality / processing size. Drives resolution and the per-output price (see [Credits](#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). Requires the custom-dimensions entitlement (Pro). |
| `conversation_id` | string | `null` | Continue a prior generation instead of starting fresh (see [Continuing a conversation](#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 [#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](/docs/v2/concepts/credits).
## Continuing a conversation [#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.
```python lineNumbers
# 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 [!code highlight]
},
).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 [#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.
```python lineNumbers
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"))
```
```python lineNumbers
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)
```
```jsonc title="image_result notification"
{
"id": 12345,
"name": "image_result", // Per-image event [!code highlight]
"timestamp": 1702819200.0,
"data": {
"id_task": "job_abc123...", // your job_id [!code highlight]
"image_index": 0, // which generation (0 = first)
"group_index": 0, // which variation
"status": "completed",
"output_link": "https://...", // full-size result URL [!code highlight]
"output_thumbnail_link": "https://..."
}
}
```
Use [`DELETE /notifications/{id}`](/docs/v2/notifications/notifications_delete__delete) after processing events so they are not replayed on reconnect.
## Retrieving results [#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.
```python lineNumbers
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']}")
```
```jsonc title="Response"
{
"job_id": "job_abc123...",
"job_type": "edit_chat",
"status": "completed",
"results": [
{
"image_index": 0, // generation index [!code highlight]
"group_index": 0, // variation index
"version": 0,
"output": {
"full_size": "https://...", // result image URL [!code highlight]
"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 [#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](/docs/v2/concepts/errors) for the full error model.
# Introduction
URL: /docs/v2
Applies to API version: v2
Description: General information about the APIs
Welcome to the documentation for the PiktID API version 2!
## Getting started [#getting-started]
To use the APIs, you will need to [sign up](https://app.on-model.com/signup) first. After confirming your email address, you can proceed to the authentication overview, where you'll learn how to authenticate your HTTP requests with our APIs.
Create a new account to access apps and APIs
## Using the APIs [#using-the-apis]
PiktID provides the following APIs for bulk fashion image processing and generation:
# Model swap
URL: /docs/v2/model-swap
Applies to API version: v2
Description: Replace model identities in multiple PDPs at once
You will need an API token to send HTTP requests. See [Authentication](/docs/v2/auth) for instructions.
## Quick start [#quick-start]
Create a project to organize your images. The `project_id` will be used in subsequent requests. See [Creating a project](#creating-a-project) for details.
***
Upload one or more PDP 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](#uploading-images) for details.
***
Start a model swap job by providing the identity to swap onto the images and the list of uploaded file IDs. See [Starting a job](#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](#tracking-progress) for details.
## Uploading identities [#uploading-identities]
Before starting a swap job, you need an `identity_code`. You can either use an existing identity from your gallery or upload a new one.
```python lineNumbers
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}")
```
```jsonc title="Response"
{
"identity_code": "id_xyz...", // IDENTITY_CODE [!code highlight]
"name": "Model A",
"face_detected": true,
"success": true,
// ...
}
```
You can list existing identities using:
## Creating a project [#creating-a-project]
```python lineNumbers
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-campaign"},
).json()
project_id = response["project_id"]
project_name = response["project_name"]
```
```jsonc title="Response"
{
"project_id": "abc123...", // PROJECT_ID [!code highlight]
"project_name": "my-campaign"
}
```
## Uploading images [#uploading-images]
You can repeat this process for each image you want to process. All `file_id` values must be collected to start a job.
```python lineNumbers
import requests
api_url = "https://v2.api.piktid.com"
access_token = "your_access_token"
project_name = "my-campaign"
image_path = "path/to/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": "model-photo-1.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}")
```
```jsonc title="Response"
{
"upload_url": "https://s3...", // Pre-signed PUT URL
"download_url": "https://...",
"project_id": "abc123...",
"project_name": "my-campaign",
"file_id": "img_001...", // FILE_ID [!code highlight]
"filename": "model-photo-1.jpg",
"content_type": "image/jpeg"
}
```
The `upload_url` is only valid for a limited time. Upload the image immediately after receiving the response.
## Starting a job [#starting-a-job]
```python lineNumbers
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 = ["img_001...", "img_002...", "img_003..."]
response = requests.post(
api_url + "/model-swap",
headers={"Authorization": "Bearer " + access_token},
json={
"identity_code": identity_code,
"project_id": project_id,
"images": file_ids,
"post_process": False, # Optional: enable post-processing
"swap_options": {
"model": "auto", # Optional: see "Swap options" below
"num_variations": 1, # Optional: 1–4 variations per input image
"use_anchor": False, # Optional: opt-in consistency across the set
},
},
).json()
job_id = response["job_id"]
print(f"Job started: {job_id}")
```
```jsonc title="Response"
{
"job_id": "job_abc123...", // JOB_ID [!code highlight]
"status": "pending",
"message": "Job created successfully"
}
```
### Swap options [#swap-options]
Fields inside the `swap_options` object that control how the swap runs.
| Parameter | Type | Default | Description |
| ---------------- | ----------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `model` | `"auto"` \| `"onda"` \| `"nano_banana_2"` | `"auto"` | Which swap engine runs. `auto` and `nano_banana_2` both swap via Google's Nano Banana 2 image model; `onda` routes the job to PiktID's proprietary Onda engine, which preserves the original garment pixels more literally but runs slower. |
| `num_variations` | integer (1–4) | `1` | Number of variation outputs produced per input image. |
| `use_anchor` | boolean | `false` | Keeps the model looking like the same person across every output in the set, with steadier skin tone from shot to shot. Only has an effect when the job produces more than one output, and is ignored when `model` is `"onda"`. Off by default. |
| `anchor_index` | integer | auto | Which input image (zero-indexed into `images`) anchors the set when `use_anchor` is `true`. Must satisfy `0 <= anchor_index < len(images)`. Omit it and a suitable image is chosen for you. |
```jsonc title="Request with swap options"
{
"identity_code": "id_xyz...",
"project_id": "abc123...",
"images": ["img_001...", "img_002..."],
"swap_options": {
"model": "nano_banana_2", // [!code highlight]
"num_variations": 2, // [!code highlight]
"use_anchor": true // [!code highlight]
}
}
```
`use_anchor` costs a little wall-clock time before the batch fans out, so leave it off for one-off swaps and turn it on when a set has to look consistent, for example a full PDP sequence of the same product.
The legacy `use_alternative_method` flag is still accepted for backward compatibility and is mapped internally to `model: "nano_banana_2"`. New integrations should use `model` directly.
### Post-processing [#post-processing]
Set `post_process` to `true` (default) in the job request to enable automatic post-processing of results. The job status will include `post_processing_status` to track this additional step.
```jsonc title="Request with post-processing"
{
"identity_code": "id_xyz...",
"project_id": "abc123...",
"images": ["img_001...", "img_002..."],
"post_process": true // [!code highlight]
}
```
### AI-generated disclosure watermark [#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 `swap_options`. 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.
```jsonc title="Request with AI disclosure"
{
"identity_code": "id_xyz...",
"project_id": "abc123...",
"images": ["img_001...", "img_002..."],
"swap_options": {
"add_ai_watermark": true // [!code highlight]
}
}
```
## Tracking progress [#tracking-progress]
Use either SSE or webhooks to receive notifications for job updates.
```python lineNumbers
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"] == "completed":
print("Job completed!")
break
if notification["name"] == "error":
raise RuntimeError(str(data))
```
```python lineNumbers
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"] == "completed":
print("Job completed!")
elif notification["name"] == "error":
print(f"Error: {data}")
return "", 204
app.run(port=8000)
```
```jsonc title="Response"
[
{
"id": 12345,
"name": "completed", // Notification type [!code highlight]
"timestamp": 1702819200.0,
"data": { // Job-specific data [!code highlight]
"job_id": "job_abc123...",
"status": "completed"
}
}
]
```
Use [`DELETE /notifications/{id}`](/docs/v2/notifications/notifications_delete__delete) after processing events so they are not replayed on reconnect.
## Retrieving results [#retrieving-results]
Once the job is complete, retrieve the processed images.
```python lineNumbers
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['url']}")
```
```jsonc title="Response"
{
"job_id": "job_abc123...",
"job_type": "model_swap",
"status": "completed",
"results": [
{
"image_index": 0,
"url": "https://...", // Result image URL [!code highlight]
"model_used": "nano_banana_2", // Engine that produced this output [!code highlight]
"status": "completed"
},
{
"image_index": 1,
"url": "https://...",
"model_used": "nano_banana_2",
"status": "completed"
}
],
"summary": {
// Job statistics
}
}
```
Each result carries a `model_used` string indicating which engine actually generated the image. Inspect this field when you need to know per output which model was used.
```python
for result in response["results"]:
print(f"Image {result['image_index']}: model_used = {result.get('model_used')}")
```
### Bulk download [#bulk-download]
For bulk downloads, generate a temporary download URL that packages all results into a ZIP file.
```python lineNumbers
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)
```
```jsonc title="Response"
{
"download_url": "https://v2.api.piktid.com/download/token123...",
"expires": "2024-12-17T11:00:00Z" // URL expiration time [!code highlight]
}
```
# Coordinates and bounding boxes
URL: /docs/v1/concepts/coordinates
Applies to API version: v1
Description: Common concepts and functionality shared between services
Many endpoints will return a list containing bounding boxes for detected faces. The bounding boxes are served in the following format:
Python
Typescript
```python
from dataclasses import dataclass
@dataclass
class Coordinates:
id: int # Points to a `FACE_ID`
boxHeight: float # Height of bounding box as percentage of image height (0-1)
boxWidth: float # Width of bounding box as percentage of image width (0-1)
centerX: float # Horizontal coordinate of box center as percentage of image width (0-1)
centerY: float # Vertical coordinate of box center as percentage of image height (0-1)
cornerX: float # Horizontal coordinate of top-left corner as percentage of image width (0-1)
cornerY: float # Vertical coordinate of top-left corner as percentage of image height (0-1)
```
```ts
interface Coordinates {
/** Points to a `FACE_ID` */
id: number
/** Height of bounding box as percentage of image height (0-1) */
boxHeight: number
/** Width of bounding box as percentage of image width (0-1) */
boxWidth: number
/** Horizontal coordinate of box center as percentage of image width (0-1) */
centerX: number
/** Vertical coordinate of box center as percentage of image height (0-1) */
centerY: number
/** Horizontal coordinate of top-left corner as percentage of image width (0-1) */
cornerX: number
/** Vertical coordinate of top-left corner as percentage of image height (0-1) */
cornerY: number
}
```
As such, an example of such data returned by the `/api/consistent_identities/upload_target` endpoint could be the following:
You can hover over the JSON fields to see descriptions of the data they contain.
```ts twoslash
interface Coordinates {
/** Points to a `FACE_ID` */
id: number
/** Height of bounding box as percentage of image height (0-1) */
boxHeight: number
/** Width of bounding box as percentage of image width (0-1) */
boxWidth: number
/** Horizontal coordinate of box center as percentage of image width (0-1) */
centerX: number
/** Vertical coordinate of box center as percentage of image height (0-1) */
centerY: number
/** Horizontal coordinate of top-left corner as percentage of image width (0-1) */
cornerX: number
/** Vertical coordinate of top-left corner as percentage of image height (0-1) */
cornerY: number
}
type Obj = {
coordinates_list: Coordinates[]
}
const obj: Obj = {
// ---cut-before---
// ...
"coordinates_list": [
{
id: 0,
boxHeight: 0.273,
boxWidth: 0.338,
centerX: 0.197,
centerY: 0.309,
cornerX: 0.028,
cornerY: 0.173
},
{
id: 1,
boxHeight: 0.182,
boxWidth: 0.233,
centerX: 0.539,
centerY: 0.459,
cornerX: 0.422,
cornerY: 0.367
},
],
// ...
// ---cut-after---
}
```
The following example outlines parsing coordinates data from a response in Python. See [Uploading a target image](/docs/v1/swap#uploading-a-target-image) to understand how the `response` object is obtained.
```python lineNumbers
# Refer to the SwapID docs to see how to send a valid request.
# The "response" object is taken from that code.
faces = response.get("faces")
coordinates = []
for coord in faces.get("coordinates_list", []):
parsed_coords = Coordinates(**coord)
coordinates.append(parsed_coords)
print(coordinates)
```
Coordinates will always start from the **top-left corner**, i.e. the coordinate `(0, 0)` is the **topmost** and **leftmost** pixel at the corner of the image.
As a simple example, let's consider an image 1000 by 1000 pixel was uploaded, generating the response above.
All parameters are returned as percentages of the origin image's dimensions so they can be easily converted to pixels:
```python
image_height = 1000
image_width = 1000
coords = coordinates_list[0]
boxHeight = cooords.boxHeight * image_height # 273 px
boxWidth = cooords.boxWidth * image_width # 338 px
cornerY = cooords.cornerY * image_height # 173 px
cornerX = cooords.cornerX * image_width # 29 px
```
# Image ID
URL: /docs/v1/concepts/image-id
Applies to API version: v1
Description: Common concepts and functionality shared between services
All image IDs expire after 24 hours of being created. This action cannot be undone.
You can think of the image ID as a unique identifier for the currently ongoing editing process.
To make the concept clearer, follow this diagram outlining the [change expression](/docs/v1/change-expression) process:
# Ask questions
URL: /docs/v1/tag/ask-questions
Applies to API version: v1
Description: Ask our model a question about your images
You will need an API token to send HTTP requests. See [Authentication](/docs/v1/auth) for instructions.
## Quick start [#quick-start]
Firstly, you'll need to convert the image you want to process to a Base64 string (with UTF-8 encoding).
You can then send it in the request along with the question you want to ask.
```jsonc title="Request"
{
"base64": "/9j/4QIiRX...",
"question": "Is the image good for a blog post?"
}
```
Please note that we only support the following formats: **WEBP**, **JPEG** and **PNG**.
```jsonc title="Response"
{
"data": "Yes, the image of a man wearing a crown and glasses is a good choice for a blog post..."
}
```
## Asking questions on images [#asking-questions-on-images]
Our advanced Vision Language Model (**VLM**) can easily answer questions regarding images you send to our API. Try asking anything you want!
```python lineNumbers
import requests
api_url = "https://api.piktid.com/api"
access_token = "your_access_token"
target_path = "path_to_image"
question = "Is this image the right format for a poster?"
def open_image_from_file_to_base64(image_path):
with open(image_path, "rb") as image_file:
data_base64 = base64.b64encode(image_file.read())
return data_base64
response = requests.post(
api_url + "/getQuestion",
headers={"Authorization": "Bearer " + access_token},
json={
"base64": open_image_from_file_to_base64(target_path).decode("utf8"),
"question": question,
},
).json()
print(response["data"])
```
# Describe an image
URL: /docs/v1/tag/describe-image
Applies to API version: v1
Description: Extract useful descriptions and visual aspects of your image
You will need an API token to send HTTP requests. See [Authentication](/docs/v1/auth) for instructions.
## Quick start [#quick-start]
Firstly, you'll need to upload the image you want to process. These parameters must be supplied as **multipart form data** and only either one of `file` or `url` must be used:
| Parameter | Example | Description |
| --------- | ------------------------------- | ------------------------------------------------------------------------ |
| `file` | `@file.png` | The image file as form data. |
| `url` | `https://example.com/image.png` | A static link to an image on the web. Our servers will fetch it for you. |
Please note that we only support the following formats: **WEBP**, **JPEG** and **PNG**.
```jsonc title="Response"
{
"data": {
"long_description": "A man wearing a crown and glasses is standing in a room.",
"short_description": "A man wearing a crown and glasses.",
"colors": "['brown', 'orange', 'white', 'black', 'green']",
"objects_present": "['crown', 'glasses', 'man']",
"mood": "Humorous"
}
}
```
## Describing images [#describing-images]
Our advanced Vision Language Model (**VLM**) is able to describe in detail any image you send to our API.
The response will contain:
* Both a long and short description of the image in natural language
* Dominant colors present in the image
* Common objects' presence
* General mood of the picture
```python lineNumbers
import requests
from pprint import pprint
api_url = "https://api.piktid.com/api"
access_token = "your_access_token"
target_path = "path_to_image"
with open(target_path, "rb") as target:
response = requests.post(
api_url + "/getCaption/v2",
headers={"Authorization": "Bearer " + access_token},
files={
"file": target,
},
).json()
pprint(response)
```
# Assets
URL: /docs/v2/concepts/assets
Applies to API version: v2
Description: Manage the uploaded images you reuse across jobs
An **asset** is an uploaded image stored in your library so you can browse, tag, reuse, and clean up the images you work with.
Every image you send through [`POST /upload`](/docs/v2/model-swap#uploading-images) becomes an asset. The `file_id` returned by the upload is the same identifier you pass to a service through the `images` field, so your library and your jobs stay connected.
## Key fields [#key-fields]
| Field | Description |
| ---------------------------------- | ------------------------------------------------------------------------------- |
| `id` | Stable identifier for the asset. |
| `filename` | Original filename of the uploaded image. |
| `url` | URL used to access the full image. |
| `thumbnail_small`, `thumbnail_big` | Convenience thumbnail URLs, when available. |
| `dimensions` | `[width, height]` in pixels, when available. |
| `tags` | Labels applied to the asset, drawn from a fixed set. |
| `user_tags` | Tags you or your team applied to the asset. See [Tags](/docs/v2/concepts/tags). |
| `visibility` | Who can see the asset: `private`, `shared`, or `default`. |
| `styling_note` | Free-form styling instruction kept with the asset. |
| `created_by` | Username of the uploader. |
| `created_at` | ISO 8601 timestamp of when the asset was created. |
## Tags [#tags]
Tags help you group and filter your library. Each asset accepts up to 10 tags, chosen from a fixed set:
| Tag | Typical use |
| ------------ | ------------------------------------- |
| `model` | Photos of a person or model. |
| `product` | Standalone product or garment shots. |
| `background` | Scenes and backdrops. |
| `other` | Anything that does not fit the above. |
Only the owner of an asset can change its tags. When listing assets you can narrow results with `include_tags` (keep assets that have all of the listed tags) and `exclude_tags` (drop assets that have any of the listed tags).
These fixed tags are separate from [Tags](/docs/v2/concepts/tags), the labels you create yourself. Both live on an asset at the same time: the fixed ones under `tags`, yours under `user_tags`.
## Visibility and sharing [#visibility-and-sharing]
Visibility controls who can see an asset, and lines up with the `ownership` filter on the list endpoint.
| Value | `ownership` filter | Who can see it |
| --------- | ------------------ | ---------------------------------------- |
| `private` | `mine` | Only you. |
| `shared` | `shared` | You and the other members of your group. |
| `default` | `default` | Everyone, provided by the platform. |
If you belong to a group, listing with `ownership=shared` surfaces assets uploaded by your teammates alongside your own.
## Styling notes [#styling-notes]
An asset can carry a free-form `styling_note`: a short instruction such as "keep the sleeves rolled up" that you want remembered for this image. Set or clear it with the styling note endpoint. Sending an empty value removes it. The note is pre-filled in the picker the next time you reuse the asset in a job, so recurring guidance follows the image instead of being retyped.
## Detected attributes [#detected-attributes]
When an image is uploaded, the platform can attach a set of auto-detected attributes under `detected`. These are best-effort hints and may be absent for older or ambiguous images.
| Field | Description |
| ------------------- | ------------------------------------------------------------------------- |
| `has_person` | Whether a person was detected in the image. |
| `has_clothing` | Whether clothing was detected in the image. |
| `person_attributes` | High-level descriptors of a detected person, such as `Adult` or `Female`. |
| `clothing_items` | Detected garments, such as `Shirt` or `Pants`. |
| `categories` | Broad content categories, such as `Apparel and Accessories`. |
## Good to know [#good-to-know]
* Only the original uploader can delete an asset. You can remove a single asset or delete in bulk, up to 100 assets per request.
* Bulk deletion is rate limited to 3 requests per second. Exceeding it returns `429 Too Many Requests`. A bulk delete reports how many assets were removed and lists any that could not be deleted.
* Listing assets is paginated, with up to 100 results per page, and `search` matches against tags, filename, and project name.
* Storing assets does not consume [credits](/docs/v2/concepts/credits). Credits are spent when you run a job or extract a preset, not when you upload or keep images.
## Where assets are used [#where-assets-are-used]
Uploaded assets are the starting point for the main services:
* [Model swap](/docs/v2/model-swap)
* [Flat lay on model](/docs/v2/flat-lay)
* [Create an identity](/docs/v2/create-identity) (as reference images through `input_assets`)
## API endpoints [#api-endpoints]
# Credits
URL: /docs/v2/concepts/credits
Applies to API version: v2
Description: Understand cost calculation, pre-checks, and billing ownership
Every processing job consumes **credits** from your account balance. Credits are checked before a job starts, and requests are rejected when the balance is insufficient.
## Billing ownership [#billing-ownership]
By default, credits are deducted from your personal balance. If you belong to a group with shared billing, credits are deducted from the group owner's balance.
## Insufficient credits response [#insufficient-credits-response]
If you do not have enough credits, the API returns `402 Payment Required` with details about credits needed for the request.
## Checking your balance [#checking-your-balance]
Use the billing endpoint to read your current available credits.
## API endpoints [#api-endpoints]
# Errors
URL: /docs/v2/concepts/errors
Applies to API version: v2
Description: Error response formats, validation details, and reference codes
All error responses are returned as JSON.
## Error response types [#error-response-types]
The API returns one of three common error shapes.
### HTTP errors [#http-errors]
Standard HTTP errors return a short message in the `error` field.
```json title="404 Not Found"
{
"error": "Not Found"
}
```
### Validation errors [#validation-errors]
Request validation errors return `422 Unprocessable Entity` with a structured `detail` array.
```json title="422 Unprocessable Entity"
{
"error": "Invalid request",
"detail": [ // [!code highlight]
{ // [!code highlight]
"field": "images.0.url", // [!code highlight]
"message": "Field required", // [!code highlight]
"in": "body" // [!code highlight]
} // [!code highlight]
] // [!code highlight]
}
```
Each item in `detail` includes:
| Field | Description |
| --------- | ---------------------------------------------------------------------------- |
| `field` | Dot path to the invalid value |
| `message` | Human-readable validation message |
| `in` | Request location where validation failed: `query`, `body`, `form`, or `path` |
### Internal errors [#internal-errors]
Unexpected failures return `500 Internal Server Error` with a generic message and a `reference` code.
```json title="500 Internal Server Error"
{
"error": "An internal error occurred.",
"reference": "a1b2c3d4" // [!code highlight]
}
```
Reference codes help map a client-visible error to server-side logs without exposing internal details.
If you contact support about a `500` error, include the `reference` value from the response.
## Handling errors in code [#handling-errors-in-code]
The example below shows one way to parse known error response shapes.
```python lineNumbers
import requests
def read_api_error(response: requests.Response) -> str:
try:
payload = response.json()
except ValueError:
return f"HTTP {response.status_code}: unexpected non-JSON error"
if response.status_code == 422 and "detail" in payload:
first = payload["detail"][0] if payload["detail"] else {}
location = first.get("in", "body")
field = first.get("field", "unknown")
message = first.get("message", "Invalid request")
return f"Validation error ({location}.{field}): {message}"
if response.status_code >= 500 and "reference" in payload:
return f"Server error. Reference code: {payload['reference']}"
return payload.get("error", f"HTTP {response.status_code} error")
```
# Groups
URL: /docs/v2/concepts/groups
Applies to API version: v2
Description: Collaborate with your team through shared resources and billing
A **group** is a team of users who share access to resources and can optionally share a billing destination. Groups let you collaborate without duplicating identities, templates, projects, or presets across accounts.
Every group has a unique name (**case-insensitive**) and at least one member. The user who creates a group becomes its owner automatically.
## Roles [#roles]
Each membership has a role that determines what the user can do within the group.
| Role | Capabilities |
| -------- | --------------------------------------------------------------------------------------------- |
| `owner` | Full control: invite and remove members, change roles, delete the group, manage group billing |
| `admin` | Invite and remove members |
| `member` | Access resources shared with the group |
Each group has exactly one owner. Promoting another member to owner automatically demotes the current owner to admin.
## Creating a group [#creating-a-group]
Call the create endpoint with a unique group name. You become the owner.
Non-enterprise accounts can create a limited number of groups. Once the cap is reached, the create endpoint returns `403`.
## Inviting members [#inviting-members]
Owners and admins can invite users by email. The invitee receives an email containing a join link with a time-limited token. Invitations can optionally request a billing change, prompting the invitee to set the group as their billing destination when they accept.
## Joining a group [#joining-a-group]
The invitee accepts the invitation by calling the join endpoint with the token from the email. The request is idempotent: joining a group you already belong to returns success without creating a duplicate membership.
An invitation is bound to the email it was sent to. Accepting from a different account is rejected.
## Shared resources [#shared-resources]
Groups are the unit of sharing across the platform. When you share a resource with a group, every member gains read access to it.
The following resources support group-based sharing:
* **Identities**: share with one or more groups via the identity share endpoint.
* **Presets**: share with one or more groups via the preset share endpoint.
* **Templates**: share every preset they hold with a group in one call. See [Templates](/docs/v2/concepts/templates).
* **Tags**: visible to every member of your groups automatically, with no share step. See [Tags](/docs/v2/concepts/tags).
* **Projects**: list endpoints can include projects created by members of your groups.
Unsharing a resource from a group removes access for all its members.
## Shared billing [#shared-billing]
Members can route their usage to a group instead of their own account by setting the group as their billing destination. When they do, credits for their jobs are charged to the group owner rather than the individual member.
See [Credits](/docs/v2/concepts/credits) for the full billing model.
Group owners cannot set their own billing destination to a group they own. If you currently bill to a group, you must reset your billing destination before creating a new group.
## Removing members and deleting groups [#removing-members-and-deleting-groups]
* Owners can remove any member, change any member's role, and delete the group.
* Admins can invite and remove members but cannot change roles or delete the group.
* Members cannot remove other members.
* A user cannot change their own role.
When a group is deleted, all memberships are removed and any member who was billing to that group has their billing destination reset to their own account.
## API endpoints [#api-endpoints]
# Identities
URL: /docs/v2/concepts/identities
Applies to API version: v2
Description: Upload, generate, preprocess, and reuse model identities across jobs
An **identity** represents the model/person used by generation jobs and is referenced with `identity_code`.
There are two ways to add an identity to your library:
* **Upload** an existing photo of a person. The system validates that it contains exactly **one clearly visible face** and starts preprocessing automatically.
* **Generate** a new identity from a brief or a reference image, then promote one of the drafts. See [Create an identity](/docs/v2/create-identity) for the full walkthrough.
Identity names must be unique per user. If you upload (or promote) with an existing `name`, the API returns `409`.
## Processing status [#processing-status]
Identities move through a preprocessing lifecycle (`pending`, `processing`, `completed`, `failed`).
Useful fields during and after preprocessing:
| Field | Description |
| ---------------------- | ----------------------------------------------------------------- |
| `preprocessing_status` | Current status: `pending`, `processing`, `completed`, or `failed` |
| `preprocessing_time` | How long preprocessing took (seconds) |
| `quality_score` | Quality rating of the processed identity |
| `error_message` | Reason for failure, if any |
## Visibility and sharing [#visibility-and-sharing]
Identities can be:
* `private`: only you can see it
* `shared`: visible to members of your groups
* `default`: platform-provided identities available based on your plan
## Generating identities [#generating-identities]
Beyond uploading photos, you can generate brand-owned identities from a brief or a reference image. The flow runs in two stages:
1. **Creation job**: `POST /identity/create` accepts a list of structured instructions (or a free-form prompt) and starts an async job that produces N draft images. Drafts are not identities yet: they are returned by the job results endpoint, each carrying an `image_result_id`. They consume credits but no identity slot.
2. **Promotion**: pick the draft you want and call `POST /identity/promote-generated` with its `image_result_id`. The response returns the new `identity_code` along with a `preprocessing_job_id` you can track. This step charges 50 credits and counts toward your identity slot cap.
Once preprocessing completes, the new identity behaves exactly like an uploaded one: usable in Model Swap, Flat-to-Model, and any other job that takes an `identity_code`.
See [Create an identity](/docs/v2/create-identity) for the end-to-end walkthrough with Python examples.
## API endpoints [#api-endpoints]
# Jobs
URL: /docs/v2/concepts/jobs
Applies to API version: v2
Description: Understand job states, progress tracking, and per-image outputs
A **job** is an asynchronous processing request. Jobs are created when you use [Model swap](/docs/v2/model-swap) or [Flat lay on model](/docs/v2/flat-lay).
## Job lifecycle [#job-lifecycle]
Core fields:
| Field | Description |
| ------------------ | -------------------------------------- |
| `status` | Current job status (see diagram above) |
| `total_images` | Number of images submitted |
| `processed_images` | Number of images completed so far |
| `progress` | Completion percentage (0–100) |
| `error_message` | Reason for failure, if any |
## Image results [#image-results]
Each job produces one or more output images. Use the results endpoint to retrieve them.
If a job processes multiple input images, each result includes an `image_index` so you can map outputs back to the original input.
## Abort and regenerate [#abort-and-regenerate]
* You can abort a job that is still in progress.
* You can regenerate a specific output image without rerunning the entire job.
## API endpoints [#api-endpoints]
# Notifications
URL: /docs/v2/concepts/notifications
Applies to API version: v2
Description: Track progress via polling, SSE streams, and webhook fan-out
Notifications are the primary way to track asynchronous progress.
## Delivery channels [#delivery-channels]
PiktID supports two channels:
1. **SSE stream** via `GET /notifications/events`
2. **Webhooks** (see [Webhooks](/docs/v2/concepts/webhooks))
All channels use the same event shape:
```json
{
"id": 123,
"name": "batch_edit",
"data": {},
"timestamp": 1702819200.0
}
```
Common event names:
* `batch_edit`: a job status changed
* `image_result`: an individual image finished processing
* `identity_preprocessing`: an identity preprocessing update
## SSE behavior [#sse-behavior]
When you connect to the SSE stream, you receive any events that occurred while you were disconnected, then live events as they happen.
Notifications are temporary. Delete them after processing to avoid receiving duplicates on reconnect.
## API endpoints [#api-endpoints]
# Projects
URL: /docs/v2/concepts/projects
Applies to API version: v2
Description: Organize uploads and jobs into reusable workspaces
A **project** is a container for your uploaded images and processing jobs.
* `project_id` is the stable identifier you should store in your integration.
* `project_name` is the human-readable label.
* If you omit `project_name` on creation, the API generates one automatically.
## Good to know [#good-to-know]
* Project names must be unique per user. If you create a project with a name that already exists, the API returns `409 Conflict`.
* A project cannot be deleted while it still has jobs in progress.
* If you belong to a group, project lists can also include projects created by other group members.
## API endpoints [#api-endpoints]
## Where projects are used [#where-projects-are-used]
Both main services require a project before processing:
* [Model swap](/docs/v2/model-swap)
* [Flat lay on model](/docs/v2/flat-lay)
# Rate limits
URL: /docs/v2/concepts/rate-limits
Applies to API version: v2
Description: API throttling, 429 behavior, and best practices for retries
All endpoints are protected by rate limiting.
## Limits [#limits]
* Global default: **3 requests/second per IP**
* Some endpoints define stricter/explicit overrides
Example overrides:
| Endpoint | Limit |
| ----------------------- | --------- |
| `POST /model-swap` | 5/minute |
| `POST /flat-2-model` | 5/minute |
| `POST /identity/upload` | 5/minute |
| `POST /notifications` | 12/minute |
| `PUT /webhooks` | 10/minute |
| `POST /webhooks/test` | 5/minute |
## Rate-limit response headers [#rate-limit-response-headers]
When a rate-limit is hit, responses include:
| Header | Description |
| ----------------------- | ------------------------------------------------------------------------ |
| `X-RateLimit-Limit` | the total number of requests allowed for the current window and endpoint |
| `X-RateLimit-Remaining` | the number of requests remaining in the current window |
| `X-RateLimit-Reset` | the timestamp (epoch) at which the current window resets |
| `Retry-After` | the time (in seconds) to wait before making a new request |
## Handling 429 responses [#handling-429-responses]
Respect `Retry-After` and retry with backoff:
```python lineNumbers
import time
import requests
def call_with_retry(url, headers, payload, retries=3):
for attempt in range(retries):
response = requests.post(url, headers=headers, json=payload)
if response.status_code != 429:
return response
wait_seconds = int(response.headers.get("Retry-After", 2 ** attempt))
# NOTE: this is an example. Do not sleep in production code!
# Use an async backoff strategy instead.
time.sleep(wait_seconds)
return response
```
## Note on account caps [#note-on-account-caps]
Some actions also have account-level limits (for example, the maximum number of identities you can create). These are separate from request-rate limits and depend on your plan.
# Tags
URL: /docs/v2/concepts/tags
Applies to API version: v2
Description: Label assets, identities, and results with a shared vocabulary
A **tag** is a colored label you create once and apply to the things you work with. Tags let you carve up a large library by campaign, season, client, or review state, and then filter on those labels when listing assets, identities, and results.
Tags come in two kinds:
* **System tags** are provided by the platform and available to everyone. You can apply them, but you cannot edit or delete them.
* **Your tags** are the ones you create. They are visible to you and to the members of your groups.
## Key fields [#key-fields]
| Field | Description |
| ------------ | ------------------------------------------------------ |
| `id` | Stable identifier you pass when assigning or filtering |
| `name` | Display name, 1 to 64 characters |
| `color` | Six-digit hex color, for example `#2E5A3B` |
| `created_by` | Email of the creator, `null` for system tags |
| `created_at` | Creation datetime (UTC, ISO 8601) |
The list endpoint returns everything you can see in one response, split into a `system` array and a `user` array.
## Creating tags [#creating-tags]
You can create a tag up front with the create endpoint, or inline while assigning. Every endpoint that accepts tags takes the same pair of fields:
| Field | Description |
| ---------- | ----------------------------------------- |
| `tag_ids` | Identifiers of existing tags to apply |
| `new_tags` | Tags to create and apply in the same call |
```python
import requests
api_url = "https://v2.api.piktid.com"
asset_id = "a1b2c3d4..."
response = requests.put(
f"{api_url}/assets/{asset_id}/tags",
headers={"Authorization": "Bearer " + access_token},
json={
"tag_ids": ["4f1c...", "9ab2..."],
"new_tags": [{"name": "SS26 campaign", "color": "#2E5A3B"}],
},
)
```
Creating a tag never reuses an existing one by name. Sending the same name twice produces two separate tags. List the tags you already have and reuse their `id` when you want a single shared label.
Tag names are stored exactly as you send them, including case and spacing. They are not normalized and do not have to be unique.
## Editing and deleting [#editing-and-deleting]
You can rename or recolor any tag you created. Tags created by a teammate are visible and usable but cannot be modified, and system tags are read-only for everyone.
| Situation | Result |
| ---------------- | ------------------------- |
| Your own tag | Update and delete succeed |
| A system tag | `403 Forbidden` |
| A teammate's tag | `404 Not Found` |
Deleting a tag removes it everywhere at once: it disappears from the tag list, from every object carrying it, and from any filter referencing it. Deletion is not reversible through the API.
## What you can tag [#what-you-can-tag]
| Object | Assign to | Returned as |
| ----------------- | ----------------------------------------------- | ----------- |
| Assets | `PUT /assets/{asset_id}/tags` | `user_tags` |
| Identities | `PUT /identity/{identity_code}/tags` | `user_tags` |
| Job output images | `PUT /jobs/{job_id}/outputs/{image_index}/tags` | `tags` |
Assigning follows the same rules everywhere:
* **Assignment is additive.** A call adds the tags you send and leaves existing ones untouched. There is no replace operation.
* **Assigning a tag you already applied is a no-op.** The request still succeeds.
* **Removing is idempotent.** Removing a tag that is not there returns `204 No Content`.
* **Unknown or inaccessible tag identifiers return `400`**, together with the list of identifiers that could not be found. Nothing is applied when this happens.
Who can assign depends on the object, not on the tag: you can tag anything you can already reach, whether you own it or reach it through a group.
## Tagging on upload [#tagging-on-upload]
The upload endpoint accepts `tag_ids` and `new_tags` so a file arrives already labelled. Bulk upload does not take tags: apply them afterwards with the asset endpoint.
If any tag identifier is unknown, the upload fails with `400` and no asset is created. Nothing is uploaded partially tagged.
## Tagging output images [#tagging-output-images]
Job outputs are versioned: regenerating or retouching an image produces a new version of the same output. Tags belong to the version they were applied to and do not carry forward. Reading results back, each output's `tags` field reflects only that specific version's own assignments.
Assignment targets the latest version by default. Pass `version` to target an older one, and `group_index` when the job produces several groups of outputs per input.
A new version starts with no tags of its own, even if an earlier version of the same output was tagged. `GET /jobs/{job_id}/results` and both gallery endpoints only ever return the latest version of an output, so a regenerate or retouch will visibly clear its tags until you re-apply them.
Removing a tag works the other way by default: `DELETE /jobs/{job_id}/outputs/{image_index}/tags/{tag_id}` with no `version` removes the tag from **every** version of that output, not just the latest one. Pass `version` to remove it from one specific version only.
Each result in `GET /jobs/{job_id}/results` and both gallery endpoints also carries a `tags` array on every entry in `inputs`, and, for `detail_repair` jobs, on every entry in `detail_repair_references`, so you can label input and output images from a single response.
## Filtering [#filtering]
Listing endpoints accept `tag_ids` as a repeated query parameter, for example `?tag_ids=4f1c...&tag_ids=9ab2...`.
Matching is inclusive: an object is returned if it carries **any** of the tags you list. There is no exclusion counterpart, and unknown identifiers simply match nothing rather than returning an error.
Filtering by tag is supported on:
* `GET /assets`
* `GET /identity`
* `GET /jobs/{job_id}/results`
* `GET /gallery/by-project`
* `GET /gallery/by-job`
On results and galleries, an output matches when its **latest** version carries the tag. Since tags do not carry forward across regenerate/retouch, an output that was tagged and later regenerated without re-tagging no longer matches. This keeps the filter consistent with what `tags` actually shows for that output.
On `GET /assets`, `tag_ids` is separate from the `include_tags` and `exclude_tags` parameters, which filter on the fixed asset labels described in [Assets](/docs/v2/concepts/assets). The two can be combined in one request.
## Visibility and sharing [#visibility-and-sharing]
Tags are personal but group-visible. You can see and apply:
* every system tag
* every tag you created
* every tag created by a member of one of your groups
There is no separate share step. Joining a group is what makes a teammate's tags available to you.
A tag becomes visible to everyone who can see the object it is applied to, including its name, color, and the email of whoever created it. Treat tag names as shared with your collaborators rather than private notes.
See [Groups](/docs/v2/concepts/groups) for how groups themselves work.
## Good to know [#good-to-know]
* There is no limit on how many tags an object can carry, or on how many tags you can create.
* Colors must be a full six-digit hex string starting with `#`. Shorthand and alpha values are rejected.
* The tag list is not paginated and not searchable. Fetch it once and cache it in your integration.
* Tags are metadata only. Applying one never consumes [credits](/docs/v2/concepts/credits) and never affects how a job is processed.
## API endpoints [#api-endpoints]
### Managing tags [#managing-tags]
### Assigning tags [#assigning-tags]
# Templates
URL: /docs/v2/concepts/templates
Applies to API version: v2
Description: Build reusable sets of instructions and run them across jobs
A **template** is a named set of saved instructions you reuse across jobs. Each instruction inside a template is a **preset**: one saved scene and styling configuration, identified by `preset_code`.
Running a template means turning each of its presets into one instruction, so a template holding six presets produces six output images from the same input. Instead of rebuilding the same configuration every time, save it once and reference it by code.
Templates are called **preset categories** in the API. Their endpoints live under `/preset/categories`, and the presets they hold live under `/preset`.
## Presets [#presets]
A preset holds the actual instructions. Presets are typed: each one targets a specific kind of job, and the platform validates that a preset's instructions match the job you use it with.
| Type | Used with |
| ------------------- | ------------------------ |
| `flat_2_model` | Flat Lay on Model jobs |
| `model_swap` | Model Swap jobs |
| `create_packshot` | Packshot creation jobs |
| `identity_creation` | Identity generation jobs |
A template normally holds presets of a single type, matching the job you intend to run it with.
### Preset fields [#preset-fields]
| Field | Description |
| ------------------ | ------------------------------------------------------ |
| `preset_code` | Stable identifier you should store in your integration |
| `name` | Human-readable label |
| `description` | Optional free-form description |
| `type` | One of the preset types above |
| `instruction_data` | JSON object holding the scene and styling instructions |
| `visibility` | `private`, `shared`, or `system` |
| `categories` | Templates the preset belongs to |
| `groups` | Names of the groups the preset is shared with |
| `created_by` | Email of the creator, `null` for system presets |
| `created_at` | Creation datetime (UTC, ISO 8601) |
| `updated_at` | Last update datetime (UTC, ISO 8601) |
Only `name`, `description`, and `instruction_data` can be updated after creation. A preset's `type` is fixed, and template membership is changed through the assign and unassign endpoints rather than through an update.
## Template fields [#template-fields]
| Field | Description |
| ----------------- | ------------------------------------------------------------- |
| `id` | Stable identifier you should store in your integration |
| `name` | Display name, unique per user and compared case-insensitively |
| `description` | Optional free-form description |
| `thumbnail_url` | Optional cover image |
| `visibility` | `private`, `shared`, or `system` |
| `preset_count` | Number of presets currently in the template |
| `preset_types` | Distinct preset types present, usually exactly one |
| `preset_previews` | Up to 8 preset previews for building a cover, newest first |
| `groups` | Names of the groups the template is shared with |
| `created_by` | Email of the creator, `null` for system templates |
| `created_at` | Creation datetime (UTC, ISO 8601) |
Read `preset_count`, `preset_types`, and `preset_previews` from the list or get endpoints. Responses that return a template as the result of another action, such as creating or duplicating one, do not include them.
Fetching a single template adds the presets it holds, plus a few fields describing your own access:
| Field | Description |
| -------------------- | -------------------------------------------------------- |
| `viewer_role` | How you reach the template: `owner`, `team`, or `system` |
| `can_edit` | Whether you can rename or delete the template |
| `can_manage_presets` | Whether you can assign and unassign presets |
| `presets` | The presets you are allowed to read, newest first |
Pass `include_presets=false` when you only need the template metadata.
`preset_count` is the total number of presets in the template, while `presets` only contains the ones you are allowed to read. On a shared template these two can legitimately disagree.
## Building a template [#building-a-template]
There are three ways to build a template.
### Manually [#manually]
Create an empty template with a `name`, then create presets and assign them to it. You can also pass `category_ids` when creating a preset to file it into one or more templates in a single call.
Template names must be unique per user. Reusing a name you already have returns `409 Conflict`.
### From a set of reference images [#from-a-set-of-reference-images]
If you have between 2 and 6 images that share a common visual style, the platform can extract a complete template in one call. The result is:
* a new template named after your input, or auto-named from the type and the current date
* one preset per image, in the same order as the `file_ids` you sent
* consistent wording across presets for traits that look identical in every image, and per-image wording for traits that visibly differ
The response also returns `shared_traits`, the fields the platform treated as common to the whole set. This is the fastest way to bootstrap a coherent template from existing creative.
Batch extraction is only supported for the `flat_2_model` and `create_packshot` types. Any other type returns `400`, along with the list of supported types.
### By duplicating an existing template [#by-duplicating-an-existing-template]
Duplicating makes a deep copy: a new template you own, plus a fresh copy of every preset inside it with a new `preset_code`. You only need read access to the source, so this is how you fork a system template or a teammate's template and then edit it freely.
Set `share_with_groups` to `true` to carry the source presets' group sharing over to the copies. Duplication is capped at 50 presets and consumes no credits.
## Creating presets [#creating-presets]
Besides writing `instruction_data` by hand, the platform can generate it for you.
### From a reference image [#from-a-reference-image]
Upload an image and the platform extracts a matching `instruction_data` payload. The response also includes a suggested name and description so you can save the preset in one step.
This is useful when you have a target look or scene in mind and want to recreate it consistently across future jobs.
### From a text description [#from-a-text-description]
Provide a natural-language description of the scene or styling you want, and the platform generates a matching `instruction_data` payload.
Both extractions return a payload without saving anything. Send the resulting `instruction_data` to the create endpoint to persist it as a preset. Extracting a whole template from images is different: it saves the template and its presets for you.
Extracting from an image or from text consumes credits. Extracting a whole template from images consumes credits per image. See [Credits](/docs/v2/concepts/credits) for details.
## Managing template contents [#managing-template-contents]
A single preset can belong to multiple templates, and you can assign or unassign at any time. Both operations require write access to the template and to the preset.
Deleting a template also deletes the presets that depend on it: the ones you own whose only remaining template is this one. Presets that also belong to another template are kept, as are presets owned by other users. The response reports how many presets were removed alongside the template.
## Visibility and sharing [#visibility-and-sharing]
Templates and presets can be:
* `private`: only you can see them
* `shared`: visible to members of your groups
* `system`: platform-provided, available to all users
Sharing is group-based, and it applies to the presets a template holds rather than to the template itself. Sharing a template grants the target group access to each preset inside it, which is what makes the template visible to that group.
Sharing a template only shares the presets that exist at that moment. Presets added afterwards are not shared automatically, so share again after adding to a template your team already uses.
Two consequences worth planning for:
* Only the owner of a preset can share it. Presets owned by someone else are skipped and returned in `skipped_preset_codes`.
* Reaching a template does not grant access to everything inside it. A shared template can hold presets a teammate cannot read.
System templates and system presets are read-only. Attempting to modify, delete, or share one returns `403`.
See [Groups](/docs/v2/concepts/groups) for how groups themselves work.
## Browsing your library [#browsing-your-library]
The template list returns every template you can reach in one response. Narrow it with `ownership` (`mine`, `shared`, or `default`), `type`, and `search`, and order it with `sort_by` and `order`.
The preset list is paginated and supports the same filters, plus `category_id` to scope to a single template and `created_by` to scope to a teammate. `search` matches both preset names and the names of the templates they belong to, so searching for a template name returns its contents.
Pass `group_by=category` to receive presets already bucketed by template, with an `Uncategorized` bucket for presets that belong to none.
When you group by template, pagination applies to the templates rather than to the individual presets.
## Referencing a template in a job [#referencing-a-template-in-a-job]
Job instructions carry `preset_name`, `preset_code`, and `category_names` as metadata. These fields record where an instruction came from and are surfaced when you read the job back. They do not affect processing: the instruction payload you submit is what gets rendered.
Job results also report the templates a preset currently belongs to, so you can navigate from an output image back to the template that produced it. Unlike `category_names`, which records the names as they were when the job was submitted, this reflects the template as it stands today.
See [Flat Lay on Model](/docs/v2/flat-lay) and [Create a packshot](/docs/v2/create-packshot) for the instruction payloads.
## Good to know [#good-to-know]
* Updating a template replaces `description` and `thumbnail_url` with whatever you send. Omitting them clears the stored values, so always send the fields you want to keep.
* Deleting a preset or a template is not reversible through the API.
* Presets can be deleted in bulk. The response reports how many were removed and lists the codes it could not delete, instead of failing the whole request.
* Group names in the share endpoints are matched case-insensitively.
## Account limits [#account-limits]
Non-enterprise accounts have a maximum number of presets. If you reach the cap, the create endpoint returns `429`. Delete unused presets or contact us to raise the limit.
## API endpoints [#api-endpoints]
### Templates [#templates]
### Presets [#presets-1]
# Webhooks
URL: /docs/v2/concepts/webhooks
Applies to API version: v2
Description: Receive notification events as signed HTTP callbacks
Webhooks deliver the same events as SSE, but push them to your server.
## Configuration flow [#configuration-flow]
1. Create/update webhook URL with `PUT /webhooks`
2. Receive and securely store the returned `secret`
3. Verify signatures for every incoming request
4. Optionally trigger a test ping with `POST /webhooks/test`
The webhook secret is only returned on create/update. Store it securely.
## Payload and headers [#payload-and-headers]
Example payload:
```json
{
"id": 123,
"name": "batch_edit",
"data": {},
"timestamp": 1702819200.0
}
```
Important headers:
* `X-Webhook-Signature: sha256=`
* `X-Webhook-Event: `
## Signature verification (Python) [#signature-verification-python]
```python lineNumbers
import hashlib
import hmac
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)
```
If your server does not return a `2xx` response, delivery is retried automatically with exponential backoff.
## Testing webhooks [#testing-webhooks]
You can trigger a test ping with `POST /webhooks/test`. This sends a test event to your webhook URL, allowing you to verify your setup and signature verification logic.
The following is a fairly complete script which allows you to test your webhook endpoint locally (it requires the `requests` library):
```python
#!/usr/bin/env python3
"""
Standalone webhook tester for local development.
Starts a local HTTP server, registers it as the webhook endpoint via the API,
then prints every incoming delivery with HMAC signature verification.
Usage:
python etc/test_webhook.py --token [options]
Example:
python etc/test_webhook.py --token mytoken123
python etc/test_webhook.py --token mytoken123 --port 9000
"""
import argparse
import hashlib
import hmac
import json
import re
import signal
import subprocess
import sys
import threading
from datetime import datetime, timezone
from http.server import BaseHTTPRequestHandler, HTTPServer
import requests
# ── ANSI colour helpers ────────────────────────────────────────────────────────
RESET = "\033[0m"
BOLD = "\033[1m"
GREEN = "\033[92m"
RED = "\033[91m"
YELLOW = "\033[93m"
CYAN = "\033[96m"
DIM = "\033[2m"
def _c(color: str, text: str) -> str:
return f"{color}{text}{RESET}"
def _ok(text: str) -> str:
return _c(GREEN, f"✓ {text}")
def _err(text: str) -> str:
return _c(RED, f"✗ {text}")
def _warn(text: str) -> str:
return _c(YELLOW, f"⚠ {text}")
def _info(text: str) -> str:
return _c(CYAN, f"→ {text}")
def _dim(text: str) -> str:
return _c(DIM, text)
# ── HMAC verification ──────────────────────────────────────────────────────────
def _verify_signature(secret: str, body: bytes, header_value: str | None) -> bool:
"""Return True if the X-Webhook-Signature header matches the expected HMAC."""
if not header_value:
return False
if not header_value.startswith("sha256="):
return False
expected_hex = hmac.new(secret.encode("utf-8"), body, hashlib.sha256).hexdigest()
provided_hex = header_value.removeprefix("sha256=")
return hmac.compare_digest(expected_hex, provided_hex)
# ── Local webhook receiver ─────────────────────────────────────────────────────
class _WebhookHandler(BaseHTTPRequestHandler):
"""HTTP handler that prints and verifies each incoming webhook delivery."""
# Set by the main thread before the server starts
secret: str = ""
def log_message(self, fmt, *args): # suppress default access log
pass
def do_POST(self):
length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(length)
event = self.headers.get("X-Webhook-Event", "")
signature_header = self.headers.get("X-Webhook-Signature")
sig_ok = _verify_signature(self.server.secret, body, signature_header) # type: ignore[attr-defined]
timestamp = datetime.now(timezone.utc).strftime("%H:%M:%S.%f")[:-3]
print(f"\n{_dim('─' * 60)}")
print(f" {BOLD}Delivery received{RESET} {_dim(timestamp)}")
print(f" Event : {_c(BOLD, event)}")
sig_display = signature_header or ""
if sig_ok:
print(f" Sig : {_ok(sig_display)}")
else:
print(f" Sig : {_err(sig_display)}")
try:
parsed = json.loads(body)
pretty = json.dumps(parsed, indent=4)
except Exception:
pretty = body.decode(errors="replace")
for line in pretty.splitlines():
print(f" {_dim(line)}")
# Always respond 200 so the server records success
self.send_response(200)
self.end_headers()
self.wfile.write(b"OK")
class _WebhookServer(HTTPServer):
"""HTTPServer subclass that carries the HMAC secret alongside the socket."""
def __init__(self, server_address, secret: str):
self.secret = secret
super().__init__(server_address, _WebhookHandler)
# ── API helpers ────────────────────────────────────────────────────────────────
class APIClient:
def __init__(self, base_url: str, token: str):
self.base = base_url.rstrip("/")
self.session = requests.Session()
self.session.headers.update({"Authorization": f"Bearer {token}"})
def _url(self, path: str) -> str:
return f"{self.base}{path}"
def register_webhook(self, url: str) -> dict:
resp = self.session.put(self._url("/webhooks"), json={"url": url})
resp.raise_for_status()
return resp.json()
def trigger_test(self) -> dict:
resp = self.session.post(self._url("/webhooks/test"))
resp.raise_for_status()
return resp.json()
def delete_webhook(self) -> None:
resp = self.session.delete(self._url("/webhooks"))
if resp.status_code not in (200, 404):
resp.raise_for_status()
# ── Entry point ────────────────────────────────────────────────────────────────
def _parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Local webhook receiver for testing the PiktID webhook system.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument("--token", required=True, help="Your API token")
parser.add_argument(
"--hook-url",
help="URL to register as webhook endpoint (overrides --host and --port). If not provided, a localhost.run tunnel will be created automatically.",
)
parser.add_argument(
"--api-url", default="https://v2.api.piktid.com", help="API base URL"
)
parser.add_argument("--port", type=int, default=9876, help="Local listener port")
parser.add_argument(
"--host", default="localhost", help="Hostname for the local server URL"
)
parser.add_argument(
"--no-clean",
action="store_true",
help="Do not delete the webhook configuration on exit",
)
return parser.parse_args()
def _start_tunnel(port: int) -> subprocess.Popen:
"""Start a localhost.run ssh tunnel for the given port and yield its output."""
print(_info(f"Starting localhost.run tunnel for port {port}…"))
# We pipe stdout and stderr to capture the generated domain name
proc = subprocess.Popen(
[
"ssh",
"-o",
"StrictHostKeyChecking=no",
"-R",
f"80:localhost:{port}",
"nokey@localhost.run",
"--",
"--output",
"text",
],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1, # line-buffered
)
return proc
def _get_tunnel_url(proc: subprocess.Popen) -> str | None:
"""Read the output of the tunnel process until the URL is found."""
if not proc.stdout:
return None
# Pattern to match: https://.lhr.life
pattern = re.compile(r"(https://[a-zA-Z0-9-]+\.lhr\.life)")
for line in proc.stdout:
# print(_dim(f"tunnel: {line.strip()}")) # optional debug
match = pattern.search(line)
if match:
return match.group(1)
return None
def main() -> None:
args = _parse_args()
client = APIClient(base_url=args.api_url, token=args.token)
tunnel_proc = None
if args.hook_url:
local_url = args.hook_url
else:
# Start tunnel
tunnel_proc = _start_tunnel(args.port)
tunnel_url = _get_tunnel_url(tunnel_proc)
if not tunnel_url:
print(_err("Failed to get tunnel URL from localhost.run"))
if tunnel_proc:
tunnel_proc.terminate()
sys.exit(1)
local_url = tunnel_url
print(_ok(f"Tunnel established: {local_url}"))
# ── 1. Register the webhook ────────────────────────────────────────────────
print(_info(f"Registering webhook → {local_url}"))
try:
data = client.register_webhook(local_url)
except requests.HTTPError as exc:
print(
_err(
f"Failed to register webhook: {exc.response.status_code} {exc.response.text}"
)
)
sys.exit(1)
except requests.ConnectionError:
print(_err(f"Could not connect to API at {args.api_url}"))
sys.exit(1)
secret: str = data["secret"]
action = "Created" if data.get("is_active") else "Updated"
print(_ok(f"{action} webhook (id={data['id']})"))
print(_dim(f" Secret : {secret}"))
print(_dim(f" URL : {data['url']}"))
# ── 2. Start local server ──────────────────────────────────────────────────
server = _WebhookServer(("0.0.0.0", args.port), secret=secret)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
print(_ok(f"Listening on port {args.port}"))
# ── 3. Graceful shutdown on Ctrl+C ─────────────────────────────────────────
def _shutdown(sig, frame):
print(f"\n{_warn('Shutting down…')}")
server.shutdown()
if not args.no_clean:
print(_info("Deleting webhook from API…"))
try:
client.delete_webhook()
print(_ok("Webhook deleted"))
except Exception as exc:
print(_warn(f"Could not delete webhook: {exc}"))
if tunnel_proc:
print(_info("Closing tunnel…"))
tunnel_proc.terminate()
tunnel_proc.wait(timeout=2)
sys.exit(0)
signal.signal(signal.SIGTERM, _shutdown)
# ── 4. Inform user + block ─────────────────────────────────────────────────
print()
print(_c(BOLD, "Waiting for webhook deliveries. Press Ctrl+C to stop."))
print(
_dim(
"Tip: trigger a test ping with: "
f"curl -X POST {args.api_url}/webhooks/test "
f"-H 'Authorization: Bearer {args.token}'"
)
)
# Block the main thread; KeyboardInterrupt (Ctrl+C) is portable on all platforms.
try:
threading.Event().wait()
except KeyboardInterrupt:
_shutdown(None, None)
if __name__ == "__main__":
main()
```