HiAPI
Blog

Tutorial

How to Use recraft/creative-upscale via the HiAPI API: curl and Python Request Guide

Nov 3, 2026 17 min read
文章封面图:深青底色上以扁平几何切面构成的水晶矿物剖面,青绿双色点缀,印刷图版风格。

What recraft/creative-upscale Does in a Developer Workflow

recraft/creative-upscale is an image-to-image endpoint: you send an existing image, and the model returns a larger, model-reconstructed version of it. The important word here is creative. A classic upscale step (bilinear, Lanczos, or a generic super-resolution model) tries to preserve the input as faithfully as possible. A creative upscale is allowed to regenerate detail that was never in the source pixels — edges get re-synthesized, texture is inferred, and small or soft regions can be sharpened into something plausible rather than merely interpolated.

That makes it a different tool from a resize, and it belongs at a different point in a pipeline.

When to reach for it

  • Asset promotion. A thumbnail or a legacy product shot that has to live at a larger display size, where a plain resize would look soft next to native-resolution neighbors.
  • Reference and mockup work. Concept art, storyboards, or placeholder imagery that needs to survive being shown large without exposing its low-resolution origin.
  • Upscaling inside a generation loop. You produced an image at a small working size and want a final asset without rerunning the whole generation at a bigger canvas.

When not to reach for it

If the requirement is byte-exact fidelity — an archival scan, a document, a texture atlas where pixel values are data — do not use a creative model. The regenerated detail is a plausible guess, not a reconstruction, and for measurement or archival purposes that guess is a defect. Use a deterministic resampler there and keep the creative model for perceptual output.

The shape of the call

The endpoint follows the same request pattern as other HiAPI image endpoints: an authenticated JSON POST to the model path, with an input image and a parameters object. If you have already integrated any other image model on the platform, the mental model transfers directly — which is the point of routing these calls through one interface. One working curl call becomes the template for every subsequent image endpoint you touch.

A useful way to think about the step: it consumes an image you already trust (composition, content, color are settled) and returns a variant that is bigger and more detailed. Content decisions belong upstream; recraft/creative-upscale is the finishing stage.

Prerequisites and Endpoint Setup

Before writing the first request, collect four things. None of them require a special SDK — the whole integration is plain HTTPS with a JSON body.

1. An API key

You need a HiAPI API key with access to image models. Treat it as a secret: load it from an environment variable rather than pasting it into a script, and never commit it.

export HIAPI_API_KEY="your-key-here"

The key travels in the request headers, not in the URL and not in the JSON body. Keeping it out of query strings also keeps it out of shell history and proxy logs.

2. The base URL

The base URL is your HiAPI API host. All model calls are relative paths under it, so you can set it once as a variable and reuse it in every script:

export HIAPI_BASE_URL="https://api.hiapi.ai"

Check the platform's API reference for the canonical base URL and path prefix for your account, and store whatever it gives you as this single variable. That way, a host or version change is a one-line edit.

3. The model identifier

The model ID is a path segment, spelled exactly as published: recraft/creative-upscale. It contains a slash, which matters in some tooling — more on that in the debugging section. Case and hyphenation count. A single wrong character produces a model-not-found error that looks like an auth problem if you skim the message.

4. A test image

Have one image ready on disk and know its exact path and MIME type. Use a small file for the first run so that a failure is unambiguously a request problem and not a payload-size or timeout problem. You will reuse this same image later for comparison runs, so pick something with visible fine detail (fabric, foliage, text-free signage) rather than a flat gradient.

Header checklist

Every request from here on carries the same headers:

  • Authorization: Bearer $HIAPI_API_KEY
  • Content-Type: application/json

If you send an image as base64 or as a data URI inside the JSON body, those two headers are sufficient. If you instead upload the file to obtain a URL first, do that upload as its own call and keep the resulting URL separate from the model request.

With the key, base URL, model ID, and one test image in hand, you can make a real call in under a minute.

A Working Request with curl

Here is a single runnable call. Replace the image value with your own input — either a publicly reachable URL or a base64-encoded data URI, depending on what the model's input schema accepts.

curl -sS -X POST "$HIAPI_BASE_URL/v1/models/recraft/creative-upscale" \
  -H "Authorization: Bearer $HIAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "input": {
      "image": "https://example.com/test-input.png"
    },
    "parameters": {
      "upscale": 2
    }
  }' \
  -o response.json

