
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
- Export the key locally and keep it out of chat logs and public repos:
export XAI_API_KEY="your_api_key"
- 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
- Or set
storage_optionson the REST body and poll yourself untildone:
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
- Confirm
file_idand, when you asked for one,public_url. If public URL minting failed, the file can still be stored — readpublic_url_errorand retry with the Files API on thatfile_id. If storage itself failed, readstorage_erroron the completed video object. - Pass the stored
file_idinto a later edit or extend withvideo: { "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.