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.

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.
| Check | Value | Result |
|---|---|---|
| Both edges multiples of 16 | 720 = 16 x 45, 1440 = 16 x 90 | pass |
| Longest edge at most 3840 | 1440 | pass |
| Aspect ratio at most 3:1 | 2.000:1 | pass |
| Pixels 655,360 to 8,294,400 | 1,036,800 | pass |
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.
| Quality | Provider list | Sume 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
- Handheld selfie UGC prompt for Seedance 2.5: a template and cost
A five-line prompt template for handheld, selfie-style UGC clips on Seedance 2.5, with 9:16 settings and the Sume price at 480p, 720p and 1080p.
- Hardware Store DIY Tip Video With an AI Avatar: Cost per Clip
Weekly DIY tips from a hardware store host avatar: a short scene script, seconds per scene, request JSON, and Sume cost per clip on each quality tier.
- Hinge video prompt: 30 seconds, in-app recording, and Sume's role
Hinge says video prompts are up to 30 seconds, recorded inside the app, so a rendered file is not the route. Where Sume helps: rehearsal timing.
- Hip-hop beat for a product ad: instrumental brief and cost
Brief an instrumental hip-hop beat for a product ad with BPM, drums, bass, and sample feel. One take is $0.125 on Sume; mix it under a voiceover with duck_db.
Written by Sume