Writing the response to a file with -o rather than letting it stream to the terminal keeps the JSON intact for parsing and makes the call scriptable.

What each part does

  • -X POST — the endpoint is a POST; a GET returns a method error that is easy to misread as a permissions issue.
  • The URL — base URL, then the model's path with the full model ID. Because the ID itself contains a slash, the path has two segments after the model collection; keep it as one string and do not let a client library re-encode it into recraft%2Fcreative-upscale unless the documentation says that is expected.
  • Authorization — the bearer token. If you see a 401 on this exact command, the key or the header format is wrong; nothing else in the body is being validated yet.
  • Content-Type: application/json — required. Without it some gateways treat the body as form data and the request never reaches the model.
  • input.image — the source image. Prefer a URL while you are iterating: it keeps the command short and diffs cleanly.
  • parameters.upscale — the upscaling control. Start with a modest factor, confirm the response, then raise it.

Two practical variants

To send a local file as base64 instead of a URL:

B64=$(base64 -w0 ./test-input.png)

curl -sS -X POST "$HIAPI_BASE_URL/v1/models/recraft/creative-upscale" \
  -H "Authorization: Bearer $HIAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"input\": { \"image\": \"data:image/png;base64,${B64}\" },
    \"parameters\": { \"upscale\": 2 }
  }" \
  -o response.json

And to see the HTTP status alongside the body while debugging, add -w '\nHTTP %{http_code}\n' and drop -o.

Treat this one command as your template. Once it returns an image, every later change — a different factor, a different source image — is an edit to two fields, not a new integration.

The Same Request in Python

The Python version should not be a copy-paste of the curl body; it should be a small reusable function so that later comparison runs are one call each. Standard library only — no SDK required.

import base64
import json
import os
import pathlib
import urllib.request

HIAPI_BASE_URL = os.environ["HIAPI_BASE_URL"].rstrip("/")
HIAPI_API_KEY = os.environ["HIAPI_API_KEY"]
MODEL = "recraft/creative-upscale"


def creative_upscale(image: str, upscale: int = 2, timeout: int = 120) -> dict:
    """Send one image to recraft/creative-upscale and return the parsed JSON."""
    payload = {
        "input": {"image": image},
        "parameters": {"upscale": upscale},
    }
    request = urllib.request.Request(
        url=f"{HIAPI_BASE_URL}/v1/models/{MODEL}",
        data=json.dumps(payload).encode("utf-8"),
        headers={
            "Authorization": f"Bearer {HIAPI_API_KEY}",
            "Content-Type": "application/json",
        },
        method="POST",
    )
    with urllib.request.urlopen(request, timeout=timeout) as response:
        return json.loads(response.read().decode("utf-8"))


def as_data_uri(path: str) -> str:
    data = pathlib.Path(path).read_bytes()
    mime = "image/png" if path.lower().endswith(".png") else "image/jpeg"
    return f"data:{mime};base64,{base64.b64encode(data).decode('ascii')}"


if __name__ == "__main__":
    result = creative_upscale(as_data_uri("test-input.png"), upscale=2)
    print(json.dumps(result, indent=2)[:2000])

Why this shape

  • Configuration from the environment. Base URL and key are read once at import, so nothing secret appears in the file and switching environments is an export away.
  • One function, one job. creative_upscale takes an image reference and a factor and returns parsed JSON. Everything about parameters stays visible at the call site.
  • Explicit timeout. Image models take longer than text endpoints. Set the timeout deliberately instead of inheriting a default that is too short, and keep it long enough that slow-but-successful calls are not retried unnecessarily.
  • A helper for local files. as_data_uri means you can pass either a URL or a local path without touching the request logic.

If you prefer a session-based client

The same call with httpx is a two-line swap — construct a client, set the default Authorization header, and post the JSON body. The important invariants do not change: the model ID stays a single unencoded path segment, the key stays in a header, and the body keeps the input / parameters split. Match those three and your client is interchangeable with anything else on the platform.

When you wire this into a pipeline, return the parsed dict and let the caller decide how to persist the image — that separation is what makes the next section's response handling reusable.

Upscale and Creativity Parameters

The values you send are less interesting than where you send them. Get the placement right first; tune the numbers second.

Two buckets, one contract

Every request separates into:

  • input — the image itself. The subject of the operation. Nothing about how the model behaves goes here.
  • parameters — the controls. The upscaling factor and the creativity setting both belong in this object, as siblings of each other.

This split is the contract. A first run should confirm that the model accepts your placement — that the call returns an image at all — before you start optimizing values. If a parameter lands in the wrong bucket, you will typically get the image back produced with defaults, or a validation error naming an unknown field. Either way it is a placement bug, not a quality bug, and chasing quality settings will not fix it.

The upscale factor

The factor controls output size relative to input. Reasoning about it:

  • It multiplies, so cost and latency grow with it. Going from a modest factor to the maximum is not a linear step in render time. Treat each step up as a new experiment, not a slider nudge.
  • The input resolution still matters. A creative model has more to work with when some structure exists. Extremely small inputs give it more freedom to invent, which is sometimes the goal and sometimes a surprise.
  • Pick the factor from the destination, not from the maximum. If the image needs to be 2× to fit its slot, request 2×. Requesting the maximum and downscaling afterward throws away the benefit and pays for it.

The creativity control

Creativity is the knob that decides how much the model may replace rather than enlarge. Low settings behave closer to a faithful upscale; higher settings let the model re-synthesize more aggressively, which can rescue a soft or blocky source and can equally introduce detail you did not intend.

Treat the parameter as a fidelity lever with a concrete reading for each end:

  • Preserve — logos, faces, product catalog images, anything where a viewer may compare against the original.
  • Regenerate — concept art, backgrounds, textures, decorative imagery where plausibility beats exactness.

Working method

Change one parameter at a time, keep the input fixed, and record the values alongside the output. A tiny matrix — the same image at two factors, each at two creativity settings — is enough to characterize the endpoint on your own content. Do not borrow someone else's "best" values; creative models interact strongly with input, and the right setting for a photograph is rarely the right setting for flat illustration.

If a value is rejected outright rather than producing a poor result, that is the contract telling you the domain of the parameter — check the endpoint's parameter reference for the accepted range rather than guessing.

Reading the Response and Saving the Image

Parse the response defensively. The payload type is part of the contract, but it can differ between a URL and an inline base64 blob, and your client should not fall over depending on which it receives.

First, look at the raw JSON

Before writing a parser, run the curl call once with -o response.json and read the file. Identify the field that carries the image. It is usually either:

  • a URL pointing to the generated file, or
  • an inline string, typically base64 or a data URI.

Also look for metadata worth logging: dimensions, the model ID echoed back, and any identifiers you can use when filing a support request.

A tolerant save function

import base64
import pathlib
import urllib.request


def extract_image(result: dict) -> str:
    """Find the image payload regardless of the shape returned."""
    for container in (result, result.get("data"), result.get("output")):
        if isinstance(container, list) and container:
            container = container[0]
        if isinstance(container, dict):
            for key in ("image", "url", "b64_json", "image_url"):
                value = container.get(key)
                if isinstance(value, str) and value:
                    return value
    raise ValueError(f"no image field in response: {sorted(result)}")


def save_image(payload: str, out_path: str = "upscaled.png") -> pathlib.Path:
    path = pathlib.Path(out_path)
    if payload.startswith("data:"):
        payload = payload.split(",", 1)[1]
        path.write_bytes(base64.b64decode(payload))
    elif payload.startswith("http://") or payload.startswith("https://"):
        with urllib.request.urlopen(payload) as response:
            path.write_bytes(response.read())
    else:
        path.write_bytes(base64.b64decode(payload))
    return path

