AI image API output validation: a developer checklist
Your generation job reports success, but the downloaded artifact is an HTML error page, a truncated PNG, or an image with the wrong dimensions. AI image API output validation should catch these failures before an asset reaches storage marked ready, a customer preview, or a publishing queue. A successful job status is not proof that its output meets your application contract.
For applications using the GenRelay image API, implement validation in your own ingestion pipeline rather than assuming a model response is a publishable asset. The same approach works with other image providers. Separate safe retrieval, byte-level checks, decoded-image checks, and visual acceptance so each failure has an appropriate recovery action.
What should AI image API output validation check first?
Check whether the artifact can be retrieved safely, then verify its decoded structure before evaluating its appearance. Byte integrity means that the downloaded bytes are complete and unchanged relative to a trusted reference; visual acceptance means that the image satisfies the intended creative and product requirements.
Use four explicit gates:
- Retrieval: approved destination, bounded download, successful response, and acceptable transport behavior.
- Structure: recognized decoder format, complete decoding, permitted dimensions, pixel count, and frame count.
- Product contract: expected orientation, transparency behavior, output size, and supported color handling.
- Visual acceptance: requested subject, readable text, branding rules, safety review, and composition.
Record separate outcomes instead of one valid boolean. For example, decode_failed should not share a retry policy with text_mismatch. Keep an artifact quarantined until required gates pass. Quarantine means restricted storage that downstream consumers cannot treat as approved output.
How do you verify the actual image format and size?
Use an image decoder to identify the format and dimensions from the content, then force a full decode. A filename ending in .png and an HTTP Content-Type: image/png header are hints, not evidence that the body is a PNG.
Pillow, a Python image-processing library, initially reads image metadata lazily. Calling Image.open() alone therefore does not establish that all pixel data is readable. Use verify() for structural checks, reopen the bytes, and call load() to exercise decoding. Neither operation is an authenticity check or a complete security boundary.
Set both compressed-byte and decoded-pixel budgets. A decompression bomb is a small compressed input that expands into excessive memory or processing work. Pixel count is an important limit, but decoder allocations, metadata, color conversion, and simultaneous jobs also consume resources. Keep Pillow updated and execute untrusted decoding in a worker with operating-system memory and execution-time limits.
The following example is local: it creates its own PNG fixture and makes no network or generation requests. Its 4096-pixel side limit, 20-million-pixel budget, and 16 MiB input cap are illustrative application policies, not provider limits. The dependency is Pillow; install it in your Python environment:
python3 -m pip install Pillow
Run this complete Python example directly or save it as a local script:
import hashlib
import io
import warnings
from PIL import Image
MAX_BYTES = 16 * 1024 * 1024
MAX_PIXELS = 20_000_000
MAX_SIDE = 4096
ALLOWED = {"PNG", "JPEG", "WEBP"}
def validate_image(data, expected_size=None, allowed_formats=ALLOWED):
if not data or len(data) > MAX_BYTES:
raise ValueError("empty_or_oversized_input")
def check_header(im):
if im.format not in allowed_formats:
raise ValueError("unexpected_format")
width, height = im.size
if (width < 1 or height < 1 or
width > MAX_SIDE or height > MAX_SIDE or
width * height > MAX_PIXELS):
raise ValueError("dimension_limit")
if getattr(im, "n_frames", 1) != 1:
raise ValueError("unexpected_animation")
if expected_size is not None and im.size != expected_size:
raise ValueError("unexpected_dimensions")
with warnings.catch_warnings():
warnings.simplefilter("error", Image.DecompressionBombWarning)
with Image.open(io.BytesIO(data)) as im:
check_header(im)
im.verify()
with Image.open(io.BytesIO(data)) as im:
check_header(im)
im.load()
result = {
"format": im.format,
"width": im.width,
"height": im.height,
"mode": im.mode,
}
result["sha256"] = hashlib.sha256(data).hexdigest()
result["bytes"] = len(data)
return result
def expect_rejected(label, data, **kwargs):
try:
validate_image(data, **kwargs)
except (ValueError, OSError, SyntaxError,
Image.DecompressionBombWarning,
Image.DecompressionBombError):
print(label + ": rejected")
else:
raise AssertionError(label + " unexpectedly accepted")
buffer = io.BytesIO()
Image.new("RGB", (64, 64), "navy").save(buffer, format="PNG")
png = buffer.getvalue()
accepted = validate_image(png, expected_size=(64, 64))
assert accepted["format"] == "PNG"
assert accepted["width"] == accepted["height"] == 64
print("valid PNG: accepted")
expect_rejected("HTML body", b"<html>temporary error</html>")
expect_rejected("truncated PNG", png[:40])
expect_rejected("wrong size", png, expected_size=(128, 128))
expect_rejected("wrong format", png, allowed_formats={"JPEG"})
# Lower Pillow's warning threshold to test rejection with a tiny fixture.
previous_limit = Image.MAX_IMAGE_PIXELS
try:
Image.MAX_IMAGE_PIXELS = 3000
expect_rejected("pixel warning", png)
finally:
Image.MAX_IMAGE_PIXELS = previous_limit
The returned SHA-256 is a cryptographic hash identifying these exact bytes. Without a trusted expected hash, it does not prove that a download matches the provider's original artifact. It does help detect later storage changes and recognize repeated downloads. Image decoders may tolerate trailing data, so successful decoding does not certify a file as sanitized.
Do not enable Pillow's truncated-image loading option for this validation path. If your application needs normalized delivery files, explicitly transform and re-encode after validation, strip unnecessary metadata, and validate the resulting artifact again. Retain distinct hashes for the original and transformed bytes.
How should you download image URLs without introducing SSRF?
Fetch through a restricted downloader that validates destinations and enforces network-level restrictions. Server-side request forgery, or SSRF, occurs when an attacker causes your server to request unintended destinations, including internal services and cloud metadata endpoints.
Prefer an exact allowlist of documented output hosts where feasible. Require HTTPS, reject embedded credentials and unexpected ports, and parse URLs with a standard URL parser. Do not use substring matching for host approval: an attacker-controlled hostname can contain a trusted-looking string.
Resolve and validate every destination address, rejecting loopback, private, link-local, reserved, and other nonpublic addresses for both IPv4 and IPv6. DNS rebinding means that a hostname's address changes between validation and connection. Prevent this gap with a downloader that connects to the validated address while retaining hostname-based TLS verification, or a controlled outbound proxy with equivalent enforcement.
Disable redirects unless required. If redirects are permitted, apply the same destination checks to every hop and limit the hop count. Never forward application authorization headers to an output host. Treat signed URLs as secrets and redact their query strings from logs.
Stream the response with connect, read, and overall deadlines. Enforce a running byte limit even when Content-Length is absent or misleading. Account for HTTP content decompression as well as image decompression, and restrict outbound network access independently of application checks. The local example above deliberately does not implement this network boundary.
How do you distinguish a valid image from an acceptable result?
Use a separate acceptance policy because successful decoding says nothing about prompt adherence or usefulness. A structurally valid image can contain misspelled packaging text, incorrect product details, an unwanted transparent background, or inappropriate content.
Define measurable checks for your product. Optical character recognition, or OCR, converts visible text into machine-readable text; use it to compare required wording, with human review for uncertain matches. Check aspect ratio with a stated tolerance when exact dimensions are not required. Inspect alpha-channel behavior rather than assuming that an RGBA mode necessarily contains transparent pixels.
Document orientation and color policy too. Embedded orientation metadata can affect displayed dimensions; embedded color profiles can affect appearance. Decide whether your contract uses stored pixel dimensions or orientation-normalized dimensions, and apply the same convention in previews and delivery.
Store acceptance decisions with a policy version. Automated visual scoring should support a defined decision process, not silently replace required review. Preserve the distinction between a technical rejection and a subjective rejection in dashboards and customer support records.
When should validation trigger another generation request?
Regenerate after diagnosing a generation-level failure, not automatically after every download or decoder error. An expired output URL, interrupted download, or storage write failure may be recoverable without paying for another image, depending on documented provider capabilities.
Use bounded retries for transient retrieval failures and stop repeating downloads of the same consistently invalid bytes. Consult your request deduplication strategy before retrying generation so one logical task does not become several unintended paid requests. Log the request identifier, artifact hash, validation stage, rejection reason, and retry count without exposing sensitive prompts or URLs.
As of November 1, 2026, listed GenRelay image prices are Nano Banana Pro 1K/2K at $0.030 and 4K at $0.042; Nano Banana 2 1K at $0.020 and 4K at $0.036; and GPT-image-2 at $0.014 per image. These unit prices are not acceptance guarantees. Track cost per approved image using actual generation charges divided by accepted outputs, and report validation infrastructure costs separately when relevant.
What questions come up when implementing output validation?
The recurring questions concern headers, decoding, hashes, and review boundaries; each needs a separate answer.
Is checking Content-Type enough?
No. Check decoded format and force decoding. A plausible header can accompany an error body or a different image format.
Does Pillow verify() fully decode an image?
No. Reopen the input and call load() after verify(). Apply resource limits before decoding and isolate the worker because malformed content can still stress parsers.
Can a hash detect a visually duplicated image?
An exact-byte hash detects identical files, not equivalent pictures with different encoding or metadata. Perceptual hashing estimates visual similarity but requires calibrated thresholds and does not establish visual acceptance.
Should every rejected image be regenerated?
No. Separate retrieval failures, structural failures, and visual rejections. Retry the failed stage when safe, cap attempts, and require the appropriate review before spending on another generation.