HiAPI
Blog

Tutorial

recraft-v4-styles/text-to-image API Tutorial: curl, Python, and Async Polling

A working request walkthrough for style-reference image generation on HiAPI

Oct 31, 2026 11 min read
深蓝底色上铜色细分平行线构成几何带状图案,呼应异步轮询主题的博客封面图

What recraft-v4-styles/text-to-image Gives You

recraft-v4-styles/text-to-image is a style-reference image generation endpoint. You give it a text prompt describing what you want to see, plus one or more reference images that describe how it should look. The model blends the two: the prompt carries content and composition intent, the references carry visual style.

That distinction matters when you're picking an endpoint. A plain prompt-to-image call is the right tool when the look is fully expressible in words — "a flat vector illustration of a coffee cup, two colors, thick outlines." In practice, though, most product work involves a brand look that is hard to describe and easy to show. An existing key visual, a prior approved render, or a design system specimen is a far more precise style specification than a paragraph of adjectives. That's the gap this endpoint fills.

In developer terms, think of it as a two-input transform: prompt + style references -> image. Everything about integrating it follows from that shape. You are not just serializing a string; you are assembling a small, multi-part payload where the references are first-class inputs. Your request-building code should treat them that way, and your validation layer should reject a job that has a prompt but no usable style reference before it ever leaves your service.

The endpoint is asynchronous. A submission returns a job you then poll, and the finished image arrives as a URL rather than inline bytes. That's a deliberate design for image workloads: generation takes long enough that holding an HTTP connection open is wasteful and fragile. Once you accept the submit-poll-download lifecycle, the rest of the integration is ordinary API plumbing.

Anatomy of a Working Request

Before writing any code, get the field groups straight. A recraft-v4-styles/text-to-image request has three logical parts.

The prompt

The prompt is your content instruction. Write it for what should be in the frame — subject, composition, framing, count of objects, background treatment. Avoid spending prompt budget on style adjectives that the references already convey; conflicting signals between prompt and reference tend to produce muddy results. Keep prompts short, concrete, and testable.

Style reference inputs

This is the part that separates this endpoint from a plain text-to-image call. Style references are images supplied alongside the prompt that define the visual language. When assembling them, treat them as a curated set rather than a pile:

  • Consistency beats quantity. A few references that share one coherent look teach the model more than many references in different styles.
  • Resolution and framing matter. References should be representative of the output you want, not thumbnails or heavily cropped fragments.
  • They should be reachable. If your references are passed as URLs, those URLs must be publicly fetchable at request time. A signed link that expires in sixty seconds will fail a slow job.

Keep your reference set in configuration, not scattered through code, so you can version it alongside your brand assets.

Output options

The remaining fields describe the artifact you want back — the knobs that affect the returned image rather than its content. Exact parameter names and accepted values are listed on the endpoint's reference page; read them there rather than guessing, and centralize them in one place in your code so a spec change touches one file.

A useful habit: build the JSON body as a dictionary/object in your language of choice, log it once at debug level on the first run, and eyeball the structure before blaming the model for a bad result.

Calling It with curl

Start with curl. It removes every layer of library abstraction between you and the wire format, so if the request is wrong you find out immediately.

Submission is a POST with a JSON body and an auth header carrying your HiAPI API key:

curl -sS -X POST "https://api.hiapi.ai/v1/recraft-v4-styles/text-to-image" \
  -H "Authorization: Bearer $HIAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "a ceramic coffee cup on a linen tablecloth, soft daylight",
    "style_references": [
      "https://your-cdn.example.com/style/ref-01.png"
    ]
  }'

A few notes on what you're looking at:

  • Keep the key in an environment variable ($HIAPI_API_KEY), never inline in a shell history or a committed script.
  • The -d payload is plain JSON. Quoting rules in shell are the usual source of grief — if you get a parse error, echo the body to a file and use -d @body.json instead.
  • Field names shown here follow the request anatomy from the previous section; confirm exact names and accepted output options on the endpoint reference page before you ship.

The response is not an image. It is a job acknowledgement: an identifier for the generation you just queued and a status field telling you it hasn't finished yet. Read the response body carefully the first time and note the field name that holds the job ID — that string is your handle for everything that follows. Save it. If your first response contains an error object instead, stop and fix the request; polling an ID you never received is the most common dead end.

The Same Request in Python

Port the curl call to Python once and you have a reusable primitive. The example below uses requests and mirrors the body exactly.

import os
import requests

API_KEY = os.environ["HIAPI_API_KEY"]
BASE_URL = "https://api.hiapi.ai/v1"

HEADERS = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json",
}

STYLE_REFERENCES = [
    "https://your-cdn.example.com/style/ref-01.png",
]

def submit_generation(prompt: str) -> dict:
    body = {
        "prompt": prompt,
        "style_references": STYLE_REFERENCES,
    }
    resp = requests.post(
        f"{BASE_URL}/recraft-v4-styles/text-to-image",
        headers=HEADERS,
        json=body,
        timeout=30,
    )
    resp.raise_for_status()
    return resp.json()