The point is not that this is the only correct parser — it is that the function states its assumptions (data:, http(s)://, bare base64) and raises a clear error when none match. When the response shape changes, one function needs updating, not every script that consumes an image.

Saving safely in the curl flow

curl -sS -X POST "$HIAPI_BASE_URL/v1/models/recraft/creative-upscale" \
  -H "Authorization: Bearer $HIAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d @request.json -o response.json

python - <<'PY'
import json
from save_image import extract_image, save_image
result = json.load(open("response.json"))
print(save_image(extract_image(result), "out.png"))
PY

Two habits worth keeping: write to a new filename per run so a failed or partial write never overwrites a good result, and verify the output dimensions after saving — a successful HTTP 200 with an unexpected size means a parameter did not take effect, which is easier to catch here than three pipeline stages later.

Debugging Common Failures

Most first-run failures are boring. Work through this list in order; it covers the overwhelming majority of them.

401 / 403 — authentication

  • Is the key loaded in the same shell that runs the command? echo $HIAPI_API_KEY should print a non-empty value.
  • Is the header exactly Authorization: Bearer <key> — including the space and the word Bearer?
  • Did the key end up in the URL or the JSON body instead of the headers? Move it back.
  • Is the key scoped to image models? A key valid for one product area may not be valid for this endpoint.

404 — the model or route was not found

This is almost always the model ID. Compare character by character with the published identifier recraft/creative-upscale: the slash, the lowercase, and the hyphen in creative-upscale are all load-bearing. A related trap is client-side URL escaping — if you see recraft%2Fcreative-upscale on the wire and the call 404s, your HTTP layer encoded the slash; configure it to leave the path segment alone.

Also confirm you did not double up the path prefix (for example a base URL that already ends in the version segment and a path that repeats it). One variable for the base URL makes this easy to spot.

400 — the request was understood and rejected

Read the message rather than guessing. Common causes:

  • Image input malformed. Truncated base64 (often from a shell variable being cut, or a missing data:image/png;base64, prefix when the schema expects a full data URI), or a URL that is not publicly fetchable.
  • Parameter out of range or misplaced. A factor the model does not accept, or upscale sent at the top level instead of inside parameters.
  • Missing content type. A Content-Type of text/plain or absent entirely turns a JSON body into an unparseable document.

413 / 422 / timeouts — size and duration

A very large source image can fail validation before the model runs. Downscale the input modestly, retry, and confirm the request shape is sound before sending the full-resolution file. For timeouts, distinguish slow from failed: increase the client timeout once, and if the call then succeeds, the fix belongs in your client configuration, not in the payload.

A fast triage rule

If the error mentions auth, model, or input format, you have not reached the upscaling logic at all — nothing about creativity or factor values is involved yet. Fix the wrapper, retry the smallest possible request (one small test image, default parameters), and only then start varying settings. Debugging in that order prevents the classic mistake of tuning parameters to compensate for a broken header.

Building a Repeatable Comparison Test

Creative upscaling is judgment-heavy, so replace memory with a fixture. The goal is a small harness that answers "did this setting actually help?" without you re-eyeballing old files.

Fix the inputs

  • One test image, checked in. Pick something with recoverable detail — fabric, foliage, architectural edges. Commit it next to the script so every run and every teammate uses the same source.
  • Pin the parameters. Factors and creativity values live in a list in the script, not in your head or in a chat message.
  • Pin the model ID. Same reasoning as the header checklist: recraft/creative-upscale spelled once, in one constant.

A minimal matrix runner

import itertools, json, pathlib

FACTORS = [2, 4]
CREATIVITY = ["preserve", "regenerate"]  # use the exact accepted values from the docs

src = as_data_uri("fixtures/test-input.png")
out_dir = pathlib.Path("runs"); out_dir.mkdir(exist_ok=True)

for factor, mode in itertools.product(FACTORS, CREATIVITY):
    result = creative_upscale(src, upscale=factor, creativity=mode)
    name = f"f{factor}-{mode}"
    save_image(extract_image(result), out_dir / f"{name}.png")
    (out_dir / f"{name}.json").write_text(json.dumps(result, indent=2))
    print(name, "ok")

Two details matter more than the code. First, save the raw JSON response beside each image — when a result looks wrong, the response tells you whether the parameter was applied or silently ignored. Second, name files from the parameter values, so a directory listing is the matrix and you never wonder which file came from which setting.

Compare in a fixed way

  • View outputs at intended display size, not zoomed in. Creative detail that impresses at 400% can look artificial at the size a user will actually see.
  • Check dimensions programmatically as part of the run. A mismatch means the factor did not take effect, which is a bug report for your own request, not an aesthetic question.
  • Judge against a defined criterion: fidelity to the source versus perceived sharpness, chosen per asset class. Write that criterion down next to the matrix so the next person knows what "better" meant.

Once the matrix is stable, the harness becomes a regression test: rerun it after any change to your client code, and compare against the previous run directory. Because every call in this guide shares the same request shape as the platform's other image endpoints, the runner you build here generalizes to whichever model you integrate next.

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