Stripe branding icon: JPG or PNG under 512 KB, 128 px up

Stripe wants a JPG or PNG icon under 512kb and at least 128 x 128 px. Make a 1:1 mark on Sume, shrink it in Pillow, and know where the icon shows up.

5 min readSume
All posts

Stripe's branding page says icons and logos must be JPG or PNG, less than 512kb, and 128px by 128px or larger. The icon is the square mark; the logo is a non-square override. A 1:1 image from Sume resized to 512 x 512 clears every number on that list with room to spare, as long as you check the file size after saving.

What the icon and logo are used for

The same page has a table of where each setting appears. The practical consequence: if you upload only an icon, it is the one asset that reaches emails, checkout, the customer portal, hosted invoice pages and invoice PDFs. The logo is optional and only shows on Checkout, Payment Links and invoice PDFs.

Where Stripe branding settings apply (read 2026-10-03)
SettingEmailsCheckout and Payment LinksCustomer portalHosted invoice pageInvoice PDFs
IconYesYesYesYesYes
LogoNoYesNoNoYes
Brand colorYesNoYesYesYes
Accent colorYes (background color)YesYesYesNo

Generating the square mark on Sume

Use a square ratio and leave lettering out of the prompt. Image models draw marks well and text unreliably, so add a wordmark yourself afterwards. The Image API docs show Seedream 4.5 accepting aspect_ratio: "1:1", which is what the script uses; GET /v1/images/models lists every other model that takes 1:1.

POST /v1/images answers 200 with data[].url when it finishes inside its 30-second budget and 202 with a job envelope when it does not. The script stops on 202 so you can read the job result instead of guessing at the body.

Resize and check the size

Stripe's limit is stated as 512kb, and the page does not say whether that is 512,000 or 524,288 bytes. The script treats 500,000 bytes as the ceiling so either reading passes. JPEG quality steps down until the file fits; a flat-color mark usually fits on the first try.

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", "aspect_ratio": "1:1", "prompt": "simple geometric monogram mark on a plain background, flat colors, no text"},
    timeout=60,
)
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))
img = img.convert("RGB").resize((512, 512), Image.LANCZOS)
for q in (92, 85, 75, 65):
    img.save("stripe-icon.jpg", "JPEG", quality=q)
    if os.path.getsize("stripe-icon.jpg") < 500_000:
        break
else:
    raise SystemExit("icon is still over the size limit")
print(os.path.getsize("stripe-icon.jpg"), "bytes")

Order of operations

Resize first, then compress, and check the size last. If you also need the optional non-square logo, make it a separate generation, since a square mark cropped into a wide shape usually looks wrong.

Notes

  • Stripe does not state a maximum pixel size on this page. 512 x 512 is a choice that keeps the file small while staying above 128 px.
  • Branding is account-wide and set in the Dashboard. The page notes that Connect platforms and non-Connect users cannot configure their own branding through the Accounts API.
  • A single image is billed at the amount shown in the usage.cost field of the response. Failed generations are not billed.
  • If the mark needs transparent corners, request a PNG from a model that supports background: transparent; the docs name ChatGPT Image 2.5 for that.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume