FoundationLesson 330 min

Your First Generation

By the end of this lesson you can
  • Submit a text-to-video task to POST /v2/video_generation and capture the task_id
  • Poll the query endpoint on the real five-value status enum until a terminal state
  • Retrieve the finished video directly from content.url in the query response

The shape of every H3 job

Every generation in this course follows one loop: create a task, poll its status, collect the result. Creation is asynchronous — you never get a video back from the creation call, only a receipt. Today you run that loop once, end to end, with the official example request.

First, the address. The base URL is:

https://api.minimax.io

!Watch out

The base URL has no `/v2` suffix/v2/... belongs to each endpoint path. Some circulating references define the base as https://api.minimax.io/v2 and then list /v2/...-prefixed paths, which concatenates to a doubled /v2/v2/ segment and a dead URL. Treat the base as https://api.minimax.io, full stop.

Step 1 — create the task

The endpoint is POST /v2/video_generation. Here is the official example request, verbatim from the API reference:

bash
curl --request POST \
  --url https://api.minimax.io/v2/video_generation \
  --header 'Authorization: Bearer <token>' \
  --header 'Content-Type: <content-type>' \
  --data '
{
  "model": "MiniMax-H3",
  "content": [
    {
      "type": "text",
      "text": "Epic space-opera theatrical teaser: a female captain stands alone before a massive observation window as the last fleet gathers and jumps away in a blinding flash, the bridge shaking, leaving her behind."
    }
  ],
  "resolution": "2K",
  "duration": 5,
  "ratio": "16:9"
}'

Replace <token> with your pay-as-you-go key and the <content-type> placeholder with application/json — the docs require it as an explicit header. Walk the body: model is the mandatory MiniMax-H3; content is an array of multimodal items, and every request must include one non-empty text item — the prompt is required, in every mode; resolution is 2K (or 768P); duration is an integer number of seconds; and ratio is set explicitly because pure text-to-video requires it — with no input image to adapt to, adaptive is not allowed, and omitting the field is a parameter error.

Notice what is not in the body: any kind of task-type field. Which type of task you create is determined entirely by which endpoint you POST to. Hold that thought — it becomes a full correction in the core track.

A successful creation returns only a receipt:

json
{"task_id": "424010985738629"}

Step 2 — poll

Query a single task with GET /v2/query/video_generation/{task_id} — the task ID goes in the path, and the same Bearer header applies. The docs recommend a 10-second polling interval to avoid unnecessary server load; there is no reason to poll faster, since you are waiting on seconds-to-minutes of video generation.

Each poll returns the task's current status, from a five-value enum: `queued`, `running`, `succeeded`, `failed`, `cancelled`. The first three are the ones you will see today: your task waits in queued, generates in running, and lands in one of the three terminal states — succeeded, failed, or cancelled.

Two properties of this enum are worth wiring into any loop you write, even a throwaway one. First, the states are one-way: once a task reports succeeded, failed, or cancelled, it is finished, and continuing to poll it is wasted traffic — exit the loop on any terminal state, not just on succeeded. Second, the same five values appear everywhere status is reported — in single-task queries, in the task-list filter, and in the optional webhook callbacks — so a status handler you write today keeps working when you graduate to the production patterns in the core track.

Correction

A widely-copied status table says the in-progress state is processing. It is not — the documented value is `running`, and processing appears nowhere in the API. This matters more than a naming quibble: polling code that tests status == "processing" never matches, so the loop spins until its timeout and reports every successful generation as a failure. Match on the real enum.

Step 3 — collect

When status reaches succeeded, the same query response carries the result. Here is the official example, verbatim:

json
{
  "task": {
    "id": "424010985738629",
    "model": "MiniMax-H3",
    "status": "succeeded",
    "created_at": 1785125529,
    "updated_at": 1785125946,
    "content": {
      "url": "https://your-cdn.example.com/h3-generated-2k-output.mp4"
    },
    "resolution": "2K",
    "duration": 5,
    "usage": {
      "total_seconds": 5,
      "input_seconds": 0,
      "output_seconds": 5,
      "input_image_count": 0
    },
    "ratio": "16:9",
    "task_type": "generation",
    "modality": "video"
  }
}

The finished video is a direct URL at content.url — download it and you are done. There is no file-ID exchange, no separate download endpoint, no extra hop.

Three details in this object reward a careful read. First, the field is `id` inside the nested task object — task_id is the field name only in the creation response; a poller that reads task.task_id gets nothing. Second, usage itemizes billing in seconds (output_seconds: 5 is your 5 x $0.13 from h3-f2), plus input_image_count for the image allowance. Third, task_type is "generation" — a value the response reports, never something you sent. created_at and updated_at are Unix timestamps, so their difference is your wall-clock generation time.

Don't dawdle on the download: tasks are queryable for 7 days, after which they age out of the system. Own your outputs; don't treat the API as storage.

When it fails instead

Errors share one shape across all endpoints — verbatim from the docs:

json
{
  "type": "error",
  "error": {
    "type": "bad_request_error",
    "message": "invalid params, content must include a non-empty text item (prompt is required) (2013)",
    "http_code": "400"
  },
  "request_id": "021785229015510a2c883cf675b9804d"
}

Read error.type and error.message, and keep request_id for support. The full error taxonomy — including the 402 balance error from h3-f2 and the 422 content-policy rejection — belongs to the production-operations lesson in the core track. For today: a 400 at creation time almost always means the request body broke a rule, and the message says which one. The complete rulebook is next, in h3-f4.

An optional alternative to polling exists — an asynchronous callback_url webhook that pushes status changes to you — and the core track covers it. Polling every 10 seconds is the documented, fully-supported default, and it is all you need today.

Lab

Generate, poll, download

0 / 5 steps
Success criteria

You have a playable MP4 with synchronized audio, your observed status sequence contains only values from queued / running / succeeded / failed / cancelled (and never "processing"), and you fetched the file straight from content.url with no intermediate file-ID exchange.

Knowledge check

3 questions

Pick an answer to see why it is right or wrong — including the wrong ones.

Q1

Your task has been accepted and the model is actively generating. What status does the query endpoint report?

Q2

How do you get the finished video file?

Q3

You submit a pure text-to-video request with no ratio field. What happens?