1#### Model Capabilities
2
3# Persisting Generated Output
4
5Use `storage_options` on any Imagine request to **persist the generated asset** to your [Files API](/developers/files) storage, where you can retrieve it later through the authenticated Files API. On top of that, set `storage_options.public_url` to additionally generate a **permanent, shareable public URL** for the asset — an unauthenticated link that anyone can open and that stays alive until you revoke it.
6
7Storage and public URL creation are independent: you can store privately without a public URL, or store with a public URL. Either way, the response also includes the ephemeral generation URL that's always returned by default.
8
9## Quick Start
10
11Generate an image, persist it to Files, and get a shareable public URL — all in one call:
12
13```python customLanguage="pythonXAI"
14import os
15import xai_sdk
16
17client = xai_sdk.Client(api_key=os.getenv("XAI_API_KEY"))
18
19response = client.image.sample(
20 prompt="A serene Japanese garden in winter",
21 model="grok-imagine-image-quality",
22 storage_options={"filename": "garden.jpg", "public_url": True},
23)
24
25# Ephemeral, short-lived URL.
26print(f"Ephemeral: {response.url}")
27
28# Persistent file in your Files API storage.
29print(f"File ID: {response.file_output.file_id}")
30
31# Permanent, shareable public URL.
32print(f"Public URL: {response.public_url}")
33```
34
35```bash
36curl -s -X POST https://api.x.ai/v1/images/generations \
37 -H "Authorization: Bearer $XAI_API_KEY" \
38 -H "Content-Type: application/json" \
39 -d '{
40 "model": "grok-imagine-image-quality",
41 "prompt": "A serene Japanese garden in winter",
42 "response_format": "url",
43 "storage_options": {
44 "filename": "garden.jpg",
45 "public_url": true
46 }
47 }'
48```
49
50The response body looks like this:
51
52```json
53{
54 "data": [
55 {
56 "url": "https://imgen.x.ai/xai-imgen/xai-tmp-imgen-abc123.jpg",
57 "file_output": {
58 "file_id": "file_7de029f4-eb66-42ee-87f8-b2a9d9e7466a",
59 "filename": "garden.jpg",
60 "public_url": "https://files-cdn.x.ai/ZsqeMtdcSYWPPHTQdxXDKQ/file_7de029f4-eb66-42ee-87f8-b2a9d9e7466a.jpg"
61 }
62 }
63 ]
64}
65```
66
67Key things to note:
68
69* `data[i].url` is the **ephemeral** Imagine URL — short-lived, fine for immediate use. It's always returned, regardless of whether `storage_options` is specified.
70* `data[i].file_output.file_id` is the stable Files API identifier for the generated asset.
71* `data[i].file_output.public_url` is the **permanent** public URL. Use this for sharing, embedding, and long-term storage in your app.
72
73## `storage_options` Reference
74
75| Field | Type | Description |
76| -------------------------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
77| `filename` | string (**required**) | Filename for the stored file. The extension on the public URL path is derived from this filename. |
78| `expires_after` | integer (optional) | Seconds from now until the stored file expires. Must be between `3600` (1 hour) and `2592000` (30 days). Omit for permanent storage. |
79| `public_url` | boolean or object (optional) | Set to `true` to create a public URL with default settings, or pass an object to configure it. Omit (or `false`) to store privately. |
80| `public_url.expires_after` | integer (optional) | Seconds from now until the public URL expires. Must be between `3600` (1h) and `2592000` (30d). See [Expiry Behaviour](#expiry-behaviour). |
81
82`filename` is required. Passing `storage_options={"filename": "..."}` with no other fields persists the asset privately: no expiry on the stored file and no public URL. You can always call [`create_public_url`](/developers/files/public-urls) on the stored `file_id` later if you change your mind.
83
84```python customLanguage="pythonXAI"
85response = client.image.sample(
86 prompt="A red circle on a white background",
87 model="grok-imagine-image-quality",
88 storage_options={"filename": "circle.jpg"}, # store privately, no public URL
89)
90
91print(response.file_output.file_id) # file_...
92print(response.file_output.filename) # circle.jpg
93print(response.public_url) # None — public URL was not requested
94```
95
96```bash
97curl -s -X POST https://api.x.ai/v1/images/generations \
98 -H "Authorization: Bearer $XAI_API_KEY" \
99 -H "Content-Type: application/json" \
100 -d '{
101 "model": "grok-imagine-image-quality",
102 "prompt": "A red circle on a white background",
103 "response_format": "url",
104 "storage_options": {"filename": "circle.jpg"}
105 }'
106```
107
108## Expiry Behaviour
109
110`storage_options` exposes two independent expiry knobs: `storage_options.expires_after` controls when the **stored file** auto-deletes, and `storage_options.public_url.expires_after` controls when the **public URL** auto-revokes. Omit `public_url.expires_after` and the URL inherits the file's expiry (or never expires if the file has none).
111
112A public URL can never outlive its file, and both values must be between **1 hour and 30 days**. See [Public URLs → Expiry Behaviour](/developers/files/public-urls#expiry-behaviour) for the full rules; the examples below show how the two knobs combine on an Imagine request.
113
114```python customLanguage="pythonXAI"
115import os
116import xai_sdk
117
118client = xai_sdk.Client(api_key=os.getenv("XAI_API_KEY"))
119
120# Permanent file, public URL expires in 24h.
121response = client.image.sample(
122 prompt="A futuristic city skyline at night",
123 model="grok-imagine-image-quality",
124 storage_options={"filename": "skyline.jpg", "public_url": {"expires_after": 86400}}, # 24h
125)
126print(response.file_output.file_id) # file_...
127print(response.public_url) # https://files-cdn.x.ai/<token>/file_....jpg
128print(response.file_output.public_url_expires_at) # protobuf Timestamp, ~24h from now
129
130# File and public URL both expire in 2h (URL inherits the file's expiry).
131response = client.image.sample(
132 prompt="A futuristic city skyline at night",
133 model="grok-imagine-image-quality",
134 storage_options={"filename": "skyline.jpg", "expires_after": 7200, "public_url": True},
135)
136print(response.file_output.file_id) # file_...
137print(response.public_url)
138print(response.file_output.expires_at) # ~2h from now
139print(response.file_output.public_url_expires_at) # matches file's expires_at
140
141# File expires in 24h, public URL expires in 1h (independent, shorter).
142response = client.image.sample(
143 prompt="A futuristic city skyline at night",
144 model="grok-imagine-image-quality",
145 storage_options={
146 "filename": "skyline.jpg",
147 "expires_after": 86400,
148 "public_url": {"expires_after": 3600},
149 },
150)
151print(response.file_output.file_id) # file_...
152print(response.public_url)
153print(response.file_output.expires_at) # ~24h from now
154print(response.file_output.public_url_expires_at) # ~1h from now (URL dies before file)
155```
156
157```bash
158# Permanent file, 24h public URL
159curl -s -X POST https://api.x.ai/v1/images/generations \
160 -H "Authorization: Bearer $XAI_API_KEY" \
161 -H "Content-Type: application/json" \
162 -d '{
163 "model": "grok-imagine-image-quality",
164 "prompt": "A futuristic city skyline at night",
165 "response_format": "url",
166 "storage_options": {
167 "filename": "skyline.jpg",
168 "public_url": {"expires_after": 86400}
169 }
170 }'
171
172# 2h file, public URL inherits the same expiry
173curl -s -X POST https://api.x.ai/v1/images/generations \
174 -H "Authorization: Bearer $XAI_API_KEY" \
175 -H "Content-Type: application/json" \
176 -d '{
177 "model": "grok-imagine-image-quality",
178 "prompt": "A futuristic city skyline at night",
179 "response_format": "url",
180 "storage_options": {
181 "filename": "skyline.jpg",
182 "expires_after": 7200,
183 "public_url": true
184 }
185 }'
186```
187
188## The `file_output` Response
189
190Every Imagine response with `storage_options` set includes a `file_output` block on each generated asset:
191
192| Field | Always present | Meaning |
193| ----------------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
194| `file_id` | yes | Stable Files API identifier. Use this for `client.files.*` operations. |
195| `filename` | yes | The filename you provided in `storage_options`. |
196| `expires_at` | only when the file has an expiration set | Unix timestamp when the file expires. |
197| `public_url` | only when `storage_options.public_url` was set and creation succeeded | The permanent shareable URL. |
198| `public_url_expires_at` | only when the public URL has an expiry | Unix timestamp when the public URL dies. Absent for permanent URLs. |
199| `public_url_error` | only on partial failure | Human-readable error when the asset was stored but public URL creation failed. See [Public URL Errors](#public-url-errors). |
200
201The Python SDK also surfaces `response.public_url` and `response.public_url_error` as top-level shortcuts on `ImageResponse` and `VideoResponse`.
202
203## Multiple Outputs (`n > 1`)
204
205When you request multiple images in a single call, each image gets its own `file_id` and its own `public_url` with a unique token. The files are completely independent — revoking or deleting one does not affect the others.
206
207```python customLanguage="pythonXAI"
208import os
209import xai_sdk
210
211client = xai_sdk.Client(api_key=os.getenv("XAI_API_KEY"))
212
213responses = client.image.sample_batch(
214 prompt="A cat wearing a hat, four different art styles",
215 model="grok-imagine-image-quality",
216 n=4,
217 storage_options={"filename": "cat-styles.jpg", "public_url": True},
218)
219
220for r in responses:
221 print(r.file_output.file_id, r.public_url)
222```
223
224```bash
225curl -s -X POST https://api.x.ai/v1/images/generations \
226 -H "Authorization: Bearer $XAI_API_KEY" \
227 -H "Content-Type: application/json" \
228 -d '{
229 "model": "grok-imagine-image-quality",
230 "prompt": "A cat wearing a hat, four different art styles",
231 "n": 4,
232 "response_format": "url",
233 "storage_options": {"filename": "cat-styles.jpg", "public_url": true}
234 }'
235```
236
237## Storing Image Edit Outputs
238
239`storage_options` works on `/v1/images/edits` the same way as `/v1/images/generations`. The edited result is stored as a new file.
240
241```python customLanguage="pythonXAI"
242import os
243import xai_sdk
244
245client = xai_sdk.Client(api_key=os.getenv("XAI_API_KEY"))
246
247response = client.image.sample(
248 prompt="Add a party hat to the dog",
249 model="grok-imagine-image-quality",
250 image_url="https://docs.x.ai/assets/api-examples/images/style-realistic.png",
251 storage_options={"filename": "party-dog.png", "public_url": True},
252)
253
254print(response.file_output.file_id) # new file, not the input
255print(response.public_url)
256```
257
258```bash
259curl -s -X POST https://api.x.ai/v1/images/edits \
260 -H "Authorization: Bearer $XAI_API_KEY" \
261 -H "Content-Type: application/json" \
262 -d '{
263 "model": "grok-imagine-image-quality",
264 "prompt": "Add a party hat to the dog",
265 "image": {
266 "url": "https://docs.x.ai/assets/api-examples/images/style-realistic.png"
267 },
268 "response_format": "url",
269 "storage_options": {"filename": "party-dog.png", "public_url": true}
270 }'
271```
272
273## Storing Video Outputs
274
275The video endpoints (`/v1/videos/generations`, `/v1/videos/edits`, `/v1/videos/extensions`) use the same `storage_options` shape. Since video generation is asynchronous, `file_output.public_url` is populated on the **completed** response after the video finishes generating.
276
277```python customLanguage="pythonXAI"
278import os
279import xai_sdk
280
281client = xai_sdk.Client(api_key=os.getenv("XAI_API_KEY"))
282
283# SDK handles polling automatically and returns the completed video.
284response = client.video.generate(
285 prompt="A ball bouncing slowly on a flat surface",
286 model="grok-imagine-video",
287 duration=5,
288 storage_options={"filename": "bouncing-ball.mp4", "public_url": True},
289)
290
291print(response.url) # ephemeral vidgen URL
292print(response.file_output.file_id) # file_...
293print(response.public_url) # https://files-cdn.x.ai/<token>/file_....mp4
294```
295
296```bash
297# Start the generation
298REQUEST_ID=$(curl -s -X POST https://api.x.ai/v1/videos/generations \
299 -H "Authorization: Bearer $XAI_API_KEY" \
300 -H "Content-Type: application/json" \
301 -d '{
302 "model": "grok-imagine-video",
303 "prompt": "A ball bouncing slowly on a flat surface",
304 "duration": 5,
305 "storage_options": {"filename": "bouncing-ball.mp4", "public_url": true}
306 }' | jq -r '.request_id')
307
308# Poll until done
309while true; do
310 RESULT=$(curl -s "https://api.x.ai/v1/videos/$REQUEST_ID" \
311 -H "Authorization: Bearer $XAI_API_KEY")
312 STATUS=$(echo "$RESULT" | jq -r '.status')
313 if [ "$STATUS" = "done" ]; then
314 echo "$RESULT" | jq '.video.file_output'
315 break
316 fi
317 sleep 5
318done
319```
320
321Image-to-video, video editing, and video extension all accept `storage_options` the same way.
322
323## Public URL Errors
324
325In rare cases, public URL creation can fail even when asset generation and storage succeed — for example, on a transient infrastructure issue or when the team has hit its [active-URL quota](/developers/files/public-urls#limitations). Rather than tearing down the whole request, the response still includes a valid `file_output.file_id` and the original asset URL, and only `public_url` is replaced by `public_url_error`:
326
327```json
328{
329 "data": [
330 {
331 "url": "https://imgen.x.ai/.../xai-tmp-imgen-abc123.jpg",
332 "file_output": {
333 "file_id": "file_abc123",
334 "filename": "poster.jpg",
335 "public_url_error": "Public URL creation timed out. The file was stored successfully."
336 }
337 }
338 ]
339}
340```
341
342If you see `public_url_error`, the file is still in your storage — you can retry public URL creation directly via the [Files API](/developers/files/public-urls) without regenerating the asset:
343
344```python customLanguage="pythonXAI"
345import os
346import xai_sdk
347
348client = xai_sdk.Client(api_key=os.getenv("XAI_API_KEY"))
349
350response = client.image.sample(
351 prompt="A vintage poster",
352 model="grok-imagine-image-quality",
353 storage_options={"filename": "poster.jpg", "public_url": True},
354)
355
356if response.public_url_error:
357 # The image is stored — just retry the public URL on the file directly.
358 resp = client.files.create_public_url(response.file_output.file_id)
359 public_url = resp.public_url
360else:
361 public_url = response.public_url
362
363print(public_url)
364```
365
366## Managing Files Created via Imagine
367
368Files created through `storage_options` are full first-class Files API files. Use the [Files API](/developers/files/managing-files) to list, retrieve, update, and delete them, and use the public URL endpoints to revoke or re-create the URL after the fact:
369
370```python customLanguage="pythonXAI"
371import os
372import xai_sdk
373
374client = xai_sdk.Client(api_key=os.getenv("XAI_API_KEY"))
375
376response = client.image.sample(
377 prompt="A futuristic city",
378 model="grok-imagine-image-quality",
379 storage_options={"filename": "city.jpg", "public_url": True},
380)
381file_id = response.file_output.file_id
382
383# Inspect file metadata
384file = client.files.get(file_id)
385
386# Stop sharing publicly (keeps the file in your storage)
387client.files.revoke_public_url(file_id)
388
389# Generate a new public URL later with a different expiry
390client.files.create_public_url(file_id, expires_after=604800) # 7 days
391
392# Delete the file entirely (also tears down any active public URL)
393client.files.delete(file_id)
394```
395
396```bash
397FILE_ID="<file_output.file_id from the generation response>"
398
399# Stop sharing publicly (file stays in your storage)
400curl -s -X POST "https://api.x.ai/v1/files/$FILE_ID/public-url/revoke" \
401 -H "Authorization: Bearer $XAI_API_KEY"
402
403# Re-create with a 7-day expiry
404curl -s -X POST "https://api.x.ai/v1/files/$FILE_ID/public-url" \
405 -H "Authorization: Bearer $XAI_API_KEY" \
406 -H "Content-Type: application/json" \
407 -d '{"expires_after": 604800}'
408
409# Delete the file (also revokes the public URL)
410curl -s -X DELETE "https://api.x.ai/v1/files/$FILE_ID" \
411 -H "Authorization: Bearer $XAI_API_KEY"
412```
413
414## Limitations
415
416* **Up to 1,000 active public URLs per team.** Hitting the cap sets `public_url_error` on the response; revoke unused URLs to free up slots.
417* **Expiry constraints** (see [Expiry Behaviour](#expiry-behaviour)):
418 * Both `expires_after` values must be between **1 hour and 30 days**.
419 * `public_url.expires_after` must be ≤ the file's `expires_after`.
420* **Custom filenames affect the public URL path.** Passing `storage_options.filename = "my-cover.png"` makes the public URL end in `.png`. The stored content type is still determined by the generated asset, not the filename.
421* **`response_format` does not affect storage.** `storage_options` runs whether you request `url` or `b64_json`.
422* **Public URLs are independent of the ephemeral URL.** Both are returned in the same response but have separate lifecycles. Revoking the public URL does not affect the ephemeral URL.
423* **All validation is synchronous.** Invalid storage configs are rejected before generation starts, so you never waste compute on a malformed request.
424
425## Related
426
427* [Files API Integration](/developers/model-capabilities/imagine/files) — Overview + capstone example showing inputs and outputs together.
428* [Referencing Files as Input](/developers/model-capabilities/imagine/files/inputs) — The input side: pass stored `file_id`s in place of URLs.
429* [Files → Public URLs](/developers/files/public-urls) — Public URL lifecycle for any file, regardless of how it was created.
430* [Managing Files](/developers/files/managing-files) — Upload, list, retrieve, update, and delete files.