Gateway API
OpenAI Responses API–compatible endpoint for PostProdAgent workflows. Workflow list current as of v1.9.2.
The Gateway API exposes PostProdAgent image-generation workflows through an interface compatible with the OpenAI Responses API. Any client that speaks the OpenAI protocol can point its base_url at this server and submit jobs without custom integration.
Jobs are processed asynchronously: the create call returns immediately with a response ID and status: queued; the client polls the GET endpoint until status: completed and then reads the output image(s).
http://<server>:8765
Authentication
All endpoints require HTTP Basic Authentication.
# Header format Authorization: Basic <base64(username:password)> # curl shorthand curl -u admin:admin https://<server>:8765/v1/responses
Models
Each model maps to a PostProdAgent workflow. Pass the model ID in the model field of a create request.
workflow fieldpostprod/auto together with the optional workflow field to target any workflow by its internal name (e.g. "workflow": "WKF_IN_CUSTOM_WF").
Image position mapping
Images in content[] are assigned to workflow input slots according to the workflow's image_mapping.json. For virtual_try_on:
| Position | Slot | Description |
|---|---|---|
| 1 (first image) | default | Garment / product photo |
| 2 (second image) | _REF | Model / avatar reference |
| 3+ (extra images) | _REF2, _REF3… | Additional references (workflow-dependent) |
POST /v1/responses
Create a new job. Returns a response object with status: queued.
Request body
| Field | Type | Description |
|---|---|---|
| model required | string | Model ID (see Models above) |
| input required | array | List of message objects (role + content) |
| workflow optional | string | Explicit workflow name — used when model=postprod/auto |
| prompt_append optional | string | Extra text appended to the workflow prompt. Supports SIZE=, QUALITY=, PAD_INPUT= overrides |
| output_format optional | string | jpeg (default) or png |
| output_quality optional | integer | JPEG quality 1–100, default 90 |
Content block types
| type | Fields | Description |
|---|---|---|
| input_image | image_url: data URI or URL | Image to process. Data URI format: data:image/jpeg;base64,<b64> |
| input_text | text: string | Text prompt, appended to workflow prompt |
Response
{
"id": "resp_a3f1c9e20b4d",
"object": "response",
"model": "postprod/virtual_try_on",
"status": "queued",
"output": [],
"usage": { "input_tokens": 0, "output_tokens": 0, "total_tokens": 0 },
"created_at": 1718356528
}GET /v1/responses/{id}
Poll the status of a job. When status is completed, the output array contains the result image(s) as base64 data URIs.
Status lifecycle
queued → in_progress → completed / failed
Completed response
{
"id": "resp_a3f1c9e20b4d",
"status": "completed",
"output": [
{
"role": "assistant",
"content": [
{
"type": "image_url",
"image_url": { "url": "data:image/jpeg;base64,/9j/4AAQ..." }
}
]
}
]
}idm_mapper_openai + idm_crop produces several artefacts: the
annotated photo, plus one detail crop per detail found. Only the annotated photo comes
back here. The crops are written as sibling result folders
(JOB_..._IDM_01_<label>), which this API does not walk — it reads
*_AI.* inside the job folder alone — so they are reachable from the web UI
but not over /v1. The coordinates of every detail are in the job's
idm_details.json artifact if you need to reproduce the crops yourself.
Exposing derived results over the API is on the roadmap, after the refactoring.
GET /v1/models
List all available models and their availability status. The list is generated dynamically from the workflows currently present on the server (any WKF_IN_* folder is exposed as postprod/<name>) — new, renamed, or removed workflows are reflected automatically. A handful of legacy model-id aliases from before a workflow rename are appended with "legacy_alias": true.
{
"object": "list",
"data": [
{
"id": "postprod/virtual_try_on",
"object": "model",
"workflow": "WKF_IN_VIRTUAL_TRY_ON",
"available": true
},
// ...
]
}Example — Virtual Try-On (2 images)
Send a garment photo (position 1) and a model/avatar photo (position 2) to generate a dressed-on-model result.
# 1. Encode images as base64 GARMENT_B64=$(base64 -i garment.jpg | tr -d '\n') AVATAR_B64=$(base64 -i avatar.jpg | tr -d '\n') # 2. Create the job RESPONSE=$(curl -s -u admin:admin \ -X POST http://<server>:8765/v1/responses \ -H "Content-Type: application/json" \ -d "{ \"model\": \"postprod/virtual_try_on\", \"input\": [{ \"role\": \"user\", \"content\": [ { \"type\": \"input_image\", \"image_url\": \"data:image/jpeg;base64,${GARMENT_B64}\" }, { \"type\": \"input_image\", \"image_url\": \"data:image/jpeg;base64,${AVATAR_B64}\" }, { \"type\": \"input_text\", \"text\": \"Virtual try-on: indossa il capo su questo avatar\" } ] }] }") RESP_ID=$(echo $RESPONSE | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])") echo "Job created: $RESP_ID" # 3. Poll until completed while true; do POLL=$(curl -s -u admin:admin http://<server>:8765/v1/responses/$RESP_ID) STATUS=$(echo $POLL | python3 -c "import sys,json; print(json.load(sys.stdin)['status'])") echo "Status: $STATUS" [ "$STATUS" = "completed" ] && break [ "$STATUS" = "failed" ] && { echo "Job failed"; exit 1; } sleep 5 done # 4. Extract and save result image echo $POLL | python3 -c " import sys, json, base64 data = json.load(sys.stdin) b64 = data['output'][0]['content'][0]['image_url']['url'].split(',')[1] with open('result.jpg', 'wb') as f: f.write(base64.b64decode(b64)) print('Saved result.jpg')
import base64, time, requests BASE_URL = "http://<server>:8765" AUTH = ("admin", "admin") HEADERS = {"Content-Type": "application/json"} def encode(path): with open(path, "rb") as f: return "data:image/jpeg;base64," + base64.b64encode(f.read()).decode() # 1. Create job r = requests.post( f"{BASE_URL}/v1/responses", auth=AUTH, headers=HEADERS, json={ "model": "postprod/virtual_try_on", "input": [{ "role": "user", "content": [ {"type": "input_image", "image_url": encode("garment.jpg")}, {"type": "input_image", "image_url": encode("avatar.jpg")}, {"type": "input_text", "text": "Virtual try-on: indossa il capo su questo avatar"}, ] }] } ) resp_id = r.json()["id"] print(f"Job created: {resp_id}") # 2. Poll while True: poll = requests.get(f"{BASE_URL}/v1/responses/{resp_id}", auth=AUTH).json() status = poll["status"] print(f"Status: {status}") if status == "completed": break if status == "failed": raise RuntimeError("Job failed") time.sleep(5) # 3. Save result url = poll["output"][0]["content"][0]["image_url"]["url"] img_bytes = base64.b64decode(url.split(",")[1]) with open("result.jpg", "wb") as f: f.write(img_bytes) print("Saved result.jpg")
Example — 3-image workflow
Some workflows accept more than two images (e.g. Outfit Composer: body + item 1 + item 2). Additional images are assigned to slots _REF, _REF2, etc. according to the workflow's image_mapping.json.
BODY_B64=$(base64 -i body.jpg | tr -d '\n')
ITEM1_B64=$(base64 -i item1.jpg | tr -d '\n')
ITEM2_B64=$(base64 -i item2.jpg | tr -d '\n')
curl -s -u admin:admin \
-X POST http://<server>:8765/v1/responses \
-H "Content-Type: application/json" \
-d "{
\"model\": \"postprod/outfit\",
\"input\": [{
\"role\": \"user\",
\"content\": [
{ \"type\": \"input_image\", \"image_url\": \"data:image/jpeg;base64,${BODY_B64}\" },
{ \"type\": \"input_image\", \"image_url\": \"data:image/jpeg;base64,${ITEM1_B64}\" },
{ \"type\": \"input_image\", \"image_url\": \"data:image/jpeg;base64,${ITEM2_B64}\" },
{ \"type\": \"input_text\", \"text\": \"Componi l'outfit completo\" }
]
}],
\"prompt_append\": \"SIZE=1024x1536 QUALITY=high\"
}prompt_append field supports parameter overrides: SIZE=WxH, QUALITY=high|medium|low, PAD_INPUT=white|black|transparent. These take precedence over both engine and workflow defaults.
Example — OpenAI Python SDK
Point the SDK's base_url at the PostProdAgent server. No custom client needed.
from openai import OpenAI import base64, time client = OpenAI( api_key="admin:admin", # basic auth as api_key (user:pass) base_url="http://<server>:8765", ) def encode(path): with open(path, "rb") as f: return "data:image/jpeg;base64," + base64.b64encode(f.read()).decode() # Create + poll in one call (SDK handles serialization) response = client.responses.create( model="postprod/virtual_try_on", input=[{ "role": "user", "content": [ {"type": "input_image", "image_url": encode("garment.jpg")}, {"type": "input_image", "image_url": encode("avatar.jpg")}, ] }] ) resp_id = response.id while response.status not in ("completed", "failed"): time.sleep(5) response = client.responses.retrieve(resp_id) print(response.status) url = response.output[0].content[0].image_url.url img_bytes = base64.b64decode(url.split(",")[1]) open("result.jpg", "wb").write(img_bytes)
Polling pattern reference
Recommended polling parameters:
| Parameter | Recommended value | Notes |
|---|---|---|
| Interval | 5 seconds | Most jobs complete in 20–90 s |
| Max attempts | 60 | 5 minutes total timeout |
| Backoff | Not required | Server-side queue is stable |
import time, requests def wait_for_result(base_url, resp_id, auth, interval=5, max_attempts=60): for _ in range(max_attempts): r = requests.get(f"{base_url}/v1/responses/{resp_id}", auth=auth).json() if r["status"] == "completed": return r if r["status"] == "failed": raise RuntimeError(f"Job {resp_id} failed") time.sleep(interval) raise TimeoutError(f"Job {resp_id} not completed after {max_attempts * interval}s")