NewDeepSeek V4.1 Flash and MiniMax M3.1 Flash are live
Inferway
Skip to content
Interactions API

Integrate the video model with the Interactions API

Video sample

This walkthrough uses the Interactions API for inferway/minimax-h3-768p: background create, poll, download.

Prerequisites: bash, curl, jq. The Python sample needs only the standard library; the TypeScript sample needs Node with fetch. Set the INFERWAY_API_KEY environment variable; optionally set INFERWAY_IDEMPOTENCY_KEY to resume a previous create, and INFERWAY_MAX_WAIT_SECONDS to change the maximum wait (default 7200 seconds).

  1. Create: POST a top-level JSON body to /interactions (op=create, mode=background, duration_seconds and prompt) with a persistent Idempotency-Key.
  2. Poll: check with op=get and the returned interaction_id every 15 seconds, waiting for as long as the task is queued or running and printing its position or elapsed time each round; failed/cancelled/expired/recovery_required are terminal errors.
  3. Download: once succeeded with a nonempty result.download_url, download it with a separate unauthenticated HTTPS request and no redirects.

If the wait limit is reached the job may still be running: it is NOT cancelled, it stays in the queue and is charged only if it succeeds, so do not submit it again. Keep the interaction ID and Idempotency-Key — re-running with INFERWAY_IDEMPOTENCY_KEY=<key> resumes the same task. Refresh an expired download URL by calling get again.

Privacy tier: retained_7d. Video output is retained under the selected model's policy — not chat's zero-retention.

#!/usr/bin/env bash
# Inferway video generation via the Interactions API.
# Prerequisites: bash, curl, jq.
# Required env: INFERWAY_API_KEY.
# Optional env: INFERWAY_IDEMPOTENCY_KEY — resume a previous create with this
# key; otherwise a fresh key is generated.
# Optional env: INFERWAY_MAX_WAIT_SECONDS — total seconds to wait on a
# queued/running task (default 7200 = 2 hours, positive integer).
# On success the video is saved to ./inferway-video.mp4.
set -u

API_BASE="https://api.inferway.ai/v1"
MODEL="inferway/minimax-h3-768p"
PROMPT="A drone shot along a coastline at dawn."
DURATION_SECONDS=5
SAVE_PATH=inferway-video.mp4

workdir=$(mktemp -d) || exit 1
cleanup() { rm -rf "$workdir"; }
trap cleanup EXIT HUP INT TERM

if [ -z "${INFERWAY_API_KEY:-}" ]; then
  echo "error: INFERWAY_API_KEY is required" >&2
  exit 1
fi

# The total wait budget, validated BEFORE the first request: a typo must not
# turn into a run that waits on the default budget.
max_wait_raw=$(printf '%s' "${INFERWAY_MAX_WAIT_SECONDS-}" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
max_wait=7200
if [ -n "$max_wait_raw" ]; then
  case "$max_wait_raw" in
    *[!0-9]*)
      echo "error: INFERWAY_MAX_WAIT_SECONDS must be a positive integer" >&2
      exit 1
      ;;
  esac
  if [ -z "$(printf '%s' "$max_wait_raw" | sed 's/^0*//')" ]; then
    echo "error: INFERWAY_MAX_WAIT_SECONDS must be a positive integer" >&2
    exit 1
  fi
  max_wait=$max_wait_raw
fi

idempotency_key="${INFERWAY_IDEMPOTENCY_KEY:-}"
if [ -z "$idempotency_key" ]; then
  if command -v uuidgen >/dev/null 2>&1; then
    idempotency_key=$(uuidgen | tr 'A-Z' 'a-z') || exit 1
  elif command -v uuid >/dev/null 2>&1; then
    idempotency_key=$(uuid | tr -d '-') || exit 1
  else
    idempotency_key="iv-$(date +%s%N)-$$"
  fi
fi
echo "Idempotency-Key: $idempotency_key"

interactions_url="$API_BASE/interactions"
response_headers=$workdir/headers
response_body=$workdir/body
create_body_file=$workdir/create-body.json

jq -cn \
  --arg model "$MODEL" \
  --argjson durationSeconds "$DURATION_SECONDS" \
  --arg prompt "$PROMPT" \
  '{op: "create", model: $model, mode: "background", duration_seconds: $durationSeconds, prompt: $prompt}' \
  > "$create_body_file" || exit 1

