
Send an Imagine video to your upload URL
Point the Imagine video API at a signed URL you control with output.upload_url so xAI uploads the finished MP4 to your storage with HTTP PUT. Official Security states that under Zero Data Retention there is no xAI-side video output storage, and video requests must supply your own output.upload_url. The REST Videos reference documents output.upload_url on generations, edits, and extensions. Start the job, poll GET /v1/videos/{request_id}, and treat done as the signal that the PUT finished. Create a key at console.x.ai on the team that needs this path.
What you need
An XAI_API_KEY for that team, an HTTPS signed URL that accepts HTTP PUT of an MP4 body (for example a short-lived S3 or R2 presigned URL), and a video request you already know how to send. On default retention you can also use ephemeral vidgen.x.ai URLs or Store an Imagine API video with storage_options. Grok Build under ZDR can presign this URL from ~/.grok/managed_config.toml — see Configure ZDR video output storage in Grok Build.
Attach upload_url and poll
- Export the key locally and keep it out of chat logs and public repos:
export XAI_API_KEY="your_api_key"
Mint a short-lived signed PUT URL in your own cloud (AWS S3, Cloudflare R2, MinIO, or another S3-compatible store). The URL must allow the API to PUT the generated bytes before the signature expires; leave enough headroom for a multi-minute render.
Start a generation with
output.upload_urlset to that signed URL:
REQUEST_ID=$(curl -s -X POST https://api.x.ai/v1/videos/generations \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $XAI_API_KEY" \
-d "{
\"model\": \"grok-imagine-video-1.5\",
\"prompt\": \"A paper boat drifting down a rain-soaked street\",
\"duration\": 8,
\"aspect_ratio\": \"16:9\",
\"resolution\": \"720p\",
\"output\": {
\"upload_url\": \"$SIGNED_PUT_URL\"
}
}" | jq -r '.request_id')
echo "Request ID: $REQUEST_ID"
- Poll until the job finishes:
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 .
break
elif [ "$STATUS" = "failed" ] || [ "$STATUS" = "expired" ]; then
echo "Request $STATUS"; echo "$RESULT" | jq .
break
fi
sleep 5
done
- Confirm the object landed in your bucket. Read
respect_moderationon the completed body before you publish the clip, and handlefailedwith theerror.codetable in Handle Imagine video generation errors. The sameoutputobject works onPOST /v1/videos/editsandPOST /v1/videos/extensions.
After the upload
Check the x-zero-data-retention response header when you need to confirm ZDR is active for the team that minted the key. Rotate signed URLs often enough that a long render cannot outlive the signature. For Grok Build tooling that needs the same destination without hand-minting each URL, configure the S3 table in managed config and restart Build.
Pitfalls
Leaving output.upload_url off on a ZDR team fails the video path because xAI cannot host the result. Minting a GET-only signed URL, or a PUT URL that expires in seconds, causes the server-side upload to fail after a long generate. Skipping a bucket existence check after status: done can hide a failed PUT. Pasting keys or long-lived bucket credentials into tickets forces a rotate.