Skip to main content
Lesson 13 of 14 · Generate and Edit Images with Grok

Temporary URLs and Download Strategies

Lesson 13 of 14 Updated November 3, 2026 Beginner
On this page
  1. Handling Ephemeral Generated Images
  2. The Problem with Temporary URLs
  3. Strategy 1: Immediate Download
  4. Strategy 2: Using Base64
  5. Strategy 3: Automatic Cloud Storage
  6. Strategy 4: Letting xAI Store the Image
  7. Robust Pipeline with Retry
  8. Best Practices
  9. Key Takeaways

Lesson completed on November 3, 2026 based on the Image Generation and Imagine outputs pages from xAI: the storage_options parameter keeps the image in the Files API and can create a permanent public URL; the documentation does not specify the lifespan of ephemeral URLs (the “403 or 404 errors” from the previous version do not appear there); images output as JPEG by default. Sources: Image Generation · Imagine outputs.

Handling Ephemeral Generated Images

URLs returned by the image generation and editing API are temporary: xAI’s documentation describes them as short-lived without specifying their lifespan, and asks you to download or process the images without delay. This constraint directly impacts how you design your applications.

Temp.
Output URLs
b64
Persistent Alternative
+33%
Base64 Size Overhead
30 days
xAI Storage, or unlimited

The Problem with Temporary URLs

When you use response_format="url" (the default), the API returns an ephemeral URL. After an unspecified period, it stops serving the image.

# Ephemeral URL: download without delay
response = client.images.generate(
    model="grok-imagine-image",
    prompt="A mountain landscape"
)

url = response.data[0].url

Consequence: you cannot store these URLs in a database and reuse them later. You must systematically download the images.

Strategy 1: Immediate Download

The most reliable strategy is to download each image as soon as it is generated:

import urllib.request
import os
from datetime import datetime

def generate_and_save(prompt, output_dir="images"):
    """Generates an image and saves it immediately."""
    os.makedirs(output_dir, exist_ok=True)

    response = client.images.generate(
        model="grok-imagine-image",
        prompt=prompt
    )

    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    filepath = os.path.join(output_dir, f"gen_{timestamp}.jpg")  # JPEG by default

    urllib.request.urlretrieve(response.data[0].url, filepath)
    print(f"Image saved: {filepath}")
    return filepath

Strategy 2: Using Base64

By requesting the response in Base64, you receive the image’s binary data directly, without relying on a temporary URL:

import base64

response = client.images.generate(
    model="grok-imagine-image",
    prompt="A mountain landscape",
    response_format="b64_json"
)

# The data is directly in the response
image_bytes = base64.b64decode(response.data[0].b64_json)

with open("mountain.jpg", "wb") as f:  # JPEG by default
    f.write(image_bytes)

Advantages of Base64

The advantage of Base64 comes down to one word: autonomy. The image arrives directly in the JSON response, without relying on a temporary URL that might expire before your code consumes it, and without an additional HTTP request to download it—one fewer network round trip means one fewer point of failure in your pipeline.

Disadvantages of Base64

The trade-off is mechanical: Base64 encoding swells the JSON response by about 33%, which costs you three times over—in API response transfer time, in memory consumed on the client to decode it, and in complexity if you manipulate multiple high-resolution images in the same request. For a one-off image, it’s negligible; for a batch of ten 2k images, the difference becomes tangible.

Strategy 3: Automatic Cloud Storage

For production applications, combine generation with an upload to your storage:

import boto3
import urllib.request
import tempfile

def generate_and_upload_s3(prompt, bucket, key):
    """Generates an image and uploads it directly to S3."""
    response = client.images.generate(
        model="grok-imagine-image",
        prompt=prompt
    )

    # Download to a temporary file
    with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp:
        urllib.request.urlretrieve(response.data[0].url, tmp.name)

        # Upload to S3
        s3 = boto3.client("s3")
        s3.upload_file(tmp.name, bucket, key)

    permanent_url = f"https://{bucket}.s3.amazonaws.com/{key}"
    return permanent_url

Strategy 4: Letting xAI Store the Image

Every Imagine request accepts a storage_options parameter that saves the produced image in your team’s Files API, and can additionally create a permanent public URL, to share or embed. With the xAI SDK:

import os
import xai_sdk

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

response = client_xai.image.sample(
    prompt="A mountain landscape at sunrise",
    model="grok-imagine-image-2.0",
    storage_options={"filename": "mountain.jpg", "public_url": True},
)

print(response.file_output.file_id)  # stable identifier in the Files API
print(response.public_url)           # permanent public URL

The response keeps the usual ephemeral URL and adds, for each image, a file_output block: file_id, filename, and, if requested, public_url. Only filename is required: without other options, the image is stored privately, without expiration or public URL. expires_after (from 3,600 seconds to 30 days) schedules file deletion, and public_url also accepts its own expires_after, which cannot exceed the file’s. A team can have up to 1,000 active public URLs; beyond that, or in case of an incident, the image is still stored and the response carries a public_url_error instead of the URL. Via REST, the same storage_options object goes in the request body, including for an edit.

Robust Pipeline with Retry

Temporary URLs can sometimes fail during download. Implement a retry mechanism:

import time
import urllib.request
import urllib.error

def download_with_retry(url, filepath, max_retries=3):
    """Downloads an image with retry in case of error."""
    for attempt in range(max_retries):
        try:
            urllib.request.urlretrieve(url, filepath)
            return True
        except urllib.error.URLError as e:
            if attempt < max_retries - 1:
                wait_time = 2 ** attempt  # Exponential backoff
                print(f"Attempt {attempt + 1} failed, retrying in {wait_time}s...")
                time.sleep(wait_time)
            else:
                print(f"Failed after {max_retries} attempts: {e}")
                return False

Best Practices

All discipline stems from a single fact: URLs are temporary. Downloading must therefore follow generation immediately, in the same code flow—every minute of gap is a window where the image can become unrecoverable. For the same reason, a temporary URL must never be stored in a database as a permanent reference: it will be dead by the next lookup, and this is the kind of bug that only appears in production, days later. In critical applications where reliability comes first, bypass the problem with b64_json, which delivers the image in the response, or with storage_options, which keeps it at xAI with a stable identifier.

The download itself deserves treatment as a normal network operation: a retry with backoff to absorb transient failures, file naming by timestamp or unique identifier to avoid collisions when multiple generations run in parallel. Finally, log metadata—prompt, model, parameters—with each saved image: six months later, when you need to regenerate a visual “like that one but in 2k”, this log will be invaluable.

Key Takeaways

  • Output URLs are temporary: download immediately
  • The b64_json format avoids the ephemeral URL problem
  • Base64 increases response size by ~33%
  • Implement a retry with exponential backoff for robustness
  • In production, upload to permanent storage (S3, GCS), or have xAI store the image with storage_options