Email header image 600x200: render 1920x640 at exactly 3:1

A 600x200 email header is 3:1, the widest shape GPT Image 2.5 accepts. Render 1920x640, then export 1200x400 for retina and 600x200 for 1x with Pillow.

5 min readSume
All posts

A 600x200 email header is exactly 3:1, and 3:1 is the widest shape GPT Image 2.5 allows, so you can render it without a crop. Ask for 1920x640 and export two files: 1200x400 for high-density screens and 600x200 for the 1x slot.

This shape is right on the limit. The OpenAI guide puts custom aspect ratios between 1:3 and 3:1, and Sume's catalog rejects anything past it on this model, so 3:1 passes and 3.1:1 would not.

Checks

Divide each edge by 16 and the sizes come out whole numbers, 120 and 40. The area is 1,228,800 pixels, well inside the range.

1920x640 against the GPT Image 2.5 custom-size rules (read 2026-10-05)
CheckValueResult
Both edges multiples of 161920 = 16 x 120, 640 = 16 x 40pass
Longest edge at most 38401920pass
Aspect ratio at most 3:13.000:1pass
Pixels 655,360 to 8,294,4001,228,800pass

One render, two exports

The script renders once and writes both sizes. Because the shapes match, ImageOps.fit does not crop; it only scales.

import os, requests
H = {"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"}

def generate(body):
    r = requests.post("https://api.sume.com/v1/images", headers=H, json=body, timeout=60)
    if r.status_code != 200:  # 202 = still running, read data.status_url
        raise SystemExit(f"{r.status_code}: {r.text[:300]}")
    return r.json()["data"][0]["url"]
from io import BytesIO
from PIL import Image, ImageOps

url = generate({
    "model": "openai/gpt-image-2.5",
    "prompt": "Soft watercolor header: autumn leaves along the bottom edge, pale cream sky, empty space in the middle for a title",
    "image_size": {"width": 1920, "height": 640},
    "quality": "medium",
})
master = Image.open(BytesIO(requests.get(url, timeout=60).content)).convert("RGB")
for name, size in {"header@2x.png": (1200, 400), "header.png": (600, 200)}.items():
    ImageOps.fit(master, size, Image.LANCZOS).save(name)

Price

Output-only figures from the Flare token rate are below. Sume bills list times 1.25. A header is simple art, so medium is often enough, and the Sume default of high costs roughly four times as much at this size.

Output-only price per 1920x640 image (read 2026-10-05)
QualityProvider listSume at list x 1.25
low$0.0019$0.0024
medium$0.0046$0.0058
high$0.0186$0.0232

Design for the small file

A 600-pixel header is read at a glance in an inbox preview. Use big shapes, one focal point and a quiet center, then put the title in as live text in your email template, not as pixels in the image. That keeps it readable if images are blocked and lets you change the wording without paying for a new render.

Draft cheap, finish once

A display set usually needs several concepts before one is approved. Request the first round at quality: "low", pick the layout, then repeat only the winner at high. Quality is a per-request field on both ChatGPT Image 2.5 variants (openai/gpt-image-2.5 and openai/gpt-image-2.5-sunburst), and the accepted values are auto, low, medium, high, xhigh and max. Avoid auto: Sume reserves the max price for it, so set the tier yourself.

Flare is the faster variant and Sunburst is tuned for edit precision, according to the fal pages. Both share the same token rates, so for an ad pass you can pick on behavior, not on price. Auto routing on Sume (sume/auto) uses Flare.

If the call returns 202 instead of 200

POST /v1/images waits up to 30 seconds and answers 200 with the images when the job finishes in time. When it does not, the route answers 202 with the standard job envelope, and the images come from GET /v1/jobs/{id}/result. Slow settings such as 4K, high quality and large n are the likeliest to fall back. A 1-2 megapixel ad master at high quality usually stays in the wait budget, but the code should branch on the status code, not the body shape.

For a batch of ad sizes, send each request with mode: "async" and read the results afterwards, or add a webhook_url with mode: "webhook". A failed synchronous job returns 502 with an error code and a next_action, and Sume does not bill failed generations.

Sources

Related posts

More in Use cases

All Use cases posts

Written by Sume