Create an identity
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 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
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 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 for details.
Promote the draft you want into a full identity. The API stages the image, creates the Identity record, and dispatches the preprocessing pipeline. See 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 for details.
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, 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.
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
To build a brief from an existing photo, mirror the From Image flow in the wizard: upload the reference via POST /upload, then call POST /preset/extract-from-image with type: "identity_creation" to get back an instruction_data object. Wrap that in {"instructions": [<instruction_data>], "options": {...}} and submit to /identity/create as usual. Preset extraction charges credits separately from the creation job.
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; pass each one as {"file_id": "<uuid>"}.
input_assets is not supported when options.model is orbita. Drop them or switch model.
Submitting a creation job
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",
"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}"){
"job_id": "job_abc123...", // JOB_ID
"status": "pending",
"num_instructions": 1,
"total_variations": 3,
"message": "Identity creation job dispatched: 1 instruction(s), 3 total variation(s)"
}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_pro" | "seedream" | "orbita" | "auto" | Generation engine. auto lets the platform pick. orbita has tighter constraints (see 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
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
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.
{
"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.",
"num_variations": 2,
"options": {"ar": "3:4", "size": "2K", "format": "jpg"}
}
]
}Expert prompt with a reference image
Pair prompt with input_assets when you want the generator to use a visual cue alongside text direction.
{
"instructions": [
{
"prompt": "A model styled in the same vibe as the reference, three-quarter turn, calm focused expression.",
"input_assets": [{"file_id": "img_xyz..."}],
"num_variations": 2,
"options": {"ar": "3:4", "size": "2K", "format": "jpg"}
}
]
}Tracking drafts
Identity creation uses the same notifications surface as Model Swap and Flat-to-Model. Use SSE or webhooks with id_task: <job_id> until you receive a completed notification, then fetch the draft list.
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']}")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){
"job_id": "job_abc123...",
"job_type": "identity_creation",
"status": "completed",
"results": [
{
"image_index": 0,
"image_result_id": 12345, // IMAGE_RESULT_ID
"url": "https://...", // CloudFront URL of the draft
"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 until you promote one.
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.
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}"){
"identity_code": "abc1234567xy", // IDENTITY_CODE
"preprocessing_job_id": "job_pre456...", // PREPROCESSING_JOB_ID
"status": "pending",
"message": "Identity promoted; preprocessing dispatched."
}Name uniqueness
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.
Idempotency
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
Promotion dispatches a second job (identity_preprocessing). Track that job via notifications until preprocessing is complete.
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"))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} 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
| 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
| 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. |