Skip to main content
MiniMax H3

H3 from a script: the request that works, the response you actually get, and the error codes worth handling

Quick answer: submit a job, poll it, download from content_url. The response is wrapped as {object: "video.job", job: {...}} with snake_case fields, credits_reserved is a string of internal units (divide by 100 for credits), and the REST route takes no idempotency key, so a dropped network can bill you twice.

The API for this model is small: submit a job, poll it, download the file. The reason people get stuck is not the shape of the calls — it is the details that differ between the marketing page and the contract, so this post is the contract, with the response shape as returned rather than as illustrated.

Submit

POST https://api.callirra.com/v1/videos
Authorization: Bearer sk-cal-...
Content-Type: application/json
{
  "model": "minimax-h3",
  "mode": "image_to_video",
  "prompt": "Slow dolly right at counter height; the light shifts to a warm rim; no cuts.",
  "resolution": "768P",
  "duration_seconds": 5,
  "aspect_ratio": "16:9",
  "frame_images": ["https://example.com/product-01.jpg"]
}

Field notes that are worth the two minutes:

  • mode is text_to_video, image_to_video or video_to_video. If you omit it, the route assumes text-to-video.
  • duration_seconds is the current name; duration is accepted as an alias, so older examples still run.
  • resolution is matched case-insensitively on the way in (768p and 768P both land on the 768P tier).
  • frame_images and input_references are arrays of URLs.
  • generate_audio is a hint, not a switch: the output track is kept either way.

The response is wrapped

What comes back is not the job at the top level:

{
  "object": "video.job",
  "job": {
    "id": "vid_...",
    "model": "minimax-h3",
    "status": "queued",
    "resolution": "768P",
    "duration_seconds": 5,
    "credits_reserved": "1000",
    "error_code": null,
    "content_url": null
  }
}

Every field in that view is snake_case, including duration_seconds, credits_reserved and content_url. If you copy a request example from somewhere else with camelCase in the response, that is where it breaks.

credits_reserved is a string of internal units. Divide by 100 for public credits — "1000" is 10 credits, which is five seconds at 768P. Treat it as an opaque string; large reservations are why it is not a JSON number.

Poll

GET https://api.callirra.com/v1/videos/:id

Same envelope, with the job filled in as it progresses: queued → running → completed, or one of failed, cancelled, expired. When it completes, content_url is a short-lived signed URL to the file, and config carries the effective parameters — useful for logging what was actually run, rather than what you think you sent.

When something fails, read error_code and then failure_reason: the code is the category the pipeline acted on, and the reason is the translatable detail, with failure_subject naming which attached file it concerned. That pair is what a user-facing message should be built from — not the raw upstream string.

let state;
for (let i = 0; i < 120; i += 1) {
  const r = await fetch(`https://api.callirra.com/v1/videos/${job.id}`, {
    headers: { authorization: `Bearer ${process.env.CALLIRRA_API_KEY}` },
  });
  state = (await r.json()).job;
  if (['completed', 'failed', 'cancelled', 'expired'].includes(state.status)) break;
  await new Promise((done) => setTimeout(done, 5000));
}

Five seconds between polls is fine; these are minute-scale jobs, not sub-second ones. For a long-lived pipeline, prefer the binary route — GET /v1/videos/:id/content streams the MP4 with your key attached, so you do not have to chase a signed URL that may expire before your retry logic gets to it.

The five failures worth handling

CodeWhat happenedWhat to do
content_policythe provider's safety check rejected the outputdo not retry the same prompt; the reservation is released
invalid_request_errorHTTP 400 before job creation — bad enum or unsupported combinationfix the payload; no job was reserved
upstream_errorupstream submission or generation failed after job creationinspect failure_reason; correct rejected input before retrying
job_timeoutthe job reached status: expired without completingreconcile the terminal job before resubmitting
cancelledyou cancelled it, or your session didreservation returned

The distinction that saves the most support tickets: invalid combinations are rejected before job creation with HTTP 400 and invalid_request_error. A 21:9 request at 1080P, or a two-second request at 2K, comes back this way, and the message names the constraint. upstream_error belongs to an existing failed job and is not a synonym for input validation.

Idempotency, honestly

The REST video route does not take a requestId. If your network drops after submit, you may create a second job and pay twice. Guard that on your side: write the job id you received before you do anything else with the response, and reconcile by listing your recent jobs with GET /v1/videos if you are unsure.

The MCP create_video tool has the same limitation: it accepts no caller requestId and does not automatically retry creation. If a bounded wait times out after returning a job id, resume that job with get_task; if submission itself is uncertain, reconcile recent jobs before creating another one.

CLI and MCP

The same operations exist as terminal commands and as MCP tools, against the same key and the same credit pool: submit, follow, download. Nothing about H3 is special-cased there; what changes is that an agent can call it without you writing a poll loop.

FAQ

Why is credits_reserved a string? It holds internal units and large reservations do not fit comfortably in a JSON number. Divide by 100 for public credits.

What does upstream_error mean? Upstream submission or generation failed after a job was created. Inspect failure_reason before retrying. Unsupported tier combinations instead return HTTP 400 with invalid_request_error before job creation.

How do I avoid paying twice after a network drop? Neither REST video creation nor MCP create_video accepts a caller requestId. Save any returned job id and resume it; reconcile recent jobs before repeating an uncertain submission.

Where this runs

The video contract is documented at https://callirra.com/api, including the route families and the response envelope, and the H3-specific tier matrix — which combinations are legal — is on https://callirra.com/minimax-h3.

I work on Callirra, which is why I can tell you the response is wrapped rather than flat: the marketing page's example and the route disagree, and the route wins. If you hit a payload that the matrix should accept and the API rejects, the payload plus the message is enough for me to trace it.