attempt=0
while :; do
  attempt=$((attempt + 1))
  rc=0
  curl -sS --connect-timeout 30 --max-time 30 \
    -D "$response_headers" \
    --data-binary @"$create_body_file" \
    -H "Authorization: Bearer $INFERWAY_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: $idempotency_key" \
    "$interactions_url" > "$response_body" 2> "$workdir/curl-err" || rc=$?
  if [ "$rc" -ne 0 ]; then
    if [ "$attempt" -ge 5 ]; then
      echo "error: create request failed on the 5th attempt: $(cat "$workdir/curl-err")" >&2
      exit 1
    fi
    sleep $((1 << (attempt - 1)))
    continue
  fi
  status=$(head -n 1 "$response_headers" | tr -d '\r' | awk '{print $2}')
  case "$status" in
    200|202)
      id=$(jq -er '.id | select(type == "string" and length > 0)' "$response_body" 2>/dev/null) || {
        echo "error: accepted response has no usable id" >&2
        exit 1
      }
      echo "task id: $id"
      break
      ;;
    429|5[0-9][0-9])
      if jq -e '((.error? // {}) | (type == "object") and .retryable == false) or (.retryable? == false)' "$response_body" >/dev/null 2>&1; then
        echo "error: server reported a non-retryable failure (status $status)" >&2
        jq -c . "$response_body" >&2 || true
        exit 1
      fi
      if [ "$attempt" -ge 5 ]; then
        echo "error: create request failed with status $status on the 5th attempt" >&2
        exit 1
      fi
      retry_after=$(sed -n '1,/^$/s/^Retry-After:[[:space:]]*//Ip' "$response_headers" | head -n 1 | tr -d '\r' | tr -d '[:space:]')
      if printf '%s' "${retry_after:-}" | grep -Eq '^[0-9]+$'; then
        if [ "$retry_after" -gt 30 ]; then
          echo "error: server asked to wait $retry_after s (ceiling 30 s); resume later with:" >&2
          echo "  INFERWAY_IDEMPOTENCY_KEY=$idempotency_key" >&2
          exit 1
        else
          sleep "$retry_after"
        fi
      else
        sleep $((1 << (attempt - 1)))
      fi
      continue
      ;;
    *)
      if jq -e '((.error? // {}) | (type == "object") and .retryable == false) or (.retryable? == false)' "$response_body" >/dev/null 2>&1; then
        echo "error: server reported a non-retryable failure (status $status)" >&2
      else
        echo "error: create request failed with status $status" >&2
      fi
      jq -c . "$response_body" >&2 || true
      exit 1
      ;;
  esac
done

# --- Wait on the queue: poll while the task is queued or running ------------
# Video is generated serially, so a task's queue wait is the sum of the
# generation seconds estimated from recent observed runs of every order ahead
# of it (see docs/interactions-queue-contract-v1.md,
# queue.estimated_wait_seconds). A fixed poll count therefore gives up on a
# perfectly healthy task, and a customer shown an error re-submits the same job
# and is charged twice. So this loop waits for as long as the task says queued
# or running, reports the queue position every round, and is bounded only in
# total by INFERWAY_MAX_WAIT_SECONDS (default 7200 s = 2 h). Failures of the
# status check itself are retried too; only a non-429 4xx answer stops the run
# at once.
download_url=""
waited=0
transient=0
poll_delay=15

stop_waiting() {
  echo "Stopped waiting. The task was NOT cancelled: it stays in the queue and is charged only if it succeeds." >&2
  echo "Do not submit it again. To resume, re-run this script with:" >&2
  echo "  INFERWAY_IDEMPOTENCY_KEY=$idempotency_key" >&2
  exit 1
}

poll_delay_for() {
  # 429 and 503 may name their own delay: honor a whole number of seconds up
  # to 60; anything else (fraction, negative, > 60, absent) waits 15 s.
  if [ "$1" != "429" ] && [ "$1" != "503" ]; then
    echo 15
    return
  fi
  retry_after=$(sed -n '1,/^$/s/^Retry-After:[[:space:]]*//Ip' "$response_headers" | head -n 1 | tr -d '\r' | tr -d '[:space:]')
  if printf '%s' "${retry_after:-}" | grep -Eq '^[0-9]+$' && [ "$retry_after" -le 60 ]; then
    echo "$retry_after"
  else
    echo 15
  fi
}

