calesthio/xai-grok-imagine-image
Generate and edit production images with xAI's first-party Grok Imagine API, including model selection, multiple references, synchronous and Batch API workflows, durable output handling, prompting, iteration, QA, cost and rate controls, privacy, safety, and rights review. Use for direct xAI image API integrations, not Grok video or third-party gateways.
npx skills add https://github.com/calesthio/generative-media-skills --skill xai-grok-imagine-image
Operate xAI's first-party image API as a production system, not as a prompt toy. Keep the image workflow separate from Grok Imagine video: image generation and editing are synchronous in the direct REST API, while video has a different asynchronous request/poll contract.
All volatile facts were verified against first-party xAI documentation on 2026-07-09. Recheck the model endpoint, console, pricing, rate limits, release notes, and legal terms before deployment.
Use only the documented inference base and routes:
POST https://api.x.ai/v1/images/generations
POST https://api.x.ai/v1/images/edits
Authorization: Bearer {XAI_API_KEY}
Content-Type: application/json
Create a team-scoped key with the minimum endpoint/model ACLs needed. Load it from a secret manager or XAI_API_KEY; never put it in source, logs, prompts, asset metadata, browser code, or URLs.
| Model | Current aliases | 1K output | 2K output | Input image | Use |
|---|---|---:|---:|---:|---|
| grok-imagine-image-quality | grok-imagine-image-quality-20260403, grok-imagine-image-quality-latest; retired grok-imagine-image-pro redirects here | $0.05 | $0.07 | $0.01 each | Higher-quality production candidate; stronger realism/text/control are provider claims |
| grok-imagine-image | grok-imagine-image-2026-03-02 | $0.02 | $0.02 | $0.002 each | Lower-cost candidate and high-volume exploration |
These prices are USD and were displayed by xAI on 2026-07-09. Pricing is per generated output plus each image input. Text prompt length is not separately billed on these image model pages. Confirm the invoice and current model page.
The public model pages list both models in us-east-1 and us-west-2 and the public REST base remains api.x.ai. Do not infer data residency from the generic host or a marketing statement. xAI advertises enterprise regional-processing/data-residency options, but the reviewed public image contract does not provide a self-service region parameter. Obtain a written regional commitment when residency matters.
grok-imagine-image-pro was retired on 2026-05-15 at 12:00 PT and now redirects to grok-imagine-image-quality. Do not use the retired slug in new code even though it still appears as an alias. Prefer a dated alias for controlled regression testing and an undated canonical model only when accepting provider updates. Record the actual model/fingerprint where exposed.
Before launch, query the models available to the actual key:
: "${XAI_API_KEY:?set XAI_API_KEY}"
curl --fail-with-body --silent --show-error \
https://api.x.ai/v1/image-generation-models \
-H "Authorization: Bearer ${XAI_API_KEY}"
The model-list response includes IDs, aliases, modalities, version/fingerprint, and max_prompt_length. The current reference example shows 1024, but the reviewed schema does not explain whether that unit is characters or tokens. Treat the live field as provider-defined and validate requests rather than labeling its unit.
Send prompt plus an explicit model. Optional fields are:
n: number of variations. The Imagine overview documents up to 10 images per generation request. Every successful output is billed.aspect_ratio: 1:1, 3:4, 4:3, 9:16, 16:9, 2:3, 3:2, 9:19.5, 19.5:9, 9:20, 20:9, 1:2, 2:1, or auto.resolution: lowercase 1k or 2k.response_format: url or b64_json.storage_options: persist outputs into the xAI Files API.user: a unique end-user identifier used for abuse monitoring. Send a stable pseudonymous value, not raw email, name, or other unnecessary personal data.No seed, negative-prompt field, guidance scale, mask, output-format selector, quality scalar, or streaming flag appears in the current REST image schema. Do not invent one. Filename extensions in storage_options do not convert the generated bytes.
Use /v1/images/edits with prompt, model, and exactly one of:
image: one source object; orimages: an array of up to three source objects.Each source object carries either:
url: a public HTTPS URL or base64 data URL; orfile_id: a fully uploaded private xAI Files API image.url and file_id are mutually exclusive inside an object. Supported edit inputs are JPEG, PNG, and WebP. The current direct image schema does not publish a maximum source byte size; enforce a conservative application limit and test it against the live service. The Batch API's 25 MB per-request payload limit is not evidence of the direct route's limit.
For multiple sources, refer to them as <IMAGE_0>, <IMAGE_1>, and <IMAGE_2> in request order. Multi-image editing can override aspect_ratio; by default it follows the first input. A single-image edit respects its input aspect ratio, so do not promise that aspect_ratio will reshape it.
Although n exists in the edit schema, the current overview's explicit “up to 10” statement is scoped to image generation. Default edits to one output and feature-test larger edit counts rather than assuming the same ceiling.
The OpenAI SDK's images.edit() helper is not compatible because it sends multipart form data; xAI's edit route requires JSON. Use direct HTTP, the xAI SDK, or a compatible Vercel AI SDK path. The OpenAI SDK can still call /images/generations; xAI-specific fields such as aspect_ratio and resolution may require extra_body in Python.
The first-party use-case guide recommends being specific about subject, style, lighting, composition, and mood. Expand that into an auditable brief:
Deliverable and placement; subject and action; environment and era;
camera/viewpoint and composition; light and palette; materials and rendering medium;
exact visible text and hierarchy; must-preserve details; exclusions; variation intent.
Use an operation plus invariants:
Use <IMAGE_0> as the base composition. Replace only the chair fabric with the
navy herringbone material from <IMAGE_1>. Keep camera, crop, room geometry,
person, face, hands, lighting direction, shadows, wall art, and all text unchanged.
Do not add objects.
Assign one role to every reference. State which image owns identity/geometry, which supplies style/material, and what must not transfer. There is no documented mask input, so do not promise pixel-local inpainting. If locality is critical, ask for a tightly described edit, inspect the result, and use a separate deterministic compositor/masking tool when required.
xAI documents multi-turn editing by feeding each output into the next request. This is a chain of independent image calls, not a server-side conversation. Persist each approved state and its prompt.
For broad exploration, use the lower-cost model and/or 1K. Promote shortlisted prompts to Quality/2K only after composition is stable. This is a production heuristic, not a quality guarantee.
The direct image routes synchronously return the completed data[] and usage; they do not return a video-style request_id. Async SDK clients merely run multiple synchronous calls concurrently. Use the separate Batch API for server-side queued image work.
Each data item can include:
url for URL responses, or b64_json without a data-URI prefix;mime_type;file_output when persistence was requested;storage_error when generation succeeded but Files persistence failed.file_output, when private persistence succeeds, includes file_id, filename, and optional expiry fields. public_url or public_url_error is conditional on requesting a public link; neither is expected for ordinary private-only storage. Public URL creation or Files persistence can fail while the generated ephemeral artifact remains valid. Salvage that artifact first, then repair storage/public-link state through the Files API without regenerating and paying again.
usage.cost_in_usd_ticks is exact request cost. xAI defines 100,000,000 ticks as one US cent and 10,000,000,000 ticks as one US dollar.
imgen.x.ai URL: documented only as ephemeral/short-lived. No exact lifetime is published for direct image responses. Download immediately.storage_options.expires_after is omitted, or expires after 3,600–2,592,000 seconds (1 hour–30 days).Keep outputs private by default. Use storage_options: {"filename": "..."} without public_url for iterative work. If sharing is necessary, use a short expiry and revoke it after delivery. A custom extension affects the public URL path, not the stored MIME type.
Authorization to imgen.x.ai or files-cdn.x.ai downloads; only xAI API/Files endpoints receive the token.This complete Python example is an example, not a mandatory formula and was not executed because it would incur charges. Install Pillow first. It defaults to a redacted dry run, generates two 2K Quality candidates only after explicit opt-in, caps the response and decoded bytes, verifies signatures/MIME/pixels, writes atomically, and records exact billed cost.
import base64
import binascii
import hashlib
import io
import json
import os
import pathlib
import tempfile
import urllib.error
import urllib.request
from PIL import Image, UnidentifiedImageError
API_URL = "https://api.x.ai/v1/images/generations"
MAX_RESPONSE_BYTES = 80 * 1024 * 1024
MAX_IMAGE_BYTES = 30 * 1024 * 1024
MAX_IMAGE_PIXELS = 20_000_000
OUT = pathlib.Path("grok-imagine-candidates")
Image.MAX_IMAGE_PIXELS = MAX_IMAGE_PIXELS
prompt = (
"16:9 website hero for a premium outdoor tea brand. A brushed steel kettle "
"and two matte ceramic cups on a dark slate ledge at dawn in the mountains. "
"Camera at tabletop height, generous clear negative space on the left, soft "
"mist, warm sunrise rim light against cool blue shadows, realistic metal and "
"ceramic textures, restrained editorial photography. No people, logos, labels, "
"letters, or watermarks. Create distinct composition variations."
)
payload = {
"model": "grok-imagine-image-quality",
"prompt": prompt,
"n": 2,
"aspect_ratio": "16:9",
"resolution": "2k",
"response_format": "b64_json",
"user": os.getenv("XAI_END_USER_ID", "internal-demo-user"),
}
body = json.dumps(payload, separators=(",", ":")).encode("utf-8")
request = urllib.request.Request(
API_URL,
data=body,
method="POST",
headers={
"Authorization": "Bearer " + os.environ.get("XAI_API_KEY", ""),
"Content-Type": "application/json",
"Accept": "application/json",
},
)
print({
"model": payload["model"], "n": payload["n"],
"resolution": payload["resolution"],
"request_sha256": hashlib.sha256(body).hexdigest(),
"estimated_usd_snapshot_2026_07_09": 0.14,
})
if os.environ.get("SEND_XAI_REQUEST") != "1":
raise SystemExit("Dry run only; set SEND_XAI_REQUEST=1 after approving the paid call")
if not os.environ.get("XAI_API_KEY"):
raise SystemExit("Set XAI_API_KEY before the paid call")
try:
with urllib.request.urlopen(request, timeout=240) as response:
zdr = response.headers.get("x-zero-data-retention")
raw = response.read(MAX_RESPONSE_BYTES + 1)
except urllib.error.HTTPError as exc:
detail = exc.read(64 * 1024).decode("utf-8", "replace")
raise SystemExit(f"xAI HTTP {exc.code}: {detail}") from exc
if len(raw) > MAX_RESPONSE_BYTES:
raise SystemExit("Response exceeded configured cap")
document = json.loads(raw)
items = document.get("data")
if not isinstance(items, list) or len(items) != payload["n"]:
raise SystemExit(f"Expected {payload['n']} outputs, received {len(items or [])}")
OUT.mkdir(parents=True, exist_ok=True)
artifacts = []
for index, item in enumerate(items):
encoded = item.get("b64_json")
if not encoded:
raise SystemExit(f"Output {index} has no base64 image")
if len(encoded) > ((MAX_IMAGE_BYTES + 2) // 3) * 4 + 8:
raise SystemExit(f"Output {index} exceeds encoded-size cap")
try:
image = base64.b64decode(encoded, validate=True)
except (binascii.Error, ValueError) as exc:
raise SystemExit(f"Output {index} is not strict base64") from exc
if len(image) > MAX_IMAGE_BYTES:
raise SystemExit(f"Output {index} exceeds decoded-size cap")
if image.startswith(b"\xff\xd8\xff"):
suffix, expected_mime = ".jpg", "image/jpeg"
elif image.startswith(b"\x89PNG\r\n\x1a\n"):
suffix, expected_mime = ".png", "image/png"
elif image.startswith((b"RIFF",)) and image[8:12] == b"WEBP":
suffix, expected_mime = ".webp", "image/webp"
else:
raise SystemExit(f"Output {index} has an unexpected signature")
declared = item.get("mime_type")
if declared and declared != expected_mime:
raise SystemExit(f"Output {index} MIME/signature mismatch")
try:
with Image.open(io.BytesIO(image)) as probe:
dimensions = probe.size
if dimensions[0] * dimensions[1] > MAX_IMAGE_PIXELS:
raise SystemExit(f"Output {index} exceeds pixel cap: {dimensions}")
decoded_format = probe.format
probe.verify()
with Image.open(io.BytesIO(image)) as probe:
probe.load()
except (UnidentifiedImageError, OSError, Image.DecompressionBombError) as exc:
raise SystemExit(f"Output {index} failed bounded pixel decode") from exc
expected_format = {"image/jpeg": "JPEG", "image/png": "PNG", "image/webp": "WEBP"}[expected_mime]
if decoded_format != expected_format:
raise SystemExit(f"Output {index} decoder/signature mismatch")
destination = OUT / f"candidate-{index + 1:02d}{suffix}"
fd, temporary = tempfile.mkstemp(dir=OUT, prefix=".grok-imagine-")
try:
with os.fdopen(fd, "wb") as stream:
fd = None
stream.write(image)
stream.flush()
os.fsync(stream.fileno())
os.replace(temporary, destination)
except Exception:
if fd is not None:
os.close(fd)
try:
os.unlink(temporary)
except FileNotFoundError:
pass
raise
artifacts.append({
"path": str(destination),
"bytes": len(image),
"mime_type": expected_mime,
"dimensions": list(dimensions),
"sha256": hashlib.sha256(image).hexdigest(),
})
manifest = {
"model_requested": payload["model"],
"request_sha256": hashlib.sha256(body).hexdigest(),
"zero_data_retention_header": zdr,
"usage": document.get("usage"),
"artifacts": artifacts,
"review_status": "pending",
}
(OUT / "manifest.json").write_text(
json.dumps(manifest, indent=2, ensure_ascii=False), encoding="utf-8"
)
print(json.dumps(manifest, indent=2))
Production code should additionally decode pixels with a maintained image library to enforce dimensions/area and run the application's malware/content scanners.
This complete example is an example, not a mandatory formula and was not executed. Install Pillow first. It defaults to a redacted dry run, expects two already-uploaded private image file IDs, uses documented reference tags, requests private persistence for 24 hours, and salvages the ephemeral result without forwarding the API key even if Files persistence fails. Keeping references in Files avoids public source URLs.
import hashlib
import ipaddress
import json
import os
import pathlib
import socket
import tempfile
import urllib.error
import urllib.parse
import urllib.request
from PIL import Image, UnidentifiedImageError
API_URL = "https://api.x.ai/v1/images/edits"
ALLOWED_ASSET_HOSTS = {"imgen.x.ai", "files-cdn.x.ai"}
MAX_DOWNLOAD_BYTES = 30 * 1024 * 1024
MAX_RESPONSE_BYTES = 4 * 1024 * 1024
MAX_IMAGE_PIXELS = 20_000_000
Image.MAX_IMAGE_PIXELS = MAX_IMAGE_PIXELS
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
def validate_asset_url(url):
parsed = urllib.parse.urlsplit(url)
if (
parsed.scheme != "https" or parsed.hostname not in ALLOWED_ASSET_HOSTS
or parsed.username or parsed.password or parsed.fragment
):
raise ValueError("Unexpected artifact URL")
addresses = {
info[4][0] for info in socket.getaddrinfo(parsed.hostname, parsed.port or 443)
}
if not addresses or any(not ipaddress.ip_address(value).is_global for value in addresses):
raise ValueError("Artifact host did not resolve exclusively to public addresses")
def atomic_write(path, data):
path.parent.mkdir(parents=True, exist_ok=True)
handle, temporary = tempfile.mkstemp(dir=path.parent, prefix=".manifest-")
try:
with os.fdopen(handle, "wb") as output:
handle = None
output.write(data)
output.flush()
os.fsync(output.fileno())
os.replace(temporary, path)
except Exception:
if handle is not None:
os.close(handle)
try:
os.unlink(temporary)
except FileNotFoundError:
pass
raise
payload = {
"model": "grok-imagine-image-quality",
"prompt": (
"Use <IMAGE_0> as the base product photograph. Replace only its background "
"with the pale sandstone studio texture and warm side-lighting from "
"<IMAGE_1>. Preserve the product geometry, camera angle, crop, label layout, "
"label spelling, colors, reflections, and shadow contact exactly. Do not add "
"props, text, people, or logos."
),
"images": [
{"file_id": os.environ.get("XAI_REFERENCE_FILE_ID_0", "<required-file-id-0>")},
{"file_id": os.environ.get("XAI_REFERENCE_FILE_ID_1", "<required-file-id-1>")},
],
"n": 1,
"aspect_ratio": "4:3",
"resolution": "2k",
"response_format": "url",
"storage_options": {"filename": "product-sandstone.jpg", "expires_after": 86400},
}
body = json.dumps(payload, separators=(",", ":")).encode("utf-8")
request = urllib.request.Request(
API_URL,
data=body,
method="POST",
headers={
"Authorization": "Bearer " + os.environ.get("XAI_API_KEY", ""),
"Content-Type": "application/json",
"Accept": "application/json",
},
)
print({
"model": payload["model"], "references": len(payload["images"]),
"resolution": payload["resolution"],
"request_sha256": hashlib.sha256(body).hexdigest(),
"estimated_usd_snapshot_2026_07_09": 0.09,
})
if os.environ.get("SEND_XAI_REQUEST") != "1":
raise SystemExit("Dry run only; set SEND_XAI_REQUEST=1 after approving the paid call")
if not os.environ.get("XAI_API_KEY"):
raise SystemExit("Set XAI_API_KEY before the paid call")
if any(value["file_id"].startswith("<required-") for value in payload["images"]):
raise SystemExit("Set both XAI_REFERENCE_FILE_ID environment variables")
try:
with urllib.request.urlopen(request, timeout=240) as response:
raw = response.read(MAX_RESPONSE_BYTES + 1)
except urllib.error.HTTPError as exc:
detail = exc.read(64 * 1024).decode("utf-8", "replace")
raise SystemExit(f"xAI HTTP {exc.code}: {detail}") from exc
if len(raw) > MAX_RESPONSE_BYTES:
raise SystemExit("Edit response exceeded configured JSON cap")
try:
document = json.loads(raw)
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise SystemExit("Edit response was not valid bounded JSON") from exc
items = document.get("data") or []
if len(items) != 1:
raise SystemExit(f"Expected one edit output, received {len(items)}")
item = items[0]
storage_error = item.get("storage_error")
file_output = item.get("file_output") or {}
if not storage_error and not file_output.get("file_id"):
raise SystemExit("Missing persisted file_id")
def download_xai_asset(url: str, destination: pathlib.Path) -> dict:
opener = urllib.request.build_opener(NoRedirect())
current = url
response = None
for _ in range(6):
validate_asset_url(current)
# Deliberately omit XAI_API_KEY on every ephemeral/public asset hop.
asset_request = urllib.request.Request(
current, headers={"Accept": "image/jpeg,image/png,image/webp"}
)
try:
response = opener.open(asset_request, timeout=90)
break
except urllib.error.HTTPError as exc:
if exc.code not in {301, 302, 303, 307, 308}:
raise
location = exc.headers.get("Location")
exc.close()
if not location:
raise ValueError("Artifact redirect omitted Location")
current = urllib.parse.urljoin(current, location)
else:
raise ValueError("Too many artifact redirects")
total = 0
prefix = b""
hasher = hashlib.sha256()
destination.parent.mkdir(parents=True, exist_ok=True)
fd = None
temporary = None
try:
with response:
mime = response.headers.get_content_type()
if mime not in {"image/jpeg", "image/png", "image/webp"}:
raise ValueError(f"Unexpected artifact MIME: {mime}")
declared = response.headers.get("Content-Length")
if declared and int(declared) > MAX_DOWNLOAD_BYTES:
raise ValueError("Artifact exceeds configured cap")
fd, temporary = tempfile.mkstemp(dir=destination.parent, prefix=".download-")
with os.fdopen(fd, "wb") as output:
fd = None
while True:
chunk = response.read(64 * 1024)
if not chunk:
break
total += len(chunk)
if total > MAX_DOWNLOAD_BYTES:
raise ValueError("Artifact exceeded cap while streaming")
prefix += chunk[: max(0, 12 - len(prefix))]
hasher.update(chunk)
output.write(chunk)
output.flush()
os.fsync(output.fileno())
valid = (
prefix.startswith(b"\xff\xd8\xff")
or prefix.startswith(b"\x89PNG\r\n\x1a\n")
or (prefix.startswith(b"RIFF") and prefix[8:12] == b"WEBP")
)
if not valid:
raise ValueError("Unexpected artifact signature")
expected_mime = (
"image/jpeg" if prefix.startswith(b"\xff\xd8\xff")
else "image/png" if prefix.startswith(b"\x89PNG\r\n\x1a\n")
else "image/webp"
)
if mime != expected_mime:
raise ValueError("Artifact MIME/signature mismatch")
try:
with Image.open(temporary) as probe:
dimensions = probe.size
if dimensions[0] * dimensions[1] > MAX_IMAGE_PIXELS:
raise ValueError(f"Artifact exceeds pixel cap: {dimensions}")
decoded_format = probe.format
probe.verify()
with Image.open(temporary) as probe:
probe.load()
except (UnidentifiedImageError, OSError, Image.DecompressionBombError) as exc:
raise ValueError("Artifact failed bounded pixel decode") from exc
expected_format = {"image/jpeg": "JPEG", "image/png": "PNG", "image/webp": "WEBP"}[mime]
if decoded_format != expected_format:
raise ValueError("Artifact decoder/signature mismatch")
os.replace(temporary, destination)
temporary = None
except Exception:
if fd is not None:
os.close(fd)
try:
if temporary is not None:
os.unlink(temporary)
except FileNotFoundError:
pass
raise
return {"bytes": total, "sha256": hasher.hexdigest(), "mime_type": mime,
"dimensions": list(dimensions)}
if not item.get("url"):
raise SystemExit("Missing ephemeral artifact URL")
path = pathlib.Path("grok-imagine-edit") / "product-sandstone.bin"
artifact = download_xai_asset(item["url"], path)
manifest = {
"request_sha256": hashlib.sha256(body).hexdigest(),
"file_id": file_output.get("file_id"),
"file_expires_at": file_output.get("expires_at"),
"storage_error": storage_error,
"usage": document.get("usage"),
"artifact": {"path": str(path), **artifact},
"review_status": "pending",
}
atomic_write(path.with_suffix(".json"), json.dumps(manifest, indent=2).encode("utf-8"))
print(json.dumps(manifest, indent=2))
if storage_error:
print("Generation succeeded; retry Files persistence from the saved bytes without regenerating")
Use a MIME-derived extension instead of .bin after production pixel decoding. If xAI changes its documented asset hosts, update the allowlist only after verification.
Use direct REST for interactive work. Use the Batch API for queued bulk work:
POST /v1/batches with a name; retain batch_id.POST /v1/batches/{batch_id}/requests with uniquely named batch_request_id values and image_generation or image_edit request objects.GET /v1/batches/{batch_id} until num_pending == 0.GET /v1/batches/{batch_id}/results; match every result to batch_request_id and handle succeeded, failed, and cancelled items.Batch processing usually completes within 24 hours, but xAI calls that best effort, not an SLA. Image/video requests in Batch are billed at standard rates, despite the Batch overview's general reduced-pricing language. Batch requests do not consume normal real-time rate limits. A single batch request payload is capped at 25 MB. A batch may be cancelled or expire; completed results remain available only until the batch expiry.
Example Batch request body (billable if submitted):
{
"batch_requests": [
{
"batch_request_id": "catalog-hero-0001",
"batch_request": {
"image_generation": {
"model": "grok-imagine-image-quality",
"prompt": "1:1 catalog hero of a cobalt ceramic vase on warm white paper, soft overhead light, no text or logo",
"aspect_ratio": "1:1",
"resolution": "1k",
"n": 1
}
}
}
]
}
Use a deterministic business key for batch_request_id and persist it before submission. xAI describes this as useful for idempotency and result linkage inside Batch. The direct image routes do not document an idempotency header or key; do not assume that a client retry is deduplicated.
xAI documents these broad HTTP classes:
| Status | Meaning | Response |
|---|---|---|
| 400/422 | invalid argument or invalid field format; 400 can also cover an incorrect key | Fix request; do not retry unchanged |
| 401/403 | missing/invalid auth, missing permission, blocked key/team | Fix identity/ACL/account state |
| 404/405/415 | model/route missing, wrong method, or missing JSON content type/body | Fix integration or model lifecycle |
| 429 | rate limit | Queue and retry with capped exponential backoff and jitter |
| 5xx/network | transient service or transport failure | Retry only within a bounded policy and reconcile duplicate risk |
The current model pages list 5 requests/second for both image models across tiers; there is no published TPM value. Your console is authoritative for the team's effective limit, and Imagine increases require xAI sales/support. Limit concurrency below the measured ceiling and remember one request can create up to 10 billed outputs.
For 429 or transient failure, honor Retry-After if present, otherwise use exponential backoff with full jitter and a total deadline. Do not retry content moderation, invalid prompt/reference, authorization, or storage configuration failures as transient.
Create a client job ledger before submission with request hash, intended count, attempt, and state. A timeout after the server accepted work is ambiguous: repeating a direct request can produce and bill a second set. Reconcile any response, exact usage, artifact/file records, and account billing before manual replay. Do not claim deterministic regeneration; the current API exposes no seed.
Check storage_error, public_url_error, every data member, and expected count independently. Storage/public-link partial failures should be repaired through Files endpoints, not by regenerating the image.
Estimate the maximum before each request:
generation = n_outputs × output_price(model, resolution)
edit = input_image_count × input_price(model) + n_outputs × output_price(model, resolution)
For example, a Quality edit with three references and one 2K output is currently at most 3 × $0.01 + 1 × $0.07 = $0.10, before taxes/contract adjustments. A ten-image Quality 2K generation is $0.70. Recheck pricing immediately before quoting a customer.
Set per-job caps for output count, model, resolution, attempts, and total cost_in_usd_ticks. Alert on estimate-versus-usage mismatch. Batch images have no current discount. Never hide cost expansion caused by references, n, 2K Quality, or retries.
Files are a separate billing surface: the current public price is $0.025/GiB/day for storage and $0.20/GiB for downloads. Budget expected bytes × retention days plus retrieval volume, set the shortest useful TTL, delete private files and revoke public URLs after delivery, and reconcile Files charges separately from inference ticks.
Review decoded pixels, not thumbnails or provider URLs. A release candidate must pass:
Maintain a fixed regression suite spanning photorealism, illustration, typography, product labels, one-reference edits, three-reference composition, extreme ratios, 1K/2K, n=10, base64, ephemeral download, Files partial failure, 429, timeout, moderation, and Batch expiry. Blindly compare canonical versus dated aliases before migrating.
The current API security FAQ says xAI does not train on API inputs or outputs without explicit permission. Standard API requests/responses are retained for 30 days for abuse/misuse audit and then deleted; enterprise ZDR is available and the x-zero-data-retention response header reports its state. Under the current enterprise terms, Personal Data must be submitted exclusively through a ZDR-enabled API; PHI requires both an applicable BAA and ZDR. Treat this as a contractual gate for identifiable-person inputs, not an optional optimization. The terms add exceptions for law, safety, security, moderation, abuse prevention, and investigations, so the signed contract controls.
Separate User Content from derived data: except under ZDR, the enterprise terms permit xAI to create de-identified or aggregated data derived from service use and use it for service maintenance/improvement, new products/features, research, benchmarking, and other lawful purposes, while still stating that User Content is not used to train xAI AI systems without permission. Disclose both rules accurately.
Do not confuse audit retention with intentionally persisted Files output. A private file without TTL can remain until deletion; a public URL can remain accessible until expiry/revocation. The reviewed docs do not state how storage_options interacts with ZDR. If both are required, obtain a written answer and test the header/Files behavior rather than assuming compatibility.
Under the current enterprise terms, as between xAI and the customer, the customer retains Input rights and owns Output; xAI assigns its Output interest to the fullest extent permitted. That does not guarantee copyrightability, exclusivity, accuracy, or non-infringement. Outputs may be similar across users. The customer must have necessary Input rights/consents, evaluate Output, must not represent it as human-generated, and must not use Output to train its or its providers' ML/AI models.
Apply a preflight gate for personal data, faces/likenesses, minors, private locations, copyrighted work, trademarks, credentials, regulated content, and deceptive contexts. The xAI AUP prohibits, among other things, IP/privacy/publicity violations, non-consensual sexualized alteration of real people, deceptive impersonation, child sexual exploitation, defamation/false light, fraud/forgery, doxing/stalking, high-stakes automated decisions, safety bypass, and stripping/circumventing embedded provenance metadata or watermarks.
Obtain documented consent for likeness/reference use, disclose AI involvement, preserve any embedded provenance/watermarks, and add an application-level signed manifest. The reviewed image API contract does not promise C2PA credentials or a visible watermark, so do not claim either. Human review is mandatory for public, commercial, factual, political, medical, legal, financial, identity-sensitive, or otherwise consequential use.
Checked 2026-07-09:
n maximum, direct-request idempotency, deterministic seeds, mask/inpaint inputs, output-format selection, and guaranteed provenance credentials are not documented.$0.05/image; the current detailed pricing/model table charges Quality 2K at $0.07. Use the detailed current price.Take calesthio/xai-grok-imagine-image from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.