HiAPI
Blog

Tutorial

How to Call flux-1-kontext-max on HiAPI: curl and Python Tutorial

A working request walkthrough: inputs, async polling, and output download

Oct 31, 2026 11 min read
hiapi.ai 博客封面图:深咖啡底色上交织的燕麦色与黄铜色宽织带,呈扁平编织图解式几何图案。

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:

  1. 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.
  2. 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-max as 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.
  • curl for the shell walkthrough, and Python 3 with the requests library 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. model selects flux-1-kontext-max, input.image points at your source, and input.prompt describes 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.HTTPError to 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_attempts bounds 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 url key, 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-Type starts with image/ and the file size is plausible; an error page saved as .png is 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 localhost URL 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_interval and max_attempts are 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 →

Start building with persistent AI artifacts

Get your API key and call image, video and audio models through one task-based API.

Read the API docs