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.
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.
| Field | Required | Type | Limit |
|---|---|---|---|
| uri | yes | string, uri format | none stated |
| title | yes | string | none stated |
| description | yes | string | none stated |
| thumb | no | blob, 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
- Browser voice app that starts Sume jobs: keep the key on your server
Voice apps run in the browser over WebRTC, but Sume keys belong on a server. A route handler that holds the key, allowlists models, reuses idempotency keys.
- Sume bulk queue 404 format_run_queue_not_found: three causes
A Sume bulk queue poll returned 404 format_run_queue_not_found. The id is wrong or the queue is another owner's. How to tell which, and what to do next.
- Bulk run 409 idempotency_conflict: find the first queue by queue_id
A Sume bulk create that reuses a key with a different payload returns 409 with details.queue_id. How to read the original queue and decide what to resend.
- Burn text onto a silent video: fixing caption_no_speech
A silent clip fails Sume's speech-to-captions path with caption_no_speech. Pass authored cues with text, start and end to burn overlay copy with no ASR.
Written by Sume