
This recipe shows the smallest amount of code you need to generate an image with gpt-image-2/text-to-image through the hiapi unified task API, then explains every parameter and the production-ready patterns (callbacks, idempotency, error handling) you should use once it works.
You need an API key. Create one from the API Keys page in your hiapi dashboard — keys start with sk-. The model page at gpt-image-2/text-to-image lists current per-image pricing; the full breakdown is on the pricing page.
All calls in this guide hit the unified async task endpoint:
POST https://api.hiapi.ai/v1/tasks — create a generation taskGET https://api.hiapi.ai/v1/tasks/{taskId} — poll task statusThe general reference for the platform lives in the hiapi API docs.
A single generation is a two-step exchange: create the task, then wait for it to finish.
# 1. Create the task
curl -s -X POST https://api.hiapi.ai/v1/tasks \
-H "Authorization: Bearer sk-YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-image-2/text-to-image",
"input": {
"prompt": "a calico cat curled up on a sunlit windowsill, soft focus background",
"aspect_ratio": "1:1",
"resolution": "1K"
}
}'
# => {"code":200,"data":{"taskId":"tk-hiapi-01KVY...."},"message":"success"}
# 2. Poll until status == "success"
TASK_ID="tk-hiapi-01KVY...."
curl -s "https://api.hiapi.ai/v1/tasks/$TASK_ID" \
-H "Authorization: Bearer sk-YOUR_KEY"
# => {"code":200,"data":{"status":"success","output":[{"url":"https://..."}], ...}}
The image URL is at data.output[0].url. Download it immediately — output[0].expireAt is short, so the URL is for fetching, not for permanently embedding.
import os
import time
import urllib.request
import requests # pip install requests
API_BASE = "https://api.hiapi.ai/v1/tasks"
TOKEN = os.environ["HIAPI_API_KEY"] # sk-...
def create_task(prompt: str, aspect_ratio: str = "1:1", resolution: str = "1K") -> str:
payload = {
"model": "gpt-image-2/text-to-image",
"input": {
"prompt": prompt,
"aspect_ratio": aspect_ratio,
"resolution": resolution,
},
}
r = requests.post(API_BASE, json=payload,
headers={"Authorization": f"Bearer {TOKEN}"},
timeout=60)
r.raise_for_status()
body = r.json()
if body.get("error"):
raise RuntimeError(f"create failed: {body['error']}")
return body["data"]["taskId"]
def wait_task(task_id: str, timeout_s: int = 600, poll_s: int = 5) -> dict:
deadline = time.time() + timeout_s
while time.time() < deadline:
r = requests.get(f"{API_BASE}/{task_id}",
headers={"Authorization": f"Bearer {TOKEN}"},
timeout=30)
r.raise_for_status()
task = r.json().get("data") or {}
status = task.get("status")
if status == "success":
return task
if status == "fail":
raise RuntimeError(f"task failed: {task.get('error')}")
time.sleep(poll_s)
raise TimeoutError(f"task {task_id} did not finish in {timeout_s}s")
if __name__ == "__main__":
task_id = create_task(
"a calico cat curled up on a sunlit windowsill, soft focus background",
aspect_ratio="3:2",
resolution="1K",
)
task = wait_task(task_id)
url = task["output"][0]["url"]
urllib.request.urlretrieve(url, "cat.png")
print(f"saved cat.png from task {task_id}")
// Node 18+ has global fetch
const API_BASE = "https://api.hiapi.ai/v1/tasks";
const TOKEN = process.env.HIAPI_API_KEY;
async function createTask(prompt, aspectRatio = "1:1", resolution = "1K") {
const res = await fetch(API_BASE, {
method: "POST",
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "gpt-image-2/text-to-image",
input: { prompt, aspect_ratio: aspectRatio, resolution },
}),
});
const body = await res.json();
if (body.error) throw new Error(`create failed: ${body.error.code} ${body.error.message}`);
return body.data.taskId;
}
async function waitTask(taskId, { timeoutMs = 600_000, pollMs = 5_000 } = {}) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const res = await fetch(`${API_BASE}/${taskId}`, {
headers: { "Authorization": `Bearer ${TOKEN}` },
});
const task = (await res.json()).data || {};
if (task.status === "success") return task;
if (task.status === "fail") throw new Error(`task failed: ${JSON.stringify(task.error)}`);
await new Promise(r => setTimeout(r, pollMs));
}
throw new Error(`task ${taskId} timed out`);
}
(async () => {
const id = await createTask(
"a calico cat curled up on a sunlit windowsill, soft focus background",
"16:9",
"1K",
);
const task = await waitTask(id);
const url = task.output[0].url;
const img = await fetch(url).then(r => r.arrayBuffer());
const { writeFileSync } = await import("node:fs");
writeFileSync("cat.png", Buffer.from(img));
console.log(`saved cat.png from task ${id}`);
})();
The request body always has two top-level fields: model (string, the bare model id gpt-image-2/text-to-image) and input (the per-model parameter object).
input field | Required | Type | Notes |
|---|---|---|---|
prompt | Yes | string | The text description of the image you want. |
aspect_ratio | Yes | 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. |
resolution | No | string | 1K (default), 2K, or 4K. Pixel size scales with the chosen aspect ratio. |
callback | No | object | { "url": "https://yours.example/hook", "when": "final" } — see the next section. |
A successful create returns {"code":200, "data":{"taskId":"tk-hiapi-..."}, "message":"success"}. A GET /v1/tasks/{taskId} returns data.status as one of handling, success, or fail; on success, data.output is an array whose first element has the url for the image and an expireAt timestamp.
Other hiapi image models (for example
z-image,qwen-image) take slightly differentinputfields —aspect_ratiois the convention used bygpt-image-2/text-to-image. When you swap models, check the model page for the exactinputschema.
Polling every 5 seconds is fine for prototyping, but for production traffic you want hiapi to push the finished task to you so you can free the worker. Add a callback block to input:
{
"model": "gpt-image-2/text-to-image",
"input": {
"prompt": "an astronaut sketching the moon",
"aspect_ratio": "3:2",
"resolution": "1K",
"callback": {
"url": "https://your-app.example.com/webhooks/hiapi",
"when": "final"
}
}
}
when: "final" fires once when the task reaches a terminal state (success or fail). Your endpoint receives the same payload shape as GET /v1/tasks/{id}, so the handler can reuse the polling parser. Verify the taskId against the one you stored when you created the task so you know which request the callback belongs to.
If your worker crashes after POST /v1/tasks but before storing the taskId, you can end up generating the same image twice. The cheap fix is to store (your_request_id → taskId) in your database the moment you receive the create response, in the same transaction that committed the request. If the worker retries, it finds the existing taskId and resumes polling instead of creating a new task.
hiapi auth failures return HTTP 200 with the error payload embedded:
{"error":{"code":"permission_denied","message":"This API key cannot use the selected model. ...","type":"hiapi_error"}}
So a 200 status alone does not mean success. Always check body.error first; if it exists, look at error.code (permission_denied, invalid_request, etc.) and error.message. The request_id in the message is what you paste into a support ticket if you need help.
data.output[0].url is a short-lived URL — the expireAt is in the response. Don't ship the hiapi URL into <img src> in your product; download the bytes as soon as the task succeeds, upload them to your own storage (S3, R2, Supabase Storage, etc.), and serve from there.
A reasonable default is a 60-second timeout on the POST (creating the task is fast — you're really just waiting for the API to acknowledge it) and a 10-minute total budget for the polling loop with a 5-second cadence. Tasks that take longer than 10 minutes almost always mean the task quietly went into fail, so timing out and surfacing the last task body in your logs is more useful than waiting indefinitely.
gpt-image-2 and gpt-image-2/text-to-image?gpt-image-2 is the model family; gpt-image-2/text-to-image is the specific endpoint you call to turn a text prompt into a new image. Other variants in the family expose different input schemas (for example, image edits accept reference images). When in doubt, use the bare model id you see on the model page.
aspect_ratio: auto do?auto lets the model decide the aspect ratio from the prompt. Use it when you don't have a strong layout requirement; otherwise pin the ratio yourself so downstream cropping is predictable.
permission_denied?Either the API key is wrong (typo, revoked, missing sk- prefix) or the key's plan doesn't include gpt-image-2/text-to-image. Open the API Keys dashboard, confirm the key string, and check your plan on the pricing page. Always check body.error.code — never trust the HTTP status alone.
gpt-image-2/text-to-image produces one image per task — data.output is an array, but for this model it always has length 1. To generate N variations, fire N tasks concurrently and use the callback pattern above so you don't keep N worker threads parked.
The unified task API does not currently expose a cancel endpoint. The simplest mitigation is to make your client-side worker drop the result if the user no longer wants it — you'll still be billed for the generation that already ran, so the right place to enforce cancellation is before the POST /v1/tasks call.
Change model to the new bare id (for example z-image/text-to-image) and adjust input to match that model's schema. Schemas are not identical across models — some accept size, some accept aspect_ratio, and a few do not accept resolution. The models catalog lists every available model with a per-model page that documents its input fields.