API / send-imagine-video-to-your-upload-url

API

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

  1. Export the key locally and keep it out of chat logs and public repos:
export XAI_API_KEY="your_api_key"
  1. 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.

  2. Start a generation with output.upload_url set 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"
  1. 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
  1. Confirm the object landed in your bucket. Read respect_moderation on the completed body before you publish the clip, and handle failed with the error.code table in Handle Imagine video generation errors. The same output object works on POST /v1/videos/edits and POST /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.