Twilio MMS image limit is 5 MB: compress a Sume image to fit
Twilio's MMS media limit is 5 MB, with image/jpeg, png, gif, heic, heif, tiff and bmp accepted. A 4K PNG may blow past it; recompress to JPEG before sending.

Twilio's accepted-content-types page gives 5 MB as the MMS maximum size, and lists the image MIME types it accepts: image/jpeg, image/jpg, image/gif, image/png, image/heic, image/heif, image/tiff and image/bmp. WebP is not on that list, so a WebP from an image API needs converting. The page adds that carrier-specific limits apply to non-image files, which is a separate article.
What is on the list
The practical consequence is that the Sume output_format choice matters. Of the four formats the Image API names, PNG and JPEG are accepted by Twilio, while WebP and SVG are not.
| Sume output_format | MIME type | Accepted by Twilio MMS |
|---|---|---|
| png | image/png | Yes |
| jpeg | image/jpeg | Yes |
| webp | image/webp | Not on the list |
| svg | image/svg+xml | Not on the list |
Weight rather than pixels
A 4K PNG of a photographic scene can exceed 5 MB, while the same image as a JPEG at quality 85 is a fraction of that. The Image API gives resolution tiers of 512, 1K, 2K and 4K; a 1K or 2K image is enough for a phone screen, if the model's catalog entry lists those tiers. output_compression exists in the schema but returns 400 unsupported_parameter in v1, so compress locally.
Compress until it fits
The script downloads a JPEG, then lowers quality until the file is under 5 MB. It uses 4,900,000 bytes as the ceiling to leave headroom, since the page says 5 MB without saying whether that means 5,000,000 or 5,242,880 bytes.
import os, io, requests
from PIL import Image
r = requests.post(
"https://api.sume.com/v1/images",
headers={"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"},
json={"model": "bytedance-seed/seedream-4.5", "prompt": "festive window display with warm lights, photographic", "output_format": "jpeg"},
timeout=90,
)
r.raise_for_status()
if r.status_code != 200:
raise SystemExit("202: read the finished job from /v1/jobs/{id}/result")
img = Image.open(io.BytesIO(requests.get(r.json()["data"][0]["url"], timeout=60).content))
for q in (90, 80, 70, 60, 50):
img.convert("RGB").save("mms.jpg", "JPEG", quality=q, optimize=True)
if os.path.getsize("mms.jpg") <= 4_900_000:
break
else:
raise SystemExit("still over the MMS limit; pick a smaller resolution tier")
print(os.path.getsize("mms.jpg"), "bytes at quality", q)Why JPEG is the safe default
For photographic content JPEG is both on Twilio's accepted list and much smaller than PNG at the same visual quality, so ask the Image API for jpeg directly, which the docs list in output_format. PNG makes sense for flat graphics, logos and screenshots where sharp edges matter, and those are usually far below 5 MB anyway. If you need to send the same image to several channels, keep a master and derive each output, because each platform has its own list. Twilio's page also names HEIC, HEIF, TIFF and BMP, but there is no reason to produce those from a Sume image; JPEG and PNG are the formats the Image API gives you. Treat the 5 MB ceiling as the outer limit and the carrier limits mentioned on the page as the reason to aim lower. Recheck the page before a campaign, since limits change.
Sending
Host the final JPEG on your own storage rather than passing the Sume URL along. The Image API docs describe those URLs as signed, and they are not documented as permanent. Whether a given carrier delivers a 4.9 MB image is a separate question the page flags; many senders aim far lower.
Log the final byte count and quality next to the message record, so a delivery problem can be matched to the file that was sent.
Sources
Related posts
More in Developers
- Two workers, one order: Idempotency-Key from order id and version
Two queue workers pick up the same order and both submit to Sume. Build the key from order id plus version so duplicates collapse and edits still create a run.
- Undo for AI image edits: keep a version chain of every saved result
Generative edits have no undo button. Download each result, hash it, record its parent and prompt, and walk the chain back. Python, no database.
- Unit test a Sume 429 retry loop in Python with a fake opener
Test a Sume rate_limited retry loop with unittest and a fake opener: retry-after is honored, the last try raises, no network, no spend. Stdlib only.
- unsupported_media_type: video_url served as text/html or an image
Sume video trim and filter HEAD the source and refuse a declared non-video content type. What is checked, why octet-stream passes, and a runnable check.
Written by Sume