Slack custom emoji under 128 KB: shrink a Sume image to fit
Slack says custom emoji should be square, under 128 KB, JPG, PNG or GIF. Generate a 1:1 mark on Sume, scale it small, and loop a PNG palette down to fit.

Slack's help page says custom emoji images should be square and under 128 KB, and it accepts JPG, PNG or GIF; transparent backgrounds work best. The page gives no pixel dimensions, so pick a small square, such as 128 x 128, and let the 128 KB byte limit decide whether it passes. A 1:1 request on Sume plus a Pillow resize does it.
What the page states
Animated GIFs can have up to 50 frames. For a still, the two checks are shape and weight.
| Rule | Value |
|---|---|
| Shape | Square |
| File size | Under 128 KB |
| File types | JPG, PNG or GIF |
| Background | Transparent works best |
| GIF frames | Up to 50 |
| Pixel dimensions | Not specified on the page |
Getting a transparent emoji from Sume
Use ChatGPT Image 2.5 with background: "transparent" if you want a cut-out; the Image API docs list that parameter for that model, and Image 1.0 with transparency: true is the other documented route. Keep the prompt to one bold object with no text. Fine detail turns to mush at 128 pixels, and Slack shows emoji far smaller than that in messages.
Scale, quantize, check
A PNG quantized to a smaller palette is much lighter than a 32-bit one. The script resizes to 128 x 128, then tries palettes of 256, 128, 64 and 32 colors until the file is under 128 KB.
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": "openai/gpt-image-2.5", "aspect_ratio": "1:1", "prompt": "one cheerful cartoon star, flat colors, thick outline, no text", "background": "transparent", "output_format": "png"},
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))
art = img.convert("RGBA").resize((128, 128), Image.LANCZOS)
for colors in (256, 128, 64, 32):
art.quantize(colors=colors, method=Image.Quantize.FASTOCTREE).save("emoji.png", optimize=True)
if os.path.getsize("emoji.png") < 128 * 1024:
break
print(os.path.getsize("emoji.png"), "bytes with", colors, "colors")What makes a good emoji image
Emoji are viewed at a tiny size next to text, so the image has to survive reduction. One subject, a thick outline, flat color and a transparent background hold up best, which matches Slack's note that transparent backgrounds work best. Avoid gradients and fine texture; they raise the file size and add nothing at that scale. Slack gives no pixel size on the page, so 128 x 128 is a choice, not a rule; if a file is under the limit at that size you can try a larger one, but there is little benefit. Name emoji by what they show. Because the page names the 128 KB budget and the types JPG, PNG and GIF, avoid WebP, which Slack does not list. If a generation has a messy edge or leftover background pixels, regenerate it with a plainer prompt instead of cleaning it by hand.
Cost of a set
Emoji sets are many small images, so the cost is n separate requests, each billed by usage.cost. The catalog also lets one call ask for several images with n, up to 10, though each model has its own lower ceiling in the catalog. Read the model's n range before relying on it. For a longer walk-through of the workflow, see how to make a Slack emoji with AI.
The quantize loop prints how many colors it needed. If even 32 colors do not fit, the image is too detailed, and simplifying the prompt is the better fix.
Sources
Related posts
More in Developers
- Social search returned 202 after 30 s: follow the job, don't resend
Sume's social read routes wait up to 30 seconds, then answer 202 with a job. Poll that job; resending the body runs another upstream read.
- Sora video ids in your database after the shutdown: what to keep
OpenAI's Videos API shut down 2026-09-24. Old video_ids no longer resolve, so store your own file URL and the model used. Schema fields included.
- Keep a source log for Shorts cut from long video: CSV from video trim
A source log shows which long video, timestamp and edit produced each Short. Build one from video trim results using actual_start_seconds.
- source_too_large on trim or filter: the 300 MiB source cap
Sume's video trim and filter refuse a hosted source over 300 MiB (314,572,800 bytes) at submit. What is checked, the free check call, and ways around it.
Written by Sume