Tutorial
How to Call flux-1-kontext-max on HiAPI: curl and Python Tutorial
A working request walkthrough: inputs, async polling, and output download
What the flux-1-kontext-max request looks like
Before writing any code, it helps to know the shape of the interaction. An image-editing call to flux-1-kontext-max on HiAPI is not a plain request/response where the image comes back in the same HTTP call. It is a two-phase job:
- Submit — you POST a JSON payload describing your input image and the edit you want. The API accepts the job and returns a job identifier.
- Poll — you GET the job status by that identifier until it reports completion, then read the output URL from the response.
This pattern exists because image editing is compute-heavy. Holding a single HTTP connection open for the whole render is fragile; a job handle lets you disconnect, retry, and come back later.
The payload itself has three conceptual parts:
- The model selector — a field identifying
flux-1-kontext-maxas the model to run. - The input — typically an image (a URL or base64 payload, depending on the documented schema) plus a text instruction describing the edit.
- Optional controls — parameters that adjust generation behavior, such as aspect ratio or seed, where the model documents them.
Everything else — auth headers, the submit endpoint, the status endpoint — is the same plumbing you use for every HiAPI job. Once you internalize that three-step rhythm (submit → poll → download), the curl and Python versions become interchangeable, and you can move between them without relearning the flow.
Prerequisites and authentication
You need a few things in place before the first request succeeds.
What you need
- A HiAPI account and an API key created from your dashboard.
- The base URL for the HiAPI API, as shown in the platform documentation.
- A source image reachable by the API — most commonly a public HTTPS URL. If your image is private, upload it somewhere the API can fetch, or pass it inline if the model's schema supports base64 input.
curlfor the shell walkthrough, and Python 3 with therequestslibrary for the code walkthrough.
Authentication
HiAPI authenticates requests with your API key sent in an authorization header. The convention is a bearer token:
Authorization: Bearer $HIAPI_API_KEY
And every JSON request should declare its content type:
Content-Type: application/json
Keep the key out of source code
Store the key in an environment variable rather than pasting it into scripts:
export HIAPI_API_KEY="your_key_here"
In Python, read it with os.environ["HIAPI_API_KEY"]. In curl, reference it as $HIAPI_API_KEY. This one habit prevents the most common cause of leaked credentials — a key committed to a repo and then rotated under pressure.
If a request returns an authentication error, check three things in order: the key exists and is active, the header is spelled exactly as documented, and no stray whitespace or newline got into the environment variable.
Sending the request with curl
Here is the submit call. Substitute your own image URL and edit instruction.
curl -X POST "$HIAPI_BASE_URL/jobs" \
-H "Authorization: Bearer $HIAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "flux-1-kontext-max",
"input": {
"image": "https://example.com/input.jpg",
"prompt": "Replace the sky with a dramatic sunset, keep the buildings unchanged"
}
}'
What each part does
-X POST— submission is a create operation, so it is a POST.- The endpoint — the jobs endpoint accepts new work. The base URL comes from your environment so the command is portable across setups.
Authorization— your bearer token, read from the environment variable rather than inlined.Content-Type: application/json— required; without it, servers may reject or misparse the body.-d— the JSON payload.modelselectsflux-1-kontext-max,input.imagepoints at your source, andinput.promptdescribes the edit.
Reading the response
A successful submission returns a JSON body containing a job identifier — commonly a field like id or job_id — and a status indicating the job is queued or processing. Save that identifier; polling depends on it.
curl -s -X POST "$HIAPI_BASE_URL/jobs" \
-H "Authorization: Bearer $HIAPI_API_KEY" \
-H "Content-Type: application/json" \
-d @payload.json | python -m json.tool
Piping through python -m json.tool pretty-prints the response so the job ID is easy to spot. Putting the body in payload.json keeps the command readable as your prompt grows.
At this point you have submitted work, not received an image. That comes after polling.
The same request in Python
The Python version carries the same logic, with nicer ergonomics for anything you plan to keep.
import os
import requests
BASE_URL = os.environ["HIAPI_BASE_URL"]
API_KEY = os.environ["HIAPI_API_KEY"]
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}
payload = {
"model": "flux-1-kontext-max",
"input": {
"image": "https://example.com/input.jpg",
"prompt": "Replace the sky with a dramatic sunset, keep the buildings unchanged",
},
}
response = requests.post(
f"{BASE_URL}/jobs",
headers=headers,
json=payload,
timeout=30,
)
response.raise_for_status()
job = response.json()
print("job id:", job["id"])
Why this mirrors the curl call
The json=payload argument serializes the dict and sets the content type for you — the same bytes curl sends. timeout=30 bounds the submission request so a hung connection fails loudly instead of stalling your process. raise_for_status() turns a 4xx or 5xx into an exception you can catch, rather than silently returning an error document.
Small differences that matter
- Dict instead of string — you build the payload as a Python object, which makes it easy to compute prompts or inject variables.
- Exception handling — wrap the call in
try/except requests.HTTPErrorto log the response body; error payloads usually explain what's wrong with the input. - No shell escaping — long prompts with quotes no longer need escaping, which removes a whole class of bugs.
The job ID returned here feeds directly into the polling step below. Nothing about the job changes because it was submitted from Python instead of curl.
Handling async jobs and polling for results
Submission returns quickly; the render does not. You wait by polling the status endpoint for your job ID.
The polling loop
import time
def wait_for_job(job_id, headers, interval=3, max_attempts=100):
url = f"{BASE_URL}/jobs/{job_id}"
for attempt in range(max_attempts):
r = requests.get(url, headers=headers, timeout=30)
r.raise_for_status()
data = r.json()
status = data.get("status")
if status == "succeeded" or status == "completed":
return data
if status == "failed" or status == "error":
raise RuntimeError(f"Job failed: {data}")
time.sleep(interval)
raise TimeoutError(f"Job {job_id} did not finish in time")
Interpreting job states
Expect a small vocabulary: a queued/pending state before work starts, a processing/running state while the model renders, and a terminal state — succeeded or failed. Some APIs use slightly different spellings; check the response you actually get and map those strings, don't assume. The critical rule: only treat a job as done when the status is a terminal success state and an output URL is present. A status endpoint that returns 200 does not mean the image is ready.
Polling etiquette
- Space your requests. A few seconds between polls is plenty for image jobs. Polling every 100 ms wastes rate limit and gains nothing.
- Cap the wait.
max_attemptsbounds the loop so a stuck job fails your code instead of hanging forever. - Back off on 429. If you hit a rate limit, sleep longer before retrying rather than hammering.
- Prefer webhooks if offered. If the platform documents a callback mechanism, it removes polling entirely and is friendlier at scale.
For shell testing, a quick loop works the same way:
JOB_ID="your_job_id"
while true; do
BODY=$(curl -s "$HIAPI_BASE_URL/jobs/$JOB_ID" -H "Authorization: Bearer $HIAPI_API_KEY")
echo "$BODY"
echo "$BODY" | grep -q '"succeeded"' && break
sleep 3
done
Once you break out of the loop, the terminal response holds the output reference. That is the next step.
Downloading and saving the output
The finished job response points at the rendered image, usually as a URL (sometimes a list). Retrieve it with a second request and write the bytes to disk.
import pathlib
def download_output(job_data, path="edit.png"):
out = job_data.get("output")
if isinstance(out, list):
out = out[0]
url = out["url"] if isinstance(out, dict) else out
r = requests.get(url, timeout=60)
r.raise_for_status()
pathlib.Path(path).write_bytes(r.content)
return path
Points to watch
- Don't hardcode the shape. Output fields vary between models — sometimes a string URL, sometimes an object with a
urlkey, sometimes a list. Inspect the real response and normalize before downloading. - Output URLs can expire. Treat the URL as short-lived: download promptly, or copy to your own storage immediately. Do not store the URL as your permanent record of the image.
- Verify the bytes. Check
Content-Typestarts withimage/and the file size is plausible; an error page saved as.pngis a silent failure. - Retry transient download errors. Wrap the GET in a small retry with backoff — signed URLs occasionally 403 if clocks drift, and networks blip.
For a quick manual check, curl works fine:
curl -L -o edit.png "$OUTPUT_URL"
The -L flag follows redirects, which CDN-hosted outputs often use. After the file lands, open it and confirm the edit actually matches the prompt — that is your end-to-end verification, and it's worth automating a visual or hash check in a pipeline where images must be regenerated on failure.
Common failure points and fixes
Most broken first integrations fail in one of a handful of ways.
Authentication and headers
- 401/403 — the key is wrong, expired, or has stray whitespace. Re-export the env var and confirm the header form matches the docs.
- Content-Type missing — sending JSON without the header can produce confusing 4xx errors. Include it, or use
json=in Python.
Input problems
- Image not reachable — the API fetches your image server-side. A
localhostURL or a link behind auth fails. Use a public HTTPS URL or inline base64 if supported. - Prompt ignored or rejected — vague instructions produce vague results. Describe what to change and what to keep, as in the sunset example. Very long prompts may be truncated.
- Unsupported parameters — passing fields the model doesn't accept can cause validation errors. Start with the minimal payload, then add documented controls one at a time.
Async handling mistakes
- Reading the image from the submit response — the submit call returns a job handle, not pixels. Poll first.
- Treating any 200 as done — the status endpoint returns 200 for a job that is still running. Check the status field and the presence of an output URL.
- Polling too fast or forever — add an interval and a cap, or you'll burn rate limit and hang your worker.
Download issues
- Saving HTML as an image — always check the content type and size.
- Expired output URL — download right after completion; don't queue the URL for later.
Structural mistakes
- Mismatched job ID — if you submit multiple jobs, tag each ID with your own request ID so polling results can't cross wires.
- No timeouts — every request should have one. Without it, one hung call can stall a batch.
Turning the walkthrough into a reusable helper
The three steps — submit, poll, download — belong in one function so callers don't repeat the choreography.
def edit_image(image_url, prompt, model="flux-1-kontext-max",
headers=None, poll_interval=3, max_attempts=100):
payload = {"model": model,
"input": {"image": image_url, "prompt": prompt}}
r = requests.post(f"{BASE_URL}/jobs", headers=headers,
json=payload, timeout=30)
r.raise_for_status()
job_id = r.json()["id"]
data = wait_for_job(job_id, headers,
interval=poll_interval,
max_attempts=max_attempts)
return download_output(data, path=f"{job_id}.png")
What makes it production-ready
- One entry point. Callers pass an image and a prompt, and get a file path or an exception. The workflow details stay hidden.
- Explicit failure. Every request raises on HTTP errors, and the poller raises on job failure or timeout, so your queue or caller can retry.
- Configurable timing.
poll_intervalandmax_attemptsare parameters, so you can loosen them for heavier edits without touching the logic. - Predictable output naming. Using the job ID as the filename makes it easy to correlate files with logs.
Where to take it next
Wrap the call with app-level concerns: logging job IDs and latencies, retrying failed submissions with jitter, and pushing downloaded bytes straight to object storage instead of local disk. If you handle many edits, submit a batch of jobs first and then poll them together — submissions are cheap, and concurrency lives in the waiting, not the uploading.
The flux-1-kontext-max page in the HiAPI docs is the reference for accepted fields and current parameters. Keep the helper thin, let the docs drive the schema, and the same function will cover exploratory scripts and real pipelines alike.
Related articles
View all articles →-
Tutorial
How to Call hailuo-2.3-fast/image-to-video via the HiAPI API: curl and Python
A first working request for the fast, low-cost image-to-video tier — from model ID to downloaded MP4
9 min read -
Tutorial
How to Use hailuo-2.3/text-to-video via the HiAPI API: curl, Python, and a Working Request
11 min read -
Tutorial
Kandinsky 6.0 Pro Text-to-Video on HiAPI: curl and Python Tutorial
A request-first walkthrough: build the prompt-to-clip call, submit it, poll for completion, and download the result
11 min read