
Generating a track with minimax-music-3 on hiapi is one POST /v1/tasks call plus a poll — the same async task pattern every model on the platform uses. This guide captures a real request and response (task creation, polling, and the final audio URL) so the code below is copy-paste runnable, not illustrative.
Create the task:
curl -X POST https://api.hiapi.ai/v1/tasks \
-H "Authorization: Bearer sk-<your-key>" \
-H "Content-Type: application/json" \
-d '{
"model": "minimax-music-3",
"input": {
"prompt": "chill lofi hip hop beat, mellow piano, soft drums",
"lyrics": "[Instrumental]"
}
}'
Response:
{"code":200,"data":{"taskId":"tk-hiapi-01M09A11R6WEC2JDQ8RK6E7N55"},"message":"success"}
Poll the task until it's done:
curl https://api.hiapi.ai/v1/tasks/tk-hiapi-01M09A11R6WEC2JDQ8RK6E7N55 \
-H "Authorization: Bearer sk-<your-key>"
While it's running, status moves through handling → archiving before landing on success (or failed). In testing, a track went from submitted to success in about two and a half minutes. The finished response:
{
"code": 200,
"data": {
"taskId": "tk-hiapi-01M09A11R6WEC2JDQ8RK6E7N55",
"status": "success",
"model": "minimax-music-3",
"created": 1787018905,
"completed": 1787019051,
"storage": "temp",
"output": [
{
"type": "audio",
"url": "https://temp.hiapi.ai/7c6ttvrbpt/01M09A11R6WEC2JDQ8RK6E7N55-0.wav",
"artifactId": "78585",
"expireAt": 1787623851
}
]
},
"message": "success"
}
output[0].url is the WAV file. Note expireAt — see the storage note below before you build anything that relies on this URL staying alive.
import time
import requests
API_KEY = "sk-<your-key>"
BASE_URL = "https://api.hiapi.ai/v1"
HEADERS = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
def create_track(prompt: str, lyrics: str = "[Instrumental]") -> str:
resp = requests.post(
f"{BASE_URL}/tasks",
headers=HEADERS,
json={"model": "minimax-music-3", "input": {"prompt": prompt, "lyrics": lyrics}},
timeout=30,
)
body = resp.json()
if resp.status_code != 200 or "error" in body:
raise RuntimeError(f"create failed: {body}")
return body["data"]["taskId"]
def wait_for_track(task_id: str, timeout_s: int = 300, interval_s: int = 6) -> str:
deadline = time.time() + timeout_s
while time.time() < deadline:
resp = requests.get(f"{BASE_URL}/tasks/{task_id}", headers=HEADERS, timeout=30)
data = resp.json()["data"]
if data["status"] == "success":
return data["output"][0]["url"]
if data["status"] == "failed":
raise RuntimeError(f"task {task_id} failed: {data}")
time.sleep(interval_s)
raise TimeoutError(f"task {task_id} still running after {timeout_s}s")
if __name__ == "__main__":
task_id = create_track("upbeat synthwave, driving bassline, retro 80s")
audio_url = wait_for_track(task_id)
audio = requests.get(audio_url, timeout=60).content
with open("track.wav", "wb") as f:
f.write(audio)
print(f"saved track.wav from task {task_id}")
The input schema is strict — send an unlisted field and the API rejects the whole request before it ever queues:
prompt (string, required) — genre, mood, instrumentation. This is what actually shapes the track.lyrics (string, required) — always required, even for instrumental tracks. Pass "[Instrumental]" as a placeholder; for sung tracks, use section tags like [Verse] / [Chorus].callback (object, optional) — {"url": "...", "when": "final"}. "final" is the only supported value for when; anything else 400s with invalid callback.when: only 'final' is supported.Send anything outside those three top-level keys — a sample_rate, a duration, a typo — and you get:
{"code":400,"data":null,"error_code":"INVALID_REQUEST","message":"invalid input: <root>: additional properties 'sample_rate' not allowed"}
{"code":400,"error_code":"INVALID_REQUEST","message":"..."} at the top level. An auth/permission error comes back as {"error":{"code":"permission_denied","message":"...","request_id":"..."}} — a nested error object, HTTP 401. Check for both shapes in your error handling; code that only checks resp["code"] will miss the 401 case.permission_denied means the key, not the request. If prompt/lyrics are present and correctly typed but you still get a 401 permission_denied, the API key itself doesn't have this model enabled — check it in the dashboard rather than re-reading your JSON.storage: "temp" and the expireAt unix timestamp on each output — in the captured example, about 7 days after creation. Download the file or push it to your own storage as soon as the task succeeds; don't store the temp.hiapi.ai URL as if it were permanent.callback.url and when: "final" and let hiapi push the result to you once, when the task actually finishes.taskId, not after. A network error or 5xx on the initial POST /v1/tasks is safe to retry — nothing was created yet. Once you have a taskId, resubmitting the same prompt creates a second, unrelated track; poll or wait for the callback instead.Do I need to provide lyrics for an instrumental track?
Yes — lyrics is a required field regardless of whether you want vocals. Pass "[Instrumental]" and the model generates without singing.
Can I avoid polling entirely?
Yes, set callback: {"url": "https://your-endpoint", "when": "final"} when you create the task. hiapi POSTs the same payload you'd get from GET /v1/tasks/:id to your URL once the task reaches a terminal state. "final" is currently the only supported value for when.
Why do I get "additional properties not allowed"?
The input schema for minimax-music-3 only accepts prompt and lyrics inside input (plus the top-level callback). Any other field name — even one that's valid on a different hiapi model — gets rejected before the request runs.
Why does the key that works for other models 401 on this one?
Model access is per-key. A 401 with error_code: "permission_denied" means this specific key hasn't been granted minimax-music-3 — enable it in the dashboard, it's not a bug in your request body.
How long can I wait before downloading the output file?
Don't wait — output[0].url is a temporary link (expireAt is roughly a week out in practice). Treat it as a one-time handoff: download or re-upload immediately after the task succeeds.