Bluesky
Publishing to Bluesky via the Octopost API
Bluesky
Bluesky supports text posts, images, and threads. Octopost handles session management, grapheme counting, facet generation for clickable links and mentions, image blob uploads, and reply chain threading automatically -- you just send us your content.
Content Limits
| Type | Limit |
|---|---|
| Text length | 300 graphemes (not characters -- emojis and non-Latin scripts count differently) |
| Images per post | 4 |
| Image file size | 1 MB per image |
| Image formats | JPEG, PNG |
| Video | Not supported |
| Polls | Not supported |
Good to Know
- Graphemes, not characters. Bluesky measures text length in graphemes. A single emoji counts as 1 grapheme even if it is multiple bytes or code points.
"Hello".lengthin JavaScript gives you code units, not graphemes. Octopost counts graphemes for you and rejects posts that exceed 300 before they hit Bluesky's servers. - 1 MB image limit is small. This is tighter than most platforms. If your images are large, resize them before sending. Octopost will report a clear error if an image exceeds the limit.
- Alt text matters. Bluesky supports alt text on images and the community values it. Include it when you can.
- Links and @mentions must have facets to be clickable. Plain URLs and @handles in Bluesky post text are rendered as plain text unless accompanied by byte-indexed facet metadata. Octopost generates these facets automatically from your plain text -- just write naturally and include URLs and @handles as you normally would.
Threads
Send a thread by including an array of content items. Octopost publishes them in order, wiring up the reply references so they appear as a connected thread on Bluesky.
{
"platforms": ["bluesky"],
"thread": [
{ "content": "First post in the thread." },
{ "content": "Second post, continuing the thought." },
{ "content": "Final post. https://octopost.ink" }
]
}Octopost manages the reply chain -- each post is linked to the previous one using the correct root and parent references.
Rate Limits
Bluesky does not publish official rate limits. Octopost uses conservative pacing (1 request per second) and tracks responses to back off automatically if the server signals throttling.
Error Reporting
| What happened | What Octopost reports |
|---|---|
| Text exceeds 300 graphemes | Rejected before sending, with the grapheme count |
| Image exceeds 1 MB | Rejected before sending, with the file size |
| Invalid or revoked App Password | auth_failed -- user needs to generate a new App Password in Bluesky settings |
| Session expired mid-publish | Octopost refreshes the session automatically and retries. You will not see this error unless the refresh JWT is also expired, in which case: auth_failed |
| Facet position error | Octopost generates facets for you, so this should not occur. If it does, the post may be created but links or mentions will appear as plain text |
Things Octopost Handles for You
You do not need to worry about any of the following, but in case you are curious:
- Authentication via App Passwords. Bluesky does not use OAuth. Users provide their handle and an App Password (generated in Bluesky settings). Octopost creates and manages the AT Protocol session, storing the DID, access JWT, and refresh JWT securely.
- Session refresh. Access JWTs expire frequently. Octopost detects expiration and refreshes the session using the refresh JWT before publishing, transparently.
- Grapheme counting. Octopost uses a grapheme-aware counter to validate text length accurately before sending.
- Facet generation. Octopost scans your text for URLs and @mentions, resolves mentioned users' DIDs, calculates UTF-8 byte offsets, and builds the facet array that Bluesky requires for clickable links, mentions, and hashtags.
- Image blob upload. Bluesky requires images to be uploaded as blobs via the AT Protocol before they can be attached to a post. Octopost uploads each image, receives the blob reference, and constructs the embed object.
- Reply chain management for threads. Each post in a thread must reference both the root post and its immediate parent via their AT URIs and CIDs. Octopost tracks these across the thread and sets them correctly.
Example
{
"content": "Hello from Octopost! Check out https://octopost.ink @jake.bsky.social",
"platforms": ["bluesky"],
"media": {
"images": [
{ "url": "https://example.com/photo.jpg", "alt": "A screenshot of the dashboard" }
]
}
}That's it. Octopost takes care of the rest.