> ## Documentation Index
> Fetch the complete documentation index at: https://docs.protoface.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Inference quickstart

> Generate your first video with curl, Python, or JavaScript.

Create a key in the [dashboard](https://app.protoface.com). A scoped key
needs `runs:write` to submit and `runs:read` to retrieve.
[Authentication](/guides/authentication) covers revocation and where to
keep keys.

```bash theme={null}
export PROTOFACE_API_KEY="sk_live_..."
```

## Generate a video

Submit a prompt and the model's parameters to `/v1/run/{model_id}`. The
example below uses MiniMax H3, which takes a length in seconds, a quality
tier, and an aspect ratio. It returns video with sound.

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://api.protoface.com/v1/run/minimax/minimax-h3 \
    -H "Authorization: Bearer $PROTOFACE_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: first-video-001" \
    -d '{
      "prompt": "A wide shot of a sailboat crossing a quiet bay at sunrise.",
      "duration_seconds": 8,
      "quality": "768p",
      "aspect_ratio": "16:9"
    }'
  ```

  ```python httpx theme={null}
  import os

  import httpx

  created = httpx.post(
      "https://api.protoface.com/v1/run/minimax/minimax-h3",
      headers={
          "Authorization": f"Bearer {os.environ['PROTOFACE_API_KEY']}",
          "Idempotency-Key": "first-video-001",
      },
      json={
          "prompt": "A wide shot of a sailboat crossing a quiet bay at sunrise.",
          "duration_seconds": 8,
          "quality": "768p",
          "aspect_ratio": "16:9",
      },
      timeout=60.0,
  )
  created.raise_for_status()
  run_id = created.json()["id"]
  ```

  ```javascript fetch theme={null}
  const created = await fetch(
    "https://api.protoface.com/v1/run/minimax/minimax-h3",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.PROTOFACE_API_KEY}`,
        "Content-Type": "application/json",
        "Idempotency-Key": "first-video-001",
      },
      body: JSON.stringify({
        prompt: "A wide shot of a sailboat crossing a quiet bay at sunrise.",
        duration_seconds: 8,
        quality: "768p",
        aspect_ratio: "16:9",
      }),
    },
  );

  const { id: runId } = await created.json();
  ```
</CodeGroup>

The response is a `202` with a run ID. Generation continues in the
background.

```json theme={null}
{
  "id": "run_...",
  "object": "run",
  "model": "minimax/minimax-h3",
  "operation": "video.general",
  "status": "queued",
  "queue_position": 0,
  "credit_estimate": {
    "estimated_credits": 29,
    "maximum_reserved_credits": 29
  },
  "status_url": "/v1/runs/run_.../status",
  "response_url": "/v1/runs/run_..."
}
```

`credit_estimate` is a hold on your balance, and a run that produces no
output releases it. Rates change, so read the estimate from the response
rather than hard-coding the number above.

`Idempotency-Key` is any string you choose, and sending the same one
twice returns this same `202` instead of generating twice.

## Wait for the file

Poll `status_url` about once a second until `status` reaches
`completed`, `failed`, or `canceled`. It reports state without carrying
the file.

```bash theme={null}
curl https://api.protoface.com/v1/runs/run_.../status \
  -H "Authorization: Bearer $PROTOFACE_API_KEY"
```

```json theme={null}
{
  "id": "run_...",
  "object": "run.status",
  "status": "running",
  "progress": 40,
  "queue_position": null,
  "error": null
}
```

How long a run takes depends on the model, the length you asked for, and
current load. While it waits, `queue_position` counts the runs ahead of
it. Once it starts, `progress` tracks it to completion. Fetch the run
once it completes:

```bash theme={null}
curl https://api.protoface.com/v1/runs/run_... \
  -H "Authorization: Bearer $PROTOFACE_API_KEY"
```

```json theme={null}
{
  "id": "run_...",
  "status": "completed",
  "progress": 100,
  "video": {
    "url": "https://...",
    "content_type": "video/mp4",
    "file_name": "output.mp4",
    "file_size": 3919397
  },
  "outputs": [
    {
      "role": "output",
      "modality": "video",
      "type": "asset",
      "asset_id": "asset_...",
      "file": { "url": "https://...", "content_type": "video/mp4" }
    }
  ],
  "credits": { "reserved": 29, "charged": 29 }
}
```

