US Letter flyer at 300 dpi: 2550x3300 exceeds the GPT Image 2.5 cap

US Letter at 300 dpi is 2550x3300, 8,415,000 pixels, over the 8,294,400 cap. Render 2400x3104 with GPT Image 2.5, then fit it to 2550x3300 in Pillow.

5 min readSume
All posts

A US Letter flyer at 300 dpi is 2550x3300 pixels, which is 8,415,000 pixels: 120,600 over the 8,294,400 cap in the OpenAI guide. Request 2400x3104 from GPT Image 2.5 instead and let Pillow fit it to 2550x3300.

Letter is 8.5 x 11 inches, so at 300 dpi the pixel size is 2550 x 3300. The two boxes differ by less than a tenth of a percent in shape, so the crop is a single pixel column.

Finding a legal box

A box has to pass four rules at once. 2550 is not a multiple of 16 (2550 / 16 = 159.375), and 3300 is not either (206.25). Stepping down to 2400 and 3104 gives clean multiples (150 and 194) and keeps the area at 7,449,600 pixels, comfortably under the cap.

US Letter at 300 dpi against the GPT Image 2.5 custom-size rules (read 2026-10-05)
Check2550x3300 direct2400x3104 render
Both edges multiples of 16noyes (150 x 16, 194 x 16)
Longest edge at most 38403300, pass3104, pass
Aspect ratio at most 3:11.294:1, pass1.293:1, pass
Pixels 655,360 to 8,294,4008,415,000, fail7,449,600, pass

Request

Use image_size with width and height. It has priority over aspect_ratio on Sume, so do not send both. The resize multiplies each edge by about 1.0625 and writes a 300 dpi tag into the file.

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": "Bright bake-sale flyer background, a table of cupcakes seen from above, big empty banner space at the top and bottom",
    "image_size": {"width": 2400, "height": 3104},
    "quality": "high",
})
img = Image.open(BytesIO(requests.get(url, timeout=60).content)).convert("RGB")
img = ImageOps.fit(img, (2550, 3300), Image.LANCZOS)
img.save("flyer-letter.png", dpi=(300, 300))

What it costs

The table gives output-only prices from the Flare token rate, with Sume's list times 1.25 next to it. Prompt tokens add a little on top.

Output-only price per 2400x3104 image (read 2026-10-05)
QualityProvider listSume at list x 1.25
low$0.0136$0.017
medium$0.0323$0.0404
high$0.1259$0.1573

Keep copy and layout apart

A flyer lives or dies on its words. Generate the art with a reserved headline band and an empty footer, then add the event name, time, address and phone number with a font in your layout step. That way a typo costs a re-run of your template, not a re-run of the image.

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