API / store-imagine-video-output-with-storage-options

API

Store an Imagine API video with storage_options

Keep a generated clip in your Files API storage by setting storage_options on the video request, then reading video.file_output on the completed poll. Official Persisting Generated Output documents the same shape on /v1/videos/generations, /v1/videos/edits, and /v1/videos/extensions. Because video jobs are asynchronous, file_output (and any public URL) appears only after status is done. The ephemeral vidgen.x.ai URL still returns; file_id is the stable copy you can list, download, or feed into a later edit. Create a key and load credits at console.x.ai before you bill a stored run.

What you need

An XAI_API_KEY with Console balance, a text-to-video or image-to-video request you already know how to start, and a filename ending in .mp4 for the stored object. Pair this job with Generate a video from text with the Imagine API or Animate a still with the Imagine API. For file TTL and public URL TTL, see Set Imagine storage and public URL expiry knobs.

Generate, poll, and read file_output

  1. Export the key locally and keep it out of chat logs and public repos:
export XAI_API_KEY="your_api_key"
  1. Prefer the xAI Python SDK when you want the client to poll and return the completed video:
import os
import xai_sdk

client = xai_sdk.Client(api_key=os.getenv("XAI_API_KEY"))

response = client.video.generate(
    prompt="A ball bouncing slowly on a flat surface",
    model="grok-imagine-video-1.5",
    duration=5,
    storage_options={"filename": "bouncing-ball.mp4", "public_url": True},
)

print(response.url)                   # ephemeral vidgen URL
print(response.file_output.file_id)   # file_...
print(response.public_url)            # files-cdn.x.ai when mint succeeded
  1. Or set storage_options on the REST body and poll yourself until done:
REQUEST_ID=$(curl -s -X POST https://api.x.ai/v1/videos/generations \
  -H "Authorization: Bearer $XAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "grok-imagine-video-1.5",
    "prompt": "A ball bouncing slowly on a flat surface",
    "duration": 5,
    "storage_options": {
      "filename": "bouncing-ball.mp4",
      "public_url": true
    }
  }' | jq -r '.request_id')

while true; do
  RESULT=$(curl -s "https://api.x.ai/v1/videos/$REQUEST_ID" \
    -H "Authorization: Bearer $XAI_API_KEY")
  STATUS=$(echo "$RESULT" | jq -r '.status')
  if [ "$STATUS" = "done" ]; then
    echo "$RESULT" | jq '.video.file_output'
    break
  elif [ "$STATUS" = "failed" ] || [ "$STATUS" = "expired" ]; then
    echo "Request $STATUS"; echo "$RESULT" | jq .
    break
  fi
  sleep 5
done
  1. Confirm file_id and, when you asked for one, public_url. If public URL minting failed, the file can still be stored — read public_url_error and retry with the Files API on that file_id. If storage itself failed, read storage_error on the completed video object.
  2. Pass the stored file_id into a later edit or extend with video: { "file_id": "..." } instead of re-uploading bytes. See Use Files API file IDs as Imagine inputs.

After the store

Omit public_url when the clip should stay private inside Files; mint a CDN link later with client.files.create_public_url(file_id) if you change your mind. Image-to-video, video editing, and video extension accept the same storage_options object. On a Zero Data Retention team, land the MP4 on your own signed URL with Send an Imagine video to your upload URL.

Pitfalls

Reading file_output while status is still pending returns nothing useful. Omitting filename inside storage_options fails validation before the job starts. Expecting the ephemeral vidgen.x.ai URL to last like a Files file_id loses the clip when that temporary link expires. Hitting the team cap of 1,000 active public URLs sets public_url_error without deleting the stored file — revoke unused links and retry.