## The whole sequence

The curl version uses [jq](https://jqlang.github.io/jq/) to read fields
from each response.

<CodeGroup>
  ```bash curl theme={null}
  RUN_ID=$(curl -s -X POST https://api.protoface.com/v1/run/minimax/minimax-h3 \
    -H "Authorization: Bearer $PROTOFACE_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: first-video-001" \
    -d '{
      "prompt": "A wide shot of a sailboat crossing a quiet bay at sunrise.",
      "duration_seconds": 8,
      "quality": "768p",
      "aspect_ratio": "16:9"
    }' | jq -r .id)

  while true; do
    STATUS=$(curl -s "https://api.protoface.com/v1/runs/$RUN_ID/status" \
      -H "Authorization: Bearer $PROTOFACE_API_KEY" | jq -r .status)
    case "$STATUS" in
      completed|failed|canceled) break ;;
    esac
    sleep 1
  done

  curl -s "https://api.protoface.com/v1/runs/$RUN_ID" \
    -H "Authorization: Bearer $PROTOFACE_API_KEY" \
    | jq -r '.video.url // .error.message'
  ```

  ```python httpx theme={null}
  import os
  import time

  import httpx

  API = "https://api.protoface.com"
  headers = {"Authorization": f"Bearer {os.environ['PROTOFACE_API_KEY']}"}

  with httpx.Client(base_url=API, headers=headers, timeout=60.0) as client:
      created = client.post(
          "/v1/run/minimax/minimax-h3",
          headers={"Idempotency-Key": "first-video-001"},
          json={
              "prompt": "A wide shot of a sailboat crossing a quiet bay at sunrise.",
              "duration_seconds": 8,
              "quality": "768p",
              "aspect_ratio": "16:9",
          },
      )
      created.raise_for_status()
      run_id = created.json()["id"]

      while True:
          status = client.get(f"/v1/runs/{run_id}/status")
          status.raise_for_status()
          state = status.json()["status"]
          if state in {"completed", "failed", "canceled"}:
              break
          time.sleep(1)

      run = client.get(f"/v1/runs/{run_id}")
      run.raise_for_status()
      body = run.json()
      if body["status"] != "completed":
          raise SystemExit(body.get("error") or body["status"])
      print(body["video"]["url"])
  ```

  ```javascript fetch theme={null}
  const apiKey = process.env.PROTOFACE_API_KEY;
  const headers = {
    Authorization: `Bearer ${apiKey}`,
    "Content-Type": "application/json",
  };

  const created = await fetch("https://api.protoface.com/v1/run/minimax/minimax-h3", {
    method: "POST",
    headers: { ...headers, "Idempotency-Key": "first-video-001" },
    body: JSON.stringify({
      prompt: "A wide shot of a sailboat crossing a quiet bay at sunrise.",
      duration_seconds: 8,
      quality: "768p",
      aspect_ratio: "16:9",
    }),
  });

  if (!created.ok) {
    throw new Error(await created.text());
  }

  const { id: runId } = await created.json();

  let status;
  do {
    await new Promise((resolve) => setTimeout(resolve, 1000));
    const response = await fetch(
      `https://api.protoface.com/v1/runs/${runId}/status`,
      { headers: { Authorization: `Bearer ${apiKey}` } },
    );
    if (!response.ok) {
      throw new Error(await response.text());
    }
    status = (await response.json()).status;
  } while (status === "queued" || status === "running");

  const run = await fetch(`https://api.protoface.com/v1/runs/${runId}`, {
    headers: { Authorization: `Bearer ${apiKey}` },
  });
  const body = await run.json();
  if (body.status !== "completed") {
    throw new Error(JSON.stringify(body.error ?? body.status));
  }
  console.log(body.video.url);
  ```
</CodeGroup>

Only the body changes between models. Field names, defaults, and bounds
come from the model's [catalog entry](/guides/models).
[Runs](/guides/runs) documents the rest of the run API.

## Failures

A failed generation puts `error` on both the status payload and the run.
Branch on `error.code`. Exceeding your concurrency returns `429` with
`run.concurrency_exceeded`. An empty balance returns `402` with
`insufficient_credit_balance`. [Errors](/reference/errors) lists the
rest.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.