Bluesky API
Schedule and automate Bluesky posts with Zernio API - Text posts, images, videos, threads, and App Password authentication
Quick Reference
| Property | Value |
|---|---|
| Character limit | 300 (HARD LIMIT) |
| Images per post | 4 |
| Videos per post | 1 |
| Image formats | JPEG, PNG, WebP, GIF |
| Image max size | 1 MB (auto-compressed, strict) |
| Video format | MP4 only |
| Video max size | 50 MB |
| Video max duration | 60 seconds |
| Post types | Text, Image, Video, Thread |
| Scheduling | Yes |
| Inbox (DMs) | Yes (add-on, text only) |
| Inbox (Comments) | Yes (add-on) |
| Analytics | No |
Before You Start
Bluesky has a HARD 300 character limit. This is the #1 cause of failed posts -- 95% of all Bluesky failures are character limit exceeded. If you're cross-posting from ANY other platform (Twitter 280 is close but others are 500-63,000 chars), you MUST use customContent to provide a Bluesky-specific shorter version or your post WILL fail.
Bluesky's image limit is 1 MB per image -- much stricter than any other platform. Most phone photos are 3-5 MB. Zernio auto-compresses, but quality may degrade.
Additional requirements:
- Uses App Passwords, not OAuth (handle + app password from Bluesky Settings)
- 300 char limit includes everything (text, URLs, mentions)
- Each thread item is also limited to 300 characters
- Images are strictly 1 MB per image
Quick Start
Post to Bluesky in under 60 seconds:
const { post } = await zernio.posts.createPost({
content: 'Hello from Zernio API!',
platforms: [
{ platform: 'bluesky', accountId: 'YOUR_ACCOUNT_ID' }
],
publishNow: true
});
console.log('Posted to Bluesky!', post._id);result = client.posts.create(
content="Hello from Zernio API!",
platforms=[
{"platform": "bluesky", "accountId": "YOUR_ACCOUNT_ID"}
],
publish_now=True
)
post = result.post
print(f"Posted to Bluesky! {post['_id']}")curl -X POST https://zernio.com/api/v1/posts \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Hello from Zernio API!",
"platforms": [
{"platform": "bluesky", "accountId": "YOUR_ACCOUNT_ID"}
],
"publishNow": true
}'Content Types
Text Post
A simple text-only post. Keep it under 300 characters.
const { post } = await zernio.posts.createPost({
content: 'Just shipped a new feature!',
platforms: [
{ platform: 'bluesky', accountId: 'YOUR_ACCOUNT_ID' }
],
publishNow: true
});
console.log('Posted to Bluesky!', post._id);result = client.posts.create(
content="Just shipped a new feature!",
platforms=[
{"platform": "bluesky", "accountId": "YOUR_ACCOUNT_ID"}
],
publish_now=True
)
post = result.post
print(f"Posted to Bluesky! {post['_id']}")curl -X POST https://zernio.com/api/v1/posts \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Just shipped a new feature!",
"platforms": [
{"platform": "bluesky", "accountId": "YOUR_ACCOUNT_ID"}
],
"publishNow": true
}'Image Post
Attach up to 4 images per post. JPEG, PNG, WebP, and GIF formats are supported. Each image must be under 1 MB.
const { post } = await zernio.posts.createPost({
content: 'Check out this photo!',
mediaItems: [
{ type: 'image', url: 'https://cdn.example.com/photo.jpg' }
],
platforms: [
{ platform: 'bluesky', accountId: 'YOUR_ACCOUNT_ID' }
],
publishNow: true
});
console.log('Posted with image!', post._id);result = client.posts.create(
content="Check out this photo!",
media_items=[
{"type": "image", "url": "https://cdn.example.com/photo.jpg"}
],
platforms=[
{"platform": "bluesky", "accountId": "YOUR_ACCOUNT_ID"}
],
publish_now=True
)
post = result.post
print(f"Posted with image! {post['_id']}")curl -X POST https://zernio.com/api/v1/posts \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Check out this photo!",
"mediaItems": [
{"type": "image", "url": "https://cdn.example.com/photo.jpg"}
],
"platforms": [
{"platform": "bluesky", "accountId": "YOUR_ACCOUNT_ID"}
],
"publishNow": true
}'Multi-Image Post
Attach up to 4 images. Remember: each image must be under 1 MB.
const { post } = await zernio.posts.createPost({
content: 'Product launch gallery',
mediaItems: [
{ type: 'image', url: 'https://cdn.example.com/photo1.jpg' },
{ type: 'image', url: 'https://cdn.example.com/photo2.jpg' },
{ type: 'image', url: 'https://cdn.example.com/photo3.jpg' },
{ type: 'image', url: 'https://cdn.example.com/photo4.jpg' }
],
platforms: [
{ platform: 'bluesky', accountId: 'YOUR_ACCOUNT_ID' }
],
publishNow: true
});
console.log('Multi-image post created!', post._id);result = client.posts.create(
content="Product launch gallery",
media_items=[
{"type": "image", "url": "https://cdn.example.com/photo1.jpg"},
{"type": "image", "url": "https://cdn.example.com/photo2.jpg"},
{"type": "image", "url": "https://cdn.example.com/photo3.jpg"},
{"type": "image", "url": "https://cdn.example.com/photo4.jpg"}
],
platforms=[
{"platform": "bluesky", "accountId": "YOUR_ACCOUNT_ID"}
],
publish_now=True
)
post = result.post
print(f"Multi-image post created! {post['_id']}")curl -X POST https://zernio.com/api/v1/posts \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Product launch gallery",
"mediaItems": [
{"type": "image", "url": "https://cdn.example.com/photo1.jpg"},
{"type": "image", "url": "https://cdn.example.com/photo2.jpg"},
{"type": "image", "url": "https://cdn.example.com/photo3.jpg"},
{"type": "image", "url": "https://cdn.example.com/photo4.jpg"}
],
"platforms": [
{"platform": "bluesky", "accountId": "YOUR_ACCOUNT_ID"}
],
"publishNow": true
}'Video Post
Attach a single video per post. MP4 format only, up to 50 MB, max 60 seconds.
const { post } = await zernio.posts.createPost({
content: 'New product demo',
mediaItems: [
{ type: 'video', url: 'https://cdn.example.com/demo.mp4' }
],
platforms: [
{ platform: 'bluesky', accountId: 'YOUR_ACCOUNT_ID' }
],
publishNow: true
});
console.log('Video post created!', post._id);result = client.posts.create(
content="New product demo",
media_items=[
{"type": "video", "url": "https://cdn.example.com/demo.mp4"}
],
platforms=[
{"platform": "bluesky", "accountId": "YOUR_ACCOUNT_ID"}
],
publish_now=True
)
post = result.post
print(f"Video post created! {post['_id']}")curl -X POST https://zernio.com/api/v1/posts \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "New product demo",
"mediaItems": [
{"type": "video", "url": "https://cdn.example.com/demo.mp4"}
],
"platforms": [
{"platform": "bluesky", "accountId": "YOUR_ACCOUNT_ID"}
],
"publishNow": true
}'Thread
Create Bluesky threads with multiple connected posts using platformSpecificData.threadItems. Each item becomes a reply to the previous post and can have its own content and media. Each thread item is limited to 300 characters.
const { post } = await zernio.posts.createPost({
platforms: [{
platform: 'bluesky',
accountId: 'YOUR_ACCOUNT_ID',
platformSpecificData: {
threadItems: [
{
content: 'A thread about building APIs',
mediaItems: [{ type: 'image', url: 'https://cdn.example.com/api.jpg' }]
},
{ content: 'First, design your endpoints around resources, not actions.' },
{ content: 'Second, always version your API from day one.' },
{ content: 'Finally, document everything! Your future self will thank you.' }
]
}
}],
publishNow: true
});
console.log('Thread posted!', post._id);result = client.posts.create(
platforms=[{
"platform": "bluesky",
"accountId": "YOUR_ACCOUNT_ID",
"platformSpecificData": {
"threadItems": [
{
"content": "A thread about building APIs",
"mediaItems": [{"type": "image", "url": "https://cdn.example.com/api.jpg"}]
},
{"content": "First, design your endpoints around resources, not actions."},
{"content": "Second, always version your API from day one."},
{"content": "Finally, document everything! Your future self will thank you."}
]
}
}],
publish_now=True
)
post = result.post
print(f"Thread posted! {post['_id']}")curl -X POST https://zernio.com/api/v1/posts \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"platforms": [{
"platform": "bluesky",
"accountId": "YOUR_ACCOUNT_ID",
"platformSpecificData": {
"threadItems": [
{
"content": "A thread about building APIs",
"mediaItems": [{"type": "image", "url": "https://cdn.example.com/api.jpg"}]
},
{
"content": "First, design your endpoints around resources, not actions."
},
{
"content": "Second, always version your API from day one."
},
{
"content": "Finally, document everything! Your future self will thank you."
}
]
}
}],
"publishNow": true
}'Media Requirements
Images
| Property | Requirement |
|---|---|
| Max images | 4 per post |
| Formats | JPEG, PNG, WebP, GIF |
| Max file size | 1 MB per image (strict) |
| Max dimensions | 2000 x 2000 px |
| Recommended | 1200 x 675 px (16:9) |
Aspect Ratios
| Type | Ratio | Dimensions |
|---|---|---|
| Landscape | 16:9 | 1200 x 675 px |
| Square | 1:1 | 1000 x 1000 px |
| Portrait | 4:5 | 800 x 1000 px |
Videos
| Property | Requirement |
|---|---|
| Max videos | 1 per post |
| Format | MP4 only |
| Max file size | 50 MB |
| Max duration | 60 seconds |
| Max dimensions | 1920 x 1080 px |
| Frame rate | 30 fps recommended |
Recommended Video Specs
| Property | Recommended |
|---|---|
| Resolution | 1280 x 720 px (720p) |
| Aspect ratio | 16:9 (landscape) or 1:1 (square) |
| Frame rate | 30 fps |
| Codec | H.264 |
| Audio | AAC |
Platform-Specific Fields
All fields go inside platformSpecificData on the Bluesky platform entry.
| Field | Type | Description |
|---|---|---|
threadItems | Array<{content, mediaItems?}> | Create thread chains. Each item becomes a reply to the previous post. Each item is limited to 300 characters and can have optional media. |
Connection
Bluesky uses App Passwords instead of OAuth. To connect a Bluesky account:
- Go to your Bluesky Settings > App Passwords
- Create a new App Password (formatted as
xxxx-xxxx-xxxx-xxxx) - Use the connect endpoint with your handle and app password
- Custom domain handles are supported (e.g.,
brand.cominstead ofbrand.bsky.social)
const account = await zernio.connect.connectBlueskyCredentials({
profileId: 'YOUR_PROFILE_ID',
handle: 'yourhandle.bsky.social',
appPassword: 'xxxx-xxxx-xxxx-xxxx'
});
console.log('Connected:', account._id);account = client.connect.connect_bluesky_credentials(
profile_id="YOUR_PROFILE_ID",
handle="yourhandle.bsky.social",
app_password="xxxx-xxxx-xxxx-xxxx"
)
print(f"Connected: {account['_id']}")curl -X POST https://zernio.com/api/v1/connect/bluesky \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"profileId": "YOUR_PROFILE_ID",
"handle": "yourhandle.bsky.social",
"appPassword": "xxxx-xxxx-xxxx-xxxx"
}'Rich Text
Zernio auto-detects and converts text to AT Protocol facets. No special formatting is needed from developers:
@handle.bsky.social-- rendered as a clickable profile link#hashtag-- rendered as a clickable hashtag- URLs -- rendered as clickable links with preview cards
When your post contains a URL, Bluesky automatically generates a link card preview. For best results, place the URL at the end of your post and ensure the target page has proper Open Graph meta tags.
Media URL Requirements
These do not work as media URLs:
- Google Drive -- returns an HTML download page, not the file
- Dropbox -- returns an HTML preview page
- OneDrive / SharePoint -- returns HTML
- iCloud -- returns HTML
Test your URL in an incognito browser window. If you see a webpage instead of the raw image or video, it will not work.
Media URLs must be:
- Publicly accessible (no authentication required)
- Returning actual media bytes with the correct
Content-Typeheader - Not behind redirects that resolve to HTML pages
- Hosted on a fast, reliable CDN
Supabase URLs: Zernio auto-proxies Supabase storage URLs, so they work without additional configuration.
Analytics
Requires Analytics add-on
Available metrics via the Analytics API:
| Metric | Available |
|---|---|
| Likes | ✅ |
| Comments | ✅ |
| Shares (reposts) | ✅ |
Bluesky does not provide impressions, reach, clicks, or view counts through its API.
const analytics = await zernio.analytics.getAnalytics({
platform: 'bluesky',
fromDate: '2024-01-01',
toDate: '2024-01-31'
});
console.log(analytics.posts);analytics = client.analytics.get(
platform="bluesky",
from_date="2024-01-01",
to_date="2024-01-31"
)
print(analytics["posts"])curl "https://zernio.com/api/v1/analytics?platform=bluesky&fromDate=2024-01-01&toDate=2024-01-31" \
-H "Authorization: Bearer YOUR_API_KEY"What You Can't Do
These features are not available through Bluesky's API:
- Create lists or starter packs
- Create custom feeds
- Pin posts to profile
- Add content warnings or labels
- Send DM attachments (Bluesky's Chat API does not support media)
- See follower counts or profile analytics
Common Errors
Bluesky has a 19.3% failure rate across Zernio's platform (3,633 failures out of 18,857 attempts). Here are the most frequent errors and how to fix them:
| Error | What it means | How to fix |
|---|---|---|
| "Bluesky posts cannot exceed 300 characters" | Content exceeds the 300 char hard limit | Shorten to 300 chars. Use customContent for cross-platform posts. |
| "Thread item N exceeds 300 characters" | A specific thread item is too long | Each thread item has its own 300 char limit. Split into more items. |
| "Publishing failed due to max retries reached" | All retries failed | Usually temporary. Retry manually. |
| App Password invalid | Wrong password type or expired credentials | Ensure you're using an App Password (xxxx-xxxx-xxxx-xxxx), not your main account password. Create a new one if needed. |
| Image too large | Image exceeds 1 MB limit | Compress images before upload. Zernio auto-compresses, but may degrade quality. |
Inbox
Requires Inbox add-on — Build: +$10/mo · Accelerate: +$50/unit · Unlimited: +$1,000/mo
Bluesky supports DMs and comments.
Direct Messages
| Feature | Supported |
|---|---|
| List conversations | ✅ |
| Fetch messages | ✅ |
| Send text messages | ✅ |
| Send attachments | ❌ (API limitation) |
| Archive/unarchive | ✅ |
Comments
| Feature | Supported |
|---|---|
| List comments on posts | ✅ |
| Reply to comments | ✅ |
| Delete comments | ✅ |
| Like comments | ✅ (requires CID) |
| Unlike comments | ✅ (requires likeUri) |
Limitations
- No DM attachments - Bluesky's Chat API does not support media
- Like requires CID - You must provide the content identifier (
cid) when liking a comment - Unlike requires likeUri - Store the
likeUrireturned when liking to unlike later
See Messages and Comments API Reference for endpoint details.
Related Endpoints
- Connect Bluesky Account - App Password authentication
- Create Post - Post creation and scheduling
- Upload Media - Image and video uploads
- Bluesky Media Download - Download Bluesky media
- Messages and Comments