Bluesky link card thumbnail: 1,000,000 bytes, any image type

The thumb on a Bluesky link card is optional, accepts image/* and is capped at 1,000,000 bytes. Make a 1280x720 preview with Sume and shrink it under the cap.

4 min readSume
All posts

The preview picture on a Bluesky link card is the optional thumb field of app.bsky.embed.external. It is a blob that accepts any image type (image/*) up to 1,000,000 bytes. A card without a thumb is still valid, because only uri, title and description are required.

What the record requires

Read today from Bluesky's lexicon file for the external embed. The file does not give pixel dimensions, and it does not say how an app crops the card, so those are left out here.

Bluesky external embed fields (read 2026-10-04)
FieldRequiredTypeLimit
uriyesstring, uri formatnone stated
titleyesstringnone stated
descriptionyesstringnone stated
thumbnoblob, image/*maxSize 1000000

Make the preview image

A 16:9 card image at 1280x720 meets Sume's GPT custom-size rules: both edges are multiples of 16 (80 and 45 times 16), the aspect ratio is under 3:1, and 921,600 pixels sits inside the 655,360 to 8,294,400 window.

Request it with image_size 1280x720 and output_format jpeg on POST /v1/images using model openai/gpt-image-2.5, as in the images docs. If the call returns 202 instead of 200, poll the job and read the result.

Shrink it to fit

Sume does not serve output_compression in v1, so shrink the file locally. This script tries lower JPEG quality first, then fewer pixels, and stops at the first file that fits.

import asyncio, io
from PIL import Image

LIMIT = 1_000_000

async def main():
    img = Image.open("card.png").convert("RGB")
    scale = 1.0
    while scale > 0.3:
        w, h = int(img.width * scale), int(img.height * scale)
        small = img.resize((w, h), Image.LANCZOS)
        for quality in (90, 80, 70, 60):
            buf = io.BytesIO()
            small.save(buf, "JPEG", quality=quality, optimize=True)
            if buf.tell() <= LIMIT:
                with open("thumb.jpg", "wb") as f:
                    f.write(buf.getvalue())
                print(w, h, quality, buf.tell())
                return
        scale -= 0.1
    raise SystemExit("could not get under the cap")

asyncio.run(main())

Checks before you post

  • Measure bytes with the same decimal rule the lexicon uses, 1000000.
  • Keep any text in the image large. A card thumbnail is small on a phone.
  • If you generate many cards, store the job id with each thumb so you can trace an image back to its prompt.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume