Quick reference
| Limit / capability | Value |
|---|---|
| Character limit | 300 graphemes |
| Images per post | 4 |
| Image formats | jpeg, png, webp, gif |
| Image size | 1,000,000 bytes (~976 KB) |
| Video formats | mp4 |
| Video size | 100,000,000 bytes (100 MB) |
| Video duration | platform-enforced (no client-side cap) |
| Mixed image + video | Rejected — pick one per post |
| Alt text per item | 2000 graphemes |
| Post types supported | text, single image, multi-image, video |
| Scheduling | Yes (via scheduledAt) |
| Reply / threads | Yes — reply to any post via options.replyToUri + replyToCid |
| First comment | Yes (firstComment.text) |
| Inbox / DM | Not supported in v1 |
| Analytics | Not supported in v1 (AT Proto exposes no impressions data) |
Connect an account
Generate an app password at bsky.app/settings/app-passwords. The format is 19 characters with hyphens (e.g.abcd-efgh-ijkl-mnop). Submit it via the dashboard’s Accounts page or programmatically:
connect.sh
complete response carries the id you’ll send posts against. App passwords can be revoked at any time on bsky.app — when that happens the connection breaks cleanly with platform_auth_failed.
App passwords don’t have OAuth scopes; they grant the same permissions as the password itself. AT Protocol JWTs are short-lived (minutes); the publisher refreshes on every 401 so you don’t manage tokens. Subscribe to token.revoked for upstream revocations.
Post types
Text post
text.json
Single image
single-image.json
Multi-image
Up to four images per post. Mixing images and video on the same post is rejected by preflight (bluesky.media.image_video_exclusive).
multi-image.json
Video
video.json
app.bsky.video.uploadVideo, probe getUploadLimits so quota exhaustion fails loudly with bluesky.video.quota_exhausted, stream bytes to uploadVideo, poll getJobStatus until COMPLETED, then embed the blob ref as app.bsky.embed.video. Videos do not go through com.atproto.repo.uploadBlob — that’s the most common reason naive integrations break.
First comment
first-comment.json
warnings[] entry with code: "first_comment_failed" instead of failing the whole call.
Reply (threading)
Thread a post under any existing Bluesky post by passing the parent’s strong ref — itsuri and cid — in the target’s options. Both come straight from the parent post’s publish response.
reply.json
replyToUri and replyToCid must be sent together. To build a multi-post thread: publish post 1, read its uri + cid from the response, and pass them as post 2’s replyToUri/replyToCid. For replies deeper than the first, also pin the thread’s original post with replyRootUri + replyRootCid so every reply stays anchored to the root — omit the root and it defaults to the parent (correct for a reply to a top-level post).
Wisdom (platform-specific things that bite)
Common errors
| Error rule | What it means | How to fix |
|---|---|---|
bluesky.text.max_graphemes | Post exceeded the 300-grapheme cap | Trim to ≤300 graphemes. |
bluesky.text.non_empty | Text field was empty or whitespace | Provide non-whitespace text — Bluesky has no media-only post shape. |
bluesky.media.image_video_exclusive | Attached both images and a video | Split into two posts; AT Proto records hold one media kind per record. |
bluesky.media.count_max | More than 4 images or more than 1 video | Reduce to 4 images max, or 1 video. |
bluesky.media.image_size_max | Image exceeded 976 KB | Re-encode under 1,000,000 bytes. |
bluesky.media.mime_allowed | Unsupported image/video mime | Images: jpeg/png/webp/gif. Videos: mp4. |
bluesky.video.quota_exhausted | Daily video upload quota hit | Wait for the quota window to roll over; letmepost reads the live limit from getUploadLimits so the check is current. |
bluesky.video.job_failed | Bluesky’s transcoder rejected the clip | Inspect platformResponse for the upstream reason — usually codec or duration. |
What you can’t do (yet)
- Starter packs, custom feeds, pin-to-profile.
- Direct messages and DM attachments (AT Proto chat lives on a separate XRPC surface not exposed in v1).
- Analytics — AT Proto records carry no impression count by design.
- Polls, quote posts, and labeler interactions.
- Editing or backdating a published record.
API reference
POST /v1/posts— primary publish.POST /v1/media— upload images/videos formediaIdreferences.POST /v1/accounts/connect/bluesky/complete— finish a Bluesky connect.

