Skip to content

Bluesky API

Schedule and automate Bluesky posts with Zernio API - Text posts, images, videos, threads, and App Password authentication

Quick Reference

PropertyValue
Character limit300 (HARD LIMIT)
Images per post4
Videos per post1
Image formatsJPEG, PNG, WebP, GIF
Image max size1 MB (auto-compressed, strict)
Video formatMP4 only
Video max size50 MB
Video max duration60 seconds
Post typesText, Image, Video, Thread
SchedulingYes
Inbox (DMs)Yes (text only)
Inbox (Comments)Yes
AnalyticsNo

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:

typescript
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);
python
result = client.posts.create_post(
    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']}")
bash
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.

typescript
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);
python
result = client.posts.create_post(
    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']}")
bash
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.

typescript
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);
python
result = client.posts.create_post(
    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']}")
bash
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.

typescript
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);
python
result = client.posts.create_post(
    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']}")
bash
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.

typescript
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);
python
result = client.posts.create_post(
    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']}")
bash
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.

> Note: When threadItems is provided, the top-level content field is used only for display and search purposes, it is NOT published. You must include your first post as threadItems[0].

typescript
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);
python
result = client.posts.create_post(
    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']}")
bash
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

PropertyRequirement
Max images4 per post
FormatsJPEG, PNG, WebP, GIF
Max file size1 MB per image (strict)
Max dimensions2000 x 2000 px
Recommended1200 x 675 px (16:9)

Aspect Ratios

TypeRatioDimensions
Landscape16:91200 x 675 px
Square1:11000 x 1000 px
Portrait4:5800 x 1000 px

Videos

PropertyRequirement
Max videos1 per post
FormatMP4 only
Max file size50 MB
Max duration60 seconds
Max dimensions1920 x 1080 px
Frame rate30 fps recommended
PropertyRecommended
Resolution1280 x 720 px (720p)
Aspect ratio16:9 (landscape) or 1:1 (square)
Frame rate30 fps
CodecH.264
AudioAAC

Platform-Specific Fields

All fields go inside platformSpecificData on the Bluesky platform entry.

FieldTypeDescription
threadItemsArray<{content, mediaItems?}>Complete sequence of posts in a Bluesky thread. The first item becomes the root post and must be provided as threadItems[0]. When threadItems is provided, top-level content is for display/search only and is NOT published.

Connection

Bluesky uses App Passwords instead of OAuth. To connect a Bluesky account:

  1. Go to your Bluesky Settings > App Passwords
  2. Create a new App Password (formatted as xxxx-xxxx-xxxx-xxxx)
  3. Use the connect endpoint with your handle and app password
  4. Custom domain handles are supported (e.g., brand.com instead of brand.bsky.social)
python
account = client.connect.connect_bluesky_credentials(
    identifier="yourhandle.bsky.social",
    app_password="xxxx-xxxx-xxxx-xxxx",
    state="profile_id=YOUR_PROFILE_ID",
)
print(f"Connected: {account['_id']}")
bash
curl -X POST https://zernio.com/api/v1/connect/bluesky/credentials \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "identifier": "yourhandle.bsky.social",
    "appPassword": "xxxx-xxxx-xxxx-xxxx",
    "state": "profile_id=YOUR_PROFILE_ID"
  }'

The Node SDK (@zernio/node) does not yet expose connect.connectBlueskyCredentials. Use the Python SDK or hit the endpoint directly via fetch.

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-Type header
  • 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

> Included — Analytics is bundled with every paid account on the Usage plan.

Available metrics via the Analytics API:

MetricAvailable
Likes
Comments
Shares (reposts)

Bluesky does not provide impressions, reach, clicks, or view counts through its API.

typescript
const analytics = await zernio.analytics.getAnalytics({
  platform: 'bluesky',
  fromDate: '2024-01-01',
  toDate: '2024-01-31'
});
console.log(analytics.posts);
python
analytics = client.analytics.get_analytics(
    platform="bluesky",
    from_date="2024-01-01",
    to_date="2024-01-31"
)
print(analytics["posts"])
bash
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:

ErrorWhat it meansHow to fix
"Bluesky posts cannot exceed 300 characters"Content exceeds the 300 char hard limitShorten to 300 chars. Use customContent for cross-platform posts.
"Thread item N exceeds 300 characters"A specific thread item is too longEach thread item has its own 300 char limit. Split into more items.
"Publishing failed due to max retries reached"All retries failedUsually temporary. Retry manually.
App Password invalidWrong password type or expired credentialsEnsure you're using an App Password (xxxx-xxxx-xxxx-xxxx), not your main account password. Create a new one if needed.
Image too largeImage exceeds 1 MB limitCompress images before upload. Zernio auto-compresses, but may degrade quality.

Inbox

> Included — Inbox (DMs, comments, reviews) is bundled with every paid account on the Usage plan.

Bluesky supports DMs and comments.

Direct Messages

FeatureSupported
List conversations
Fetch messages
Send text messages
Send attachments❌ (API limitation)
Archive/unarchive

Comments

FeatureSupported
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 likeUri returned when liking to unlike later

See Messages and Comments API Reference for endpoint details.

  • Connect Bluesky Account - App Password authentication
  • Create Post - Post creation and scheduling
  • Upload Media - Image and video uploads
  • Messages and Comments