A working code recipe for calling gpt-image-2's image-to-image endpoint on hiapi: the request envelope, the reference-image and aspect_ratio parameters, polling vs. callbacks, and copy-pasteable curl, Python, and Node.js examples.

This recipe shows you exactly how to call gpt-image-2's image-to-image mode through the hiapi unified task API. You'll get a working curl example, a Python script you can drop into a backend, and a Node.js version for serverless use — plus the parameters that actually matter and the gotchas that bite in production.
By the end you'll have a script that takes a reference image URL plus a prompt and returns a generated image URL you can download or hand to your storage layer.
That's it. There is no SDK to install — hiapi is HTTP + JSON, so any language that can POST works.
Looking for plain text-to-image first? See How to generate images with the hiapi API. The pattern below is the same async task flow, with one extra
imagefield.
hiapi exposes every image model through the same async task interface:
POST /v1/tasks — submit the job, get back a taskId.GET /v1/tasks/{taskId} until status === "success", or include a callback.url and receive the final payload as a webhook.data.output[0].url and download or store it.curl -X POST https://api.hiapi.ai/v1/tasks \
-H "Authorization: Bearer sk-YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-image-2/image-to-image",
"input": {
"prompt": "transform the subject into a watercolor painting, soft pastel palette",
"image": "https://example.com/reference.jpg",
"aspect_ratio": "1:1",
"resolution": "1K"
}
}'
The immediate response gives you the task id:
{ "data": { "taskId": "task_01HXXXX..." } }
Then poll for the result:
curl https://api.hiapi.ai/v1/tasks/task_01HXXXX... \
-H "Authorization: Bearer sk-YOUR_KEY"
When it's ready, the response looks like this:
{
"data": {
"status": "success",
"output": [{ "url": "https://cdn.hiapi.ai/...png", "expireAt": "..." }]
}
}
The output[0].url is short-lived (note the expireAt), so download or re-upload to your own storage immediately — never serve hot links to end users.
| Field | Type | Notes |
|---|---|---|
model | string | Use the bare id gpt-image-2/image-to-image. No models/ prefix. |
input.prompt | string | The transformation you want applied to the reference. |
input.image | string | HTTPS URL or a data:image/...;base64,... URL. |
input.aspect_ratio | string | One of 1:1, 3:2, 2:3, 4:3, 3:4, 5:4, 4:5, 16:9, 9:16, 21:9, or auto. |
input.resolution | string | 1K, 2K, or 4K. Defaults to 1K. |
Two important behaviors:
aspect_ratio (without resolution); others want pixel sizes. gpt-image-2/image-to-image takes the aspect_ratio + resolution pair shown above — don't pass size: "1024x1024" here.image field is the reference. If you pass a base64 data URL, keep the payload under your platform's request-body limit (a few MB is fine, multi-megabyte images should be uploaded first and passed by URL).This version handles polling with a deadline and exits cleanly on failure:
import os
import time
import requests
API = "https://api.hiapi.ai/v1/tasks"
TOKEN = os.environ["HIAPI_API_KEY"]
HEADERS = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}
def submit(prompt: str, image_url: str, aspect_ratio: str = "1:1") -> str:
payload = {
"model": "gpt-image-2/image-to-image",
"input": {
"prompt": prompt,
"image": image_url,
"aspect_ratio": aspect_ratio,
"resolution": "1K",
},
}
r = requests.post(API, headers=HEADERS, json=payload, timeout=30)
r.raise_for_status()
return r.json()["data"]["taskId"]
def wait(task_id: str, timeout_s: int = 300, interval_s: int = 5) -> str:
deadline = time.time() + timeout_s
while time.time() < deadline:
r = requests.get(f"{API}/{task_id}", headers=HEADERS, timeout=20)
r.raise_for_status()
task = r.json()["data"]
status = task.get("status")
if status == "success":
return task["output"][0]["url"]
if status == "fail":
err = task.get("error") or {}
raise RuntimeError(f"task failed: {err.get('code')} {err.get('message')}")
time.sleep(interval_s)
raise TimeoutError(f"task {task_id} did not finish in {timeout_s}s")
if __name__ == "__main__":
task_id = submit(
prompt="transform the subject into a watercolor painting, soft pastel palette",
image_url="https://example.com/reference.jpg",
)
print("task:", task_id)
print("url:", wait(task_id))
Run with HIAPI_API_KEY=sk-... python recipe.py.
const API = "https://api.hiapi.ai/v1/tasks";
const TOKEN = process.env.HIAPI_API_KEY;
const headers = {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
};
async function submit(prompt, imageUrl, aspectRatio = "1:1") {
const r = await fetch(API, {
method: "POST",
headers,
body: JSON.stringify({
model: "gpt-image-2/image-to-image",
input: { prompt, image: imageUrl, aspect_ratio: aspectRatio, resolution: "1K" },
}),
});
const data = await r.json();
if (!r.ok) throw new Error(JSON.stringify(data));
return data.data.taskId;
}
async function wait(taskId, timeoutMs = 300000, intervalMs = 5000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const r = await fetch(`${API}/${taskId}`, { headers });
const task = (await r.json()).data;
if (task.status === "success") return task.output[0].url;
if (task.status === "fail") throw new Error(JSON.stringify(task.error));
await new Promise((res) => setTimeout(res, intervalMs));
}
throw new Error(`task ${taskId} timed out`);
}
(async () => {
const id = await submit(
"transform the subject into a watercolor painting, soft pastel palette",
"https://example.com/reference.jpg",
);
console.log("task:", id, "\nurl:", await wait(id));
})();
Polling every 5 seconds works fine for one-shot scripts, but it ties up a worker for the full job duration. In a real backend you usually want a webhook:
{
"model": "gpt-image-2/image-to-image",
"input": { "prompt": "...", "image": "https://example.com/reference.jpg" },
"callback": { "url": "https://your-app.example.com/api/hiapi-callback", "when": "final" }
}
When the task settles (success or fail), hiapi POSTs the same payload you'd get from GET /v1/tasks/{id} to that URL. Treat that handler as idempotent — retries are possible — and verify the request before trusting it (your own signed token in the URL, an Authorization header you check, etc.).
A clean pattern is: submit the task, persist taskId keyed by your own job id, return 202 Accepted to your caller, and let the webhook do the storage + notification work.
Three failure modes you'll hit in practice:
401 permission_denied with "This API key cannot use the selected model." — the key isn't authorized for gpt-image-2/image-to-image. Check it in the hiapi dashboard and confirm the model is in your plan on the pricing page.400 with additional properties not allowed — you sent a field a particular model doesn't accept (e.g. size instead of aspect_ratio). The error message names the offending key; strip it and retry.status: "fail" at poll time with a data.error object — the model itself rejected the inputs (unsafe content, unreachable reference URL, etc.). Surface error.code and error.message to your logs and don't auto-retry blindly.Always set a request timeout on the POST /v1/tasks call (30 s is plenty — it returns quickly with a taskId) and a separate, longer timeout on the polling loop or your webhook receive path.
/v1/tasks and the rest of the surface.Q: Does gpt-image-2/image-to-image support multiple reference images?
A: The minimal contract uses a single image field. If your use case needs multiple references (e.g. subject + style image), pre-composite them or check the model page for the latest supported fields.
Q: Can I pass a local file instead of a URL?
A: Yes — encode it as a data:image/png;base64,... URL and pass that as input.image. For anything bigger than ~3 MB, upload to your own storage first and pass the public URL; it's faster and avoids request-body limits.
Q: How long does a job take?
A: Most gpt-image-2 image-to-image jobs finish in 10–30 seconds at 1K resolution. Higher resolutions and complex prompts can take longer — set your polling timeout to 5 minutes to be safe.
Q: How do I control how much the output differs from the reference? A: Phrase the prompt explicitly. "Keep the subject's pose and composition, only restyle to watercolor" produces tighter conformance than "make this a watercolor." gpt-image-2's image-to-image mode is prompt-driven — there is no separate "strength" slider in the request envelope.
Q: Is the output[0].url permanent?
A: No. It carries an expireAt timestamp (typically a few hours). Download the bytes and upload them to your own storage (R2, S3, etc.) as soon as the task succeeds.
Q: What does it cost per image? A: It depends on your plan and resolution. Current per-call cost is on the pricing page.