Skip to content
English

SDKs

HiAPI’s official SDKs submit a generation task, poll it until it finishes, and return a typed task object containing the output URLs — with opt-in idempotent retries, typed errors, and a webhook verification helper, so you don’t have to implement polling, backoff, or response parsing yourself.

Python

Python 3.8+. Type hints throughout (ships py.typed) and a synchronous run() helper — fits web backends, notebooks, and automation scripts.

pip install hiapi · v0.2.1 · PyPI · GitHub

Go

Go 1.21+. Standard-library-only client; every task API call takes a context.Context, and the client is safe for concurrent use by multiple goroutines.

go get github.com/HiAPIAI/hiapi-go · v0.2.1 · pkg.go.dev · GitHub

Java

Java 11+. JDK-only client (java.net.http.HttpClient) with immutable response objects and no runtime dependencies to manage.

ai.hiapi:hiapi · v0.2.1 · Maven Central · GitHub

Node.js

Node.js 18.17+. Zero-dependency client built on the global fetch; TypeScript-first with full type declarations, shipping both ESM and CommonJS entry points.

npm install hiapi_ai · v0.2.1 · npm · GitHub

Don’t see your language? Call the Unified Async API directly with any HTTP client — it’s what every SDK wraps under the hood.

  • One call, start to finish. run submits a task and polls it for you — or use create / retrieve / list / wait (waitFor in Java) directly for full control over the lifecycle.
  • Opt-in idempotent retries. Pass an idempotency key and a dropped connection becomes safe to retry, instead of risking a second, duplicate-billed task. Off by default — you choose when to use it.
  • Model routes, no string-building. Where a model offers more than one processing option, pick it with a plain parameter instead of hand-assembling a model@route name.
  • Typed errors. Common failures — bad auth, an unavailable model, a timeout, a conflicting idempotency key — map to their own catchable error per language, so you’re not parsing a generic HTTP exception for the common cases.
  • Webhook verification. A helper checks the callback signature and timestamp for you — no hand-rolled HMAC comparison.

Before you start: install the SDK above, then get an API key from your HiAPI account and make sure it has balance — a generation request bills your account, so check the model’s price on its model page first.

Submit a task, follow its progress, and read the result:

from hiapi import HiAPI
# api_key is shown here for clarity; in production, read it from an
# environment variable (HIAPI_API_KEY) instead of hardcoding it.
client = HiAPI(api_key="sk-...")
task = client.tasks.run(
model="happyhorse-1-0",
input={"prompt": "a cyan glass data center entrance", "duration": 5, "resolution": "720p"},
on_update=lambda t: print("status:", t.status),
)
for out in task.output:
print(out.type, out.url)

run polls until the task succeeds, fails, or the client-side timeout expires (10 minutes by default). A timeout stops the SDK from polling — it does not cancel the task, which may still finish and bill your account. On a timeout, use the task ID to call retrieve later instead of submitting the same request again. For full control over the lifecycle, use create / retrieve / list / wait (waitFor in Java) directly — see each SDK’s README: Python · Go · Java · Node.js.

The output URLs above are temporary — HiAPI auto-deletes them about 7 days after creation. In Node.js, pass storage: "persistent" when creating the task to keep its outputs long-term (billed by size; insufficient balance silently downgrades to "temp" — check task.storage for the tier actually used). The Python, Go, and Java SDKs don’t expose this at creation time yet; to keep an output past that window, promote it (or manage it from the dashboard) before it expires. See Output Storage for retention tiers and pricing.

You now have a working call. The sections below cover what you’ll want once it’s running in production: selecting a model route, making retries safe, verifying webhook callbacks, and handling errors.

Some model pages list more than one processing option — e.g. ext — at different prices or availability. Pass the exact route value shown on that page as a parameter, instead of hand-assembling a model@route name:

created = client.tasks.create(
model="gpt-image-2/text-to-image",
route="ext", # preferred over the old "model@ext" suffix spelling
input={"prompt": "..."},
)

Omitting the route (or passing "default") uses the model’s default route. An unknown route is rejected — before a task is created or billed — with a 400 listing the available routes. The older x@ext suffix spelling still works if you have it in existing code.

Pass an idempotency key (sent as the Idempotency-Key header, ≤255 bytes) so a retried submission can’t create a second, duplicate-billed task. Use one stable key per logical job — for example, derived from your own order or job ID:

created = client.tasks.create(
model="seedance-2.0",
input={"prompt": "..."},
idempotency_key="order-8472:video", # a stable key you derive per job
)
if created.idempotent_replay:
print("this returned the task from an earlier request; no new task was created")

How the guarantee works: the first accepted request for a key creates and bills one task. Retrying the same request body with the same key returns that same task (idempotent_replay is true) and does not create or bill a second one. Keys are cleaned up roughly 24 hours after creation — treat that as “about a day,” not a precise cutoff you can time requests against — after which the same request creates a new task. Pick a key that’s unique to the job, not one you intend to reuse indefinitely.

With a key set, the SDK also retries submission on network errors, and retries 409 IDEMPOTENCY_KEY_PROCESSING (the first request with this key is still in flight) up to the client’s configured retry limit — it does not wait indefinitely. Reusing a key with a different request body fails with a dedicated, non-retryable error (see the table below): that means the key was generated incorrectly and needs fixing, not retrying as-is.

