300x600 half-page ad image: render 720x1440, keep a 600x1200 copy

A 300x600 half-page ad is 1:2. GPT Image 2.5 can render 720x1440 in one call; scale by 2.4 for 1x or by 1.2 for a 600x1200 retina file. Checks and prices.

5 min readSume
All posts

For a 300x600 half-page ad image, ask GPT Image 2.5 for 720x1440 and downscale: divide by 2.4 for the 300x600 file, or by 1.2 for a 600x1200 file that serves high-density screens. 720x1440 keeps the 1:2 shape exactly and passes every custom-size rule.

A direct 300x600 request would fail. It is 180,000 pixels, well under the 655,360 floor the OpenAI guide gives for custom sizes, and 300 is not a multiple of 16.

The four checks

The rules are the same ones the Sume catalog enforces for ChatGPT Image 2.5: both edges multiples of 16, longest edge 3840, ratio at most 3:1, and 655,360 to 8,294,400 pixels. A 1:2 ratio is far inside the 3:1 limit, so tall ad shapes are easy. The tight cases are narrower strips, which the sibling posts on 160x600 and 970x250 cover.

720x1440 against the GPT Image 2.5 custom-size rules (read 2026-10-05)
CheckValueResult
Both edges multiples of 16720 = 16 x 45, 1440 = 16 x 90pass
Longest edge at most 38401440pass
Aspect ratio at most 3:12.000:1pass
Pixels 655,360 to 8,294,4001,036,800pass

Code

The script renders once and writes the 1x file. For the 2x file, change the fit size to (600, 1200). ImageOps.fit crops only when the shapes differ, and here they do not.

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": "Tall vertical ad background: a ceramic coffee mug on a wooden table, soft morning light, open space at the top",
    "image_size": {"width": 720, "height": 1440},
    "quality": "high",
})
img = Image.open(BytesIO(requests.get(url, timeout=60).content)).convert("RGB")
ImageOps.fit(img, (300, 600), Image.LANCZOS).save("ad-300x600.png")

Price of one render

Using the output-token formula behind the fal Flare price page, a 720x1440 image costs the amounts below at list. Sume bills list times 1.25. The figures leave out input tokens for the prompt, which add well under a cent for a short one.

Output-only price per 720x1440 image (read 2026-10-05)
QualityProvider listSume at list x 1.25
low$0.0029$0.0037
medium$0.0066$0.0082
high$0.0262$0.0328

Layout tip for a tall unit

Tall units read top to bottom: logo zone, hero, offer, button. Ask the model for the hero only, and say where the empty zones sit. Then draw the logo, price and button in code on the resized file so the text is exact.

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