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.
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.
| Check | 2550x3300 direct | 2400x3104 render |
|---|---|---|
| Both edges multiples of 16 | no | yes (150 x 16, 194 x 16) |
| Longest edge at most 3840 | 3300, pass | 3104, pass |
| Aspect ratio at most 3:1 | 1.294:1, pass | 1.293:1, pass |
| Pixels 655,360 to 8,294,400 | 8,415,000, fail | 7,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.
| Quality | Provider list | Sume 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
- Video API shutdown runbook: six steps from the Sora removal
A six-step runbook for the day a video vendor removes its API: find calls, freeze ids, pick a replacement, port, canary, and archive. Dates from OpenAI's page.
- Vet Clinic Vaccine Reminder as an AI Avatar Video: Script, Cost
Script a vet clinic vaccine reminder for an avatar video on Sume: three scenes, seconds math, request JSON, and cost on standard, plus and max.
- Vimeo's AI label vanishes in the embed when the title is hidden
Vimeo shows the AI label under the title and in the embedded player, but not when the title is hidden. What to do on a landing page that embeds an AI clip.
- Vimeo labels its own AI tools' output itself: what you control
Vimeo may apply an AI label you cannot remove when its own tools made the video. What that means for a clip you generate elsewhere, such as on Sume.
Written by Sume