while :; do
  : > "$response_headers"
  rc=0
  curl -sS --connect-timeout 30 --max-time 30 \
    -D "$response_headers" \
    --data-binary "$(jq -cn --arg id "$id" '{op: "get", interaction_id: $id}')" \
    -H "Authorization: Bearer $INFERWAY_API_KEY" \
    -H "Content-Type: application/json" \
    "$interactions_url" > "$response_body" 2> "$workdir/curl-err" || rc=$?
  status=$(head -n 1 "$response_headers" | tr -d '\r' | awk '{print $2}')
  poll_delay=15
  hard_stop=""
  if [ "$rc" -ne 0 ]; then
    # Network error or timeout: the next round gets to try again.
    transient=$((transient + 1))
  elif [[ ! "$status" =~ ^2[0-9][0-9]$ ]]; then
    case "$status" in
      429|5[0-9][0-9])
        transient=$((transient + 1))
        poll_delay=$(poll_delay_for "$status")
        ;;
      *)
        hard_stop="status=$status"
        ;;
    esac
  elif ! jq -e . "$response_body" >/dev/null 2>&1 || [ -z "$(jq -r '.state // empty' "$response_body")" ]; then
    transient=$((transient + 1))
  else
    transient=0
    state=$(jq -r '.state' "$response_body")
    case "$state" in
      failed|cancelled|expired|recovery_required)
        echo "error: task $id ended in state '$state'" >&2
        jq -r 'if ((.failure | type) == "object") and (.failure | has("charged")) then "charged: \(.failure.charged)" else empty end' "$response_body" >&2
        exit 1
        ;;
      queued)
        jq -r 'if ((.queue | type) == "object")
              and ((.queue.position | type) == "number")
              and (.queue.position == (.queue.position | floor))
              and (.queue.position >= 1)
              and ((.queue.estimated_wait_seconds | type) == "number")
              and (.queue.estimated_wait_seconds >= 0)
            then "queued: position \(.queue.position), about \([1, ((.queue.estimated_wait_seconds / 60) | ceil)] | max) min to start"
            else "queued" end' "$response_body"
        ;;
      running)
        jq -r 'if ((.progress | type) == "object")
              and ((.progress.elapsed_seconds | type) == "number")
              and (.progress.elapsed_seconds >= 0)
              and ((.progress.typical_seconds | type) == "number")
              and (.progress.typical_seconds >= 1)
            then "running: \(.progress.elapsed_seconds)s elapsed (typically \(.progress.typical_seconds)s)"
            else "running" end' "$response_body"
        ;;
      succeeded)
        candidate=$(jq -er '.result? | select(type == "object") | .download_url? | select(type == "string" and length > 0)' "$response_body" 2>/dev/null) || candidate=""
        if [ -n "$candidate" ]; then
          download_url="$candidate"
          break
        fi
        ;;
    esac
  fi

  if [ -n "$hard_stop" ]; then
    echo "polling stopped ($hard_stop); the job may still be running." >&2
    echo "resume later with:" >&2
    echo "  task id: $id" >&2
    echo "  INFERWAY_IDEMPOTENCY_KEY=$idempotency_key" >&2
    exit 1
  fi

  if [ "$transient" -ge 8 ]; then
    echo "error: the status check failed 8 times in a row" >&2
    stop_waiting
  fi

  if [ "$((waited + poll_delay))" -gt "$max_wait" ]; then
    stop_waiting
  fi
  sleep "$poll_delay"
  waited=$((waited + poll_delay))
done

case "$download_url" in
  https://*) ;;
  *)
    echo "error: refusing to download a non-HTTPS URL" >&2
    exit 1
    ;;
esac

# --- Download: separate request, no API headers, no redirects ---------------
rc=0
curl -sS --connect-timeout 30 --max-time 30 \
  -D "$response_headers" -o "$workdir/video" \
  "$download_url" || rc=$?
if [ "$rc" -ne 0 ]; then
  echo "error: download failed (network/timeout)" >&2
  exit 1
fi
status=$(head -n 1 "$response_headers" | tr -d '\r' | awk '{print $2}')
case "$status" in
  2??) ;;
  3??)
    echo "error: download redirected (status $status); refusing to follow redirects" >&2
    exit 1
    ;;
  *)
    echo "error: download failed with status $status" >&2
    exit 1
    ;;
esac

mv "$workdir/video" "$SAVE_PATH" || exit 1
echo "SAVED $SAVE_PATH"

Generate video from images

Two ways to use images: attach a first frame — optionally a last frame — and the clip starts there and lands on the last one; or attach one to three reference images and the clip follows the people or scenes in them. The two cannot be combined. Which one runs is decided by which slots you fill: you never send a mode, and mode stays background.

  1. Request an upload ticket: send prepare_upload to /interactions with purpose (first_frame, last_frame or ref_image), content_type and content_length (the image's byte count), and get back upload_url — a relative path, so join it to your own API base. One ticket per image; it is valid for 15 minutes and spent once.
  2. Upload: PUT the image's raw bytes to upload_url with the same Authorization: Bearer and the Content-Type you declared. The body is the raw bytes — not multipart, not base64 — and the receipt carries upload_id.
  3. Create: put the upload_id into first_frame (and last_frame, if you sent one), or into ref_image_1 through ref_image_3 — every other field is the text-to-video one. From the interaction id on, poll and download exactly as in the section above.

Images must be JPEG, PNG or WebP, at most 10 MiB each.

Billing is the same as text-to-video: the price of the ordered duration is held when you submit and charged only once the video is ready; a failed task costs nothing.

Which modes a model runs right now is its available_modes on GET /v1/models (always starting with t2v); a mode outside it is refused at create time, before anything is held, so nothing is charged.

Same prerequisites as above: INFERWAY_API_KEY, plus bash/curl/jq, the Python standard library, or Node with fetch. The samples stop at the interaction id; polling is the same as in the section above — one check every 15 seconds, showing how many orders are ahead while the task is queued, for up to 2 hours (INFERWAY_MAX_WAIT_SECONDS changes that limit).

Loading the samples…