Pass callback.url (a public HTTPS URL) when you create a task and HiAPI sends a POST there when the task finishes, success or fail — no polling needed. If you also set a webhook signing key in your HiAPI account settings, that request is signed; verify it against the raw request body before trusting it:

# Flask example
from flask import Flask, request
from hiapi import HiAPI, WebhookVerificationError
app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 1024 * 1024 # 1 MiB — reject oversized bodies before reading them
client = HiAPI(api_key="sk-...", webhook_secret="whsec_...")
@app.post("/hiapi/callback")
def callback():
try:
task = client.webhooks.verify(request.get_data(), request.headers)
except WebhookVerificationError:
return "", 400
# Process idempotently — e.g. upsert your own record keyed by
# task.task_id — so a duplicate delivery of this event is harmless.
if task.succeeded and task.output:
print(task.output[0].url)
return "", 200 # ack only after your side effect has succeeded

verify checks the signature and rejects timestamps outside a 300-second window — it does not deduplicate deliveries. Callbacks are sent at least once and can arrive concurrently, so make your handler idempotent, as in the examples above: key your side effect on the task ID (e.g. an upsert) and return 2xx only after it has succeeded. Duplicates are then harmless, and a failed or crashed handler is simply redelivered — redelivery acts as your retry.

One thing not to do: writing a permanent “already handled” marker before processing looks like a tidy dedup, but it loses events — if the process crashes after writing the marker, the redelivery sees “already handled” and skips the event forever. If you need to stop two concurrent deliveries from running an expensive side effect twice, use a processing state with an expiry/lease and mark it done only after success, rather than a permanent up-front claim.

Common failures map to their own catchable error per language — pick your language above to see the exact type. Two boundaries to keep in mind:

  • The error-code rows below apply to synchronous non-2xx API responses. A task that fails during polling always surfaces as the polled-failure error — TaskFailed (Python), *TaskFailedError (Go), TaskFailedException (Java), TaskFailedError (Node.js) — read its code field for the underlying reason (which may be TASK_TIMEOUT, STORAGE_UNAVAILABLE, etc.).
  • Less common responses without a dedicated type — e.g. 402 insufficient balance, 403, or 429 after retries are exhausted — surface as each SDK’s base API error, which carries the HTTP status and raw response.
WhenPythonGoJavaNode.js
401 — bad/missing API keyAuthenticationErrorErrAuthenticationAuthenticationExceptionAuthenticationError
404 — unknown task, or not yoursNotFoundErrorErrNotFoundNotFoundExceptionNotFoundError
INVALID_REQUEST — fix the requestInvalidRequestErrorErrInvalidRequestInvalidRequestExceptionInvalidRequestError
MODEL_UNAVAILABLE — retry or switch modelModelUnavailableErrorErrModelUnavailableModelUnavailableExceptionModelUnavailableError
TASK_FAILED — the submission was rejected synchronouslyTaskFailedErrorErrTaskFailedSyncAPIException (check getErrorCode())TaskFailedSyncError
TASK_TIMEOUT — the upstream task itself timed out (server-side)TaskTimeoutErrorErrTaskTimeoutTaskTimeoutExceptionTaskTimeoutError
STORAGE_UNAVAILABLE — output storage errorStorageUnavailableErrorErrStorageUnavailableStorageUnavailableExceptionStorageUnavailableError
503 — platform busy (retried automatically)ServiceUnavailableErrorErrServiceUnavailableServiceUnavailableExceptionServiceUnavailableError
409 — same idempotency key still in flight (retried up to the retry limit)IdempotencyKeyProcessingErrorErrIdempotencyKeyProcessingIdempotencyKeyProcessingExceptionIdempotencyKeyProcessingError
422 — key reused with a different body (not retryable)IdempotencyKeyMismatchErrorErrIdempotencyKeyMismatchIdempotencyKeyMismatchExceptionIdempotencyKeyMismatchError
a polled task ended in status=failTaskFailed*TaskFailedErrorTaskFailedExceptionTaskFailedError
run / wait exceeded its client-side timeoutPollTimeout*PollTimeoutErrorPollTimeoutExceptionPollTimeoutError
network failure (retried automatically for reads only — not a keyless submit)APIConnectionError*ConnectionErrorAPIConnectionExceptionAPIConnectionError
run was aborted immediately after creating a taskRunAbortedError
run failed while polling or running onUpdate after creationRunFailedError

In Go, the Err* values are sentinels for errors.Is — match categories that way, and use errors.As to pull the full *APIError (status, error code, raw body) when you need it. In Node.js, mind the two similar names: TaskFailedSyncError is the synchronous TASK_FAILED rejection, while TaskFailedError is a polled task ending in fail — the reverse of Python, where TaskFailedError is the synchronous one. Node.js RunAbortedError and RunFailedError both carry the created task’s taskId; that task may still run and bill, so call retrieve(taskId) later instead of resubmitting it. RunFailedError.cause preserves the polling, callback, or abort error that interrupted run. 429/503 responses, and network errors on idempotent calls, are retried automatically with backoff. See each SDK’s README for the full error hierarchy and client configuration: Python · Go · Java · Node.js.