mcpbeat

Xai Grok Imagine Image

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.

12k tokens
context cost
the whole folder, loaded on every use
2
files
instructions only
0
copies elsewhere
how many repositories repackaged it
112
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/calesthio/generative-media-skills --skill xai-grok-imagine-image

What comes with it

9 733 bytes besides the instruction
EVAL.md

The instruction itself

23 sections, as written by the author

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.

Evidence vocabulary

  • Documented: stated in current xAI API documentation, model pages, or terms.
  • Provider claim: qualitative language from xAI product or launch material; test it independently.
  • Production heuristic: an operational recommendation derived from the contract and common media practice, not an xAI guarantee.
  • Unknown: not established by the reviewed first-party sources. Do not fill the gap with assumptions.

Lock the interface and model

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.

Current image models

| 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.

Direct request contract

Text-to-image generation

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.

Natural-language image editing

Use /v1/images/edits with prompt, model, and exactly one of:

  • image: one source object; or
  • images: an array of up to three source objects.

Each source object carries either:

  • url: a public HTTPS URL or base64 data URL; or
  • file_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.

Prompt as a production specification

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.

Generation practice

  • State the deliverable first: “16:9 website hero,” “3:2 editorial photograph,” or “1:1 icon exploration.”
  • Name one primary subject and its action before atmosphere or style.
  • Specify spatial relationships, camera height, crop, lens feel, and negative space.
  • Describe light direction/quality and a small palette using plain language; color values are targets, not guarantees.
  • Describe physical materials and surface behavior for product work.
  • Put required copy in quotation marks and specify placement, case, and line breaks. Treat xAI's “stronger text rendering” as a provider claim; run OCR and human proofreading.
  • Say “no visible text/logos” when stray lettering would be a defect. Negative constraints are prose, not a separate API parameter.

Editing practice

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.

Iteration without drift

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.

  • Branch from the last approved state, not automatically from the newest state.
  • Make one semantic change per edit when identity or layout matters.
  • Repeat invariants every turn; the API does not retain them as hidden state.
  • After several edits, compare against the original for crop, identity, text, logo, and background drift.
  • If drift compounds, restart from the approved base with a consolidated 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.

Response and artifact lifecycles

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.

Four distinct lifetimes

  • Default imgen.x.ai URL: documented only as ephemeral/short-lived. No exact lifetime is published for direct image responses. Download immediately.
  • Batch image URL: documented to expire after one hour. Retrieve and copy promptly.
  • Private Files output: persists until deletion when storage_options.expires_after is omitted, or expires after 3,600–2,592,000 seconds (1 hour–30 days).
  • Files public URL: unauthenticated and revocable; can be permanent or expire in 1 hour–30 days, but cannot outlive its file. Up to 1,000 active public URLs per team.

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.

Artifact security rules

  • Never attach Authorization to imgen.x.ai or files-cdn.x.ai downloads; only xAI API/Files endpoints receive the token.
  • Treat returned URLs as untrusted: require HTTPS, reject embedded credentials/fragments, restrict hosts, and recheck redirects.
  • Stream into a temporary file with a hard byte ceiling before buffering or promotion.
  • Verify declared MIME, magic bytes, decoded dimensions/area, and scan policy; never trust the filename.
  • Hash final bytes and atomically promote them into controlled storage.
  • Record request hash, prompt version, references and rights records, model/alias, response MIME, dimensions, cost, file ID/expiry, artifact hash, moderation/review result, and publication decision.

Example: synchronous generation with strict base64 handling

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.

Example: multi-reference edit with private Files persistence

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.

Asynchronous Batch API

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.
  • Poll GET /v1/batches/{batch_id} until num_pending == 0.
  • Page through GET /v1/batches/{batch_id}/results; match every result to batch_request_id and handle succeeded, failed, and cancelled items.
  • Download successful image URLs immediately; batch media URLs expire after one hour.

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.

Failure, retry, and reconciliation

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.

Cost controls

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.

Quality gate

Review decoded pixels, not thumbnails or provider URLs. A release candidate must pass:

  • Brief: correct deliverable, aspect, crop, safe areas, subject count, and requested variants.
  • Reference/edit: base geometry, identity, pose, label, logo, background, lighting, and “unchanged” regions compared against every source.
  • Text: OCR plus human spelling, punctuation, line-break, and legibility review.
  • Visual: hands/faces, repeated structures, reflections, shadows, material continuity, edge halos, unwanted symbols, and compression defects.
  • Brand/rights: palette, marks, licensed assets, likeness consent, trademark context, and prohibited lookalikes.
  • Technical: true MIME/signature, decoded dimensions, color mode/profile, file size, hash, storage/public URL lifecycle, and manifest completeness.
  • Safety: moderation outcome plus application policy and human review.

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.

Data, rights, safety, and provenance

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.

First-party sources

Checked 2026-07-09:

  • Image REST schema: https://docs.x.ai/developers/rest-api-reference/inference/images
  • Image generation: https://docs.x.ai/developers/model-capabilities/images/generation
  • Image editing: https://docs.x.ai/developers/model-capabilities/images/editing
  • Multi-image editing: https://docs.x.ai/developers/model-capabilities/images/multi-image-editing
  • Imagine overview: https://docs.x.ai/developers/model-capabilities/imagine
  • Quality model: https://docs.x.ai/developers/models/grok-imagine-image-quality
  • Standard model: https://docs.x.ai/developers/models/grok-imagine-image
  • Image model discovery: https://docs.x.ai/developers/rest-api-reference/inference/models
  • Files inputs: https://docs.x.ai/developers/model-capabilities/imagine/files/inputs
  • Files outputs and lifecycle: https://docs.x.ai/developers/model-capabilities/imagine/files/outputs
  • Public URLs: https://docs.x.ai/developers/files/public-urls
  • Batch API: https://docs.x.ai/developers/advanced-api-usage/batch-api
  • Pricing and cost tracking: https://docs.x.ai/developers/pricing and https://docs.x.ai/developers/cost-tracking
  • Rate limits and errors: https://docs.x.ai/developers/rate-limits and https://docs.x.ai/developers/debugging
  • Retirement guide: https://docs.x.ai/developers/migration/may-15-retirement
  • API security/ZDR: https://docs.x.ai/developers/faq/security
  • Enterprise terms and DPA: https://x.ai/legal/terms-of-service-enterprise and https://x.ai/legal/data-processing-addendum
  • Acceptable Use Policy: https://x.ai/legal/acceptable-use-policy
  • Prompt/use-case guidance: https://x.ai/grok/use-cases/image-generation
  • Quality Mode announcement: https://x.ai/news/grok-imagine-quality-mode

Dated gaps and conflicts

  • Direct ephemeral image URL lifetime is described only as short-lived; one hour is documented for Batch URLs, not direct URLs.
  • Direct edit source-size limits, the edit n maximum, direct-request idempotency, deterministic seeds, mask/inpaint inputs, output-format selection, and guaranteed provenance credentials are not documented.
  • The Imagine overview headline presents Quality as $0.05/image; the current detailed pricing/model table charges Quality 2K at $0.07. Use the detailed current price.
  • The Batch overview discusses reduced pricing generally, while the current pricing page explicitly says image/video Batch requests use standard rates. Use standard image rates.
  • Public pages advertise enterprise regional/data-residency options but expose no public image request region selector. Contract for residency.
  • The Files/ZDR interaction is not established. Do not promise both without provider confirmation.

How to use it

Copy the folder

Take calesthio/xai-grok-imagine-image from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

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.