if __name__ == "__main__":
    job = submit_generation(
        "a ceramic coffee cup on a linen tablecloth, soft daylight"
    )
    print(job)

Two design choices are worth keeping when you adapt this:

  • Always set a timeout. A submission should return quickly; if it hangs, you want an exception, not a stuck process.
  • Return the parsed JSON, not the response object. Downstream code should depend on the job payload, not on requests internals. That makes it trivial to swap the transport later.

Define the reference set at module or config level, as above. When the brand look evolves, you update one list rather than hunting through call sites. And keep the body construction in a single function so any field the endpoint requires is added in one place.

Polling for the Async Result

Generation is asynchronous, so the job you submitted moves through states before it's done. Your client needs a wait loop that checks status, backs off between checks, and gives up eventually.

import time

def wait_for_result(job_id: str, timeout_s: int = 300, interval_s: float = 3.0) -> dict:
    deadline = time.monotonic() + timeout_s
    delay = interval_s
    while time.monotonic() < deadline:
        resp = requests.get(
            f"{BASE_URL}/recraft-v4-styles/text-to-image/{job_id}",
            headers=HEADERS,
            timeout=30,
        )
        resp.raise_for_status()
        data = resp.json()
        status = data.get("status")
        if status in ("succeeded", "failed"):
            return data
        time.sleep(delay)
        delay = min(delay * 1.5, 15.0)
    raise TimeoutError(f"job {job_id} did not finish in {timeout_s}s")

What matters in this loop:

  • Treat unknown statuses as pending. Compare against the explicit terminal states rather than an allowlist of in-progress ones; if the platform adds an intermediate state, your loop keeps waiting instead of crashing.
  • Back off. A fixed one-second poll for five minutes is five minutes of wasted requests. Start modest, multiply, cap.
  • Bound the wait. Every loop needs a deadline, and the deadline should surface as an exception your caller can handle.
  • Branch on failure. A terminal failure state carries diagnostic information. Log the full response body — that's the difference between "it broke" and "the reference URL returned a 403."

If you're processing many images, don't sleep inside a web request handler. Submit, persist the job ID, and let a background worker or a scheduled task drive the polling. The endpoint reference page documents the precise status values and the poll path; check it whenever you upgrade your client.

Downloading and Storing the Output

When the job reaches its terminal success state, the response contains a URL for the generated image. That URL is the handoff point between the API and your storage layer.

Fetch it as bytes and write them where you need them:

def download_image(url: str, dest_path: str) -> None:
    resp = requests.get(url, timeout=60)
    resp.raise_for_status()
    with open(dest_path, "wb") as f:
        f.write(resp.content)

For anything beyond a local experiment, don't leave the artifact only at the returned URL. Treat generated images as ephemeral pointers and copy them into storage you control — an object store bucket, your CDN origin, or a media pipeline. Three reasons:

  1. Lifetime. Generated output URLs are not guaranteed to persist indefinitely. Re-downloading from a stale URL during a retry is a silent failure waiting to happen.
  2. Stable keys. Your application wants a deterministic identifier it can put in a database row, not a URL that changes on each regeneration.
  3. Provenance. Storing the prompt, the style reference set version, and the job ID alongside the image lets you reproduce or audit any asset later. Save that metadata in a sidecar record or a database column.

When uploading to object storage, stream the response body rather than buffering large images fully in memory, and set content type from the response headers. If you serve the image back to clients through your own CDN, this step is also where you apply caching headers.

Troubleshooting the First Run

Most first attempts fail in a handful of predictable ways. Check these in order.

The request body is rejected

A 400-class error usually means malformed JSON or a missing required field. Run the curl version first and inspect the exact body you send. Common culprits: shell quoting mangling the JSON, a field sent as a string that the API expects as an array, or an output option with a value outside the accepted set. The endpoint reference page is the source of truth for field names and values — diff your body against it literally.

You have a prompt but no usable style reference

This endpoint is built around style references. If your request includes a prompt but the references are empty, unreachable, or return an auth error when fetched, the job will either be rejected or produce output that ignores your intended look. Verify each reference URL by fetching it with the same credentials-free client the platform would use. A reference that requires a session cookie or a signed query string counts as broken.

You never got a job ID

The submission response is an acknowledgement, not the image. If you wrote code that expects image bytes in the first response, it will fail on every run. Parse the response, extract the job ID, and only then start polling. Log the full submission response on your first few runs so you can see its shape.

You are polling too aggressively

Hundreds of status checks in a minute will not make a job finish faster, and they make rate limiting more likely. Use increasing intervals with a cap, and a single worker per job. If you're fanning out many jobs, add a small concurrency limit so a burst of submissions doesn't turn into a burst of polls.

The job completes but the download fails

A 403 or 404 when fetching the output URL usually means the URL expired or the job failed after a status check you misread. Confirm the terminal state was success, then re-read the response for the actual output field. Store the artifact promptly once it's available rather than deferring the download.

Work through these in the order the pipeline runs — submit, poll, download — and the first-run failure almost always resolves into an obvious fix. The endpoint reference and its request schema are the fastest way to confirm any field-level doubt.

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