Platform Tips

Posting to Bluesky via API: The Developer Guide to the Decentralized Social Network

408 views
Serge Bulaev
Serge Bulaev
Posting to Bluesky via API: The Developer Guide to the Decentralized Social Network

TL;DR

Post to Bluesky via Publora API without dealing with AT Protocol complexity. Covers app passwords, 300-character limits, image uploads, video support, and cross-platform posting from Bluesky.

What Makes Bluesky Different: The AT Protocol

Bluesky is not just another Twitter clone — it is built on the AT Protocol (Authenticated Transfer Protocol), a fundamentally different architecture for social networking. Where Twitter and Instagram are centralized platforms controlled by a single company, Bluesky is designed as a decentralized, federated network where users own their data, identity, and social graph.

For developers, this means Bluesky's API is radically different from what you are used to. There are no simple OAuth tokens — instead, you work with DIDs (Decentralized Identifiers), facets for rich text with precise byte offsets, blob uploads for media, and AT Protocol records instead of REST-style resources. The learning curve is steep, even for experienced API developers.

What you'll learn in this guide:

  • What makes Bluesky's AT Protocol different from traditional social APIs
  • How to connect your Bluesky account to Publora (app password setup)
  • Posting text with automatic hashtag and URL facets
  • Attaching images (up to 4) and video (up to 3 minutes)
  • How Publora abstracts the byte offset math, DID resolution, and blob uploads
  • Code examples in JavaScript and Python

Why Bluesky Matters for Developers and Brands

Bluesky has been growing rapidly since opening to public signups, attracting a developer-heavy, tech-savvy audience. For brands and developers building social media tools, Bluesky offers several unique advantages.

300

Character limit per post

4

Max images per post

3 min

Max video duration

Decentralized Identity

Users own their identity via DIDs. If Bluesky the company disappears, your handle and content can migrate to another AT Protocol server. This makes the platform appealing to users wary of centralized control.

Custom Feeds (Algorithms)

Anyone can build and publish a custom feed algorithm. This means your content can appear in niche, community-curated feeds — not just the default chronological timeline.

Open API, No Gatekeeping

Unlike Twitter/X which charges for API access, Bluesky's API is free and open. No partner approval, no expensive tiers, no access restrictions. Any developer can build on it.

Developer-First Audience

Bluesky's early adopters are disproportionately developers, tech journalists, and open-source enthusiasts. If your audience is technical, Bluesky likely has higher engagement per follower than other platforms.

The Problem: AT Protocol Complexity

While Bluesky's open architecture is a strength, it creates significant complexity for developers who just want to post content. Here is what the AT Protocol requires you to handle when posting directly.

Direct AT Protocol (DIY)

  1. Resolve your handle to a DID
  2. Create a session with app password
  3. Calculate byte offsets for every hashtag and URL
  4. Handle multi-byte characters (emojis, CJK) correctly
  5. Upload media as blobs with correct MIME types
  6. Construct AT Protocol records with proper schemas
  7. Handle session refresh when tokens expire

Via Publora (Simple)

  1. Send a POST request with content and platform ID
  2. Upload media via presigned URL (optional)
  3. Done.

The most painful part of working directly with Bluesky's API is facet creation. Bluesky uses a rich text system where hashtags and URLs must be annotated with precise byte offsets — not character offsets. This means a single emoji (which may be 4 bytes in UTF-8) shifts every subsequent offset. Getting this wrong results in broken links and garbled hashtags.

// What facets look like in raw AT Protocol — you DON'T need this with Publora
const record = {
  $type: 'app.bsky.feed.post',
  text: 'Check out #devtools at https://example.com',
  facets: [
    {
      index: { byteStart: 10, byteEnd: 19 },  // "#devtools"
      features: [{ $type: 'app.bsky.richtext.facet#tag', tag: 'devtools' }]
    },
    {
      index: { byteStart: 23, byteEnd: 43 },  // "https://example.com"
      features: [{ $type: 'app.bsky.richtext.facet#link', uri: 'https://example.com' }]
    }
  ],
  createdAt: new Date().toISOString()
};
// Publora calculates ALL of this automatically from your plain text content

Connect Bluesky to Publora

Connecting Bluesky is straightforward but works differently from OAuth-based platforms. Bluesky uses identifier + app password authentication rather than an OAuth flow.

Step 1: Generate an App Password

  1. Open the Bluesky app (web or mobile)
  2. Go to Settings → App Passwords
  3. Click "Add App Password"
  4. Give it a descriptive name (e.g., "Publora Integration")
  5. Copy the generated password — you will only see it once

Never use your main password

Always use an app password for third-party integrations. App passwords can be revoked independently without changing your main account password, and they limit the scope of access granted to the application.

Step 2: Add to Publora Dashboard

  1. Open app.publora.com and go to Connections
  2. Click "+ Add Connection" and select Bluesky
  3. Enter your identifier (your Bluesky handle, e.g., yourname.bsky.social)
  4. Enter your app password
  5. Click Connect

Your Bluesky account will appear in the Connections list with a platform ID in the format bluesky-did:plc:abc123xyz. The did:plc:... part is your Decentralized Identifier — a permanent, portable identifier that follows you even if you change your handle.

Posting Text to Bluesky

The simplest Bluesky post is a text update. Publora automatically detects and converts hashtags and URLs into clickable Bluesky facets — you just write plain text.

JavaScript

const response = await fetch('https://api.publora.com/api/v1/create-post', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-publora-key': 'sk_YOUR_API_KEY'
  },
  body: JSON.stringify({
    content: 'Just shipped v2.0 of our developer toolkit! Full changelog at https://example.com/changelog #opensource #devtools',
    platforms: ['bluesky-did:plc:abc123xyz']
  })
});

const data = await response.json();
// { "success": true, "postGroupId": "abc123..." }
console.log('Posted to Bluesky:', data.postGroupId);

Python

import requests

API_KEY = 'sk_YOUR_API_KEY'
BASE = 'https://api.publora.com/api/v1'

response = requests.post(
    f'{BASE}/create-post',
    headers={
        'Content-Type': 'application/json',
        'x-publora-key': API_KEY
    },
    json={
        'content': 'Just shipped v2.0 of our developer toolkit! Full changelog at https://example.com/changelog #opensource #devtools',
        'platforms': ['bluesky-did:plc:abc123xyz']
    }
)

data = response.json()
# {"success": True, "postGroupId": "abc123..."}
print(f'Posted to Bluesky: {data["postGroupId"]}')

Automatic rich text handling

In the examples above, #opensource, #devtools, and the URL are automatically converted to clickable Bluesky facets with correct byte offsets. This works correctly even with multi-byte characters like emojis and non-Latin scripts. You do not need to do any special formatting.

Posting with Images

Bluesky supports up to 4 images per post. All images are automatically converted to JPEG by Publora before being uploaded to Bluesky, so you can upload PNG, WebP, GIF, or any format supported by sharp.

JavaScript (with alt text)

// Step 1: Create the post with alt text
const response = await fetch('https://api.publora.com/api/v1/create-post', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-publora-key': 'sk_YOUR_API_KEY'
  },
  body: JSON.stringify({
    content: 'New dashboard design is live! Dark mode included. #buildinpublic',
    platforms: ['bluesky-did:plc:abc123xyz'],
    altTexts: ['Screenshot of the new analytics dashboard with dark mode enabled, showing traffic graphs and engagement metrics']
  })
});

const { postGroupId } = await response.json();

// Step 2: Get presigned upload URL
const uploadRes = await fetch(
  `https://api.publora.com/api/v1/upload-media/${postGroupId}?filename=dashboard-dark.png`,
  { headers: { 'x-publora-key': 'sk_YOUR_API_KEY' } }
);
const { uploadUrl } = await uploadRes.json();

// Step 3: Upload the image (PNG is fine — Publora converts to JPEG for Bluesky)
const imageBuffer = fs.readFileSync('dashboard-dark.png');
await fetch(uploadUrl, {
  method: 'PUT',
  headers: { 'Content-Type': 'image/png' },
  body: imageBuffer
});

Python (multiple images)

import requests

API_KEY = 'sk_YOUR_API_KEY'
BASE = 'https://api.publora.com/api/v1'

# Step 1: Create post with alt texts for multiple images
resp = requests.post(f'{BASE}/create-post', headers={
    'Content-Type': 'application/json',
    'x-publora-key': API_KEY
}, json={
    'content': 'Before and after our office renovation!',
    'platforms': ['bluesky-did:plc:abc123xyz'],
    'altTexts': [
        'Office before renovation with old furniture and dim lighting',
        'Office after renovation with modern desks and natural light'
    ]
})
post_id = resp.json()['postGroupId']

# Step 2: Upload each image
images = ['before.jpg', 'after.jpg']
for img in images:
    upload_resp = requests.get(
        f'{BASE}/upload-media/{post_id}?filename={img}',
        headers={'x-publora-key': API_KEY}
    )
    upload_url = upload_resp.json()['uploadUrl']

    with open(img, 'rb') as f:
        requests.put(upload_url, headers={'Content-Type': 'image/jpeg'}, data=f)

print(f'Posted with {len(images)} images!')

Image Specifications

Specification Limit
Max images per post 4
Max size per image ~1 MB (976.56 KB) after JPEG conversion
Max dimensions 2000 x 2000 pixels
Input formats JPEG, PNG, WebP, GIF, TIFF, BMP (all converted to JPEG)
Output format JPEG (always, regardless of input)
Alt text limit 2,000 characters per image

Image size constraint

The AT Protocol enforces a strict ~1 MB limit on uploaded blobs. If your image exceeds this after JPEG conversion, try compressing to 80-85% JPEG quality or reducing the dimensions. Publora handles the conversion, but the size constraint is enforced by Bluesky's servers.

Posting Video to Bluesky

Bluesky added video support with specific constraints. Videos must be MP4 format with tiered size limits based on duration.

Duration Max File Size
Under 60 seconds 50 MB
60 seconds - 3 minutes 100 MB

Daily Video Limits

Bluesky limits video uploads to 25 videos per day or 10 GB total per day, whichever is reached first. Plan your video content calendar accordingly.

Email Verification Required

Your Bluesky account must have a verified email before you can upload videos. This is a Bluesky requirement, not a Publora limitation.

// Post a video to Bluesky
const response = await fetch('https://api.publora.com/api/v1/create-post', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-publora-key': 'sk_YOUR_API_KEY'
  },
  body: JSON.stringify({
    content: 'Quick demo of our new CLI tool in action #devtools',
    platforms: ['bluesky-did:plc:abc123xyz']
  })
});

const { postGroupId } = await response.json();

// Upload video (MP4 only, max 3 minutes)
const uploadRes = await fetch(
  `https://api.publora.com/api/v1/upload-media/${postGroupId}?filename=demo.mp4`,
  { headers: { 'x-publora-key': 'sk_YOUR_API_KEY' } }
);
const { uploadUrl } = await uploadRes.json();

await fetch(uploadUrl, {
  method: 'PUT',
  headers: { 'Content-Type': 'video/mp4' },
  body: fs.readFileSync('demo.mp4')
});

How Publora Abstracts AT Protocol Complexity

To appreciate what Publora handles for you, here is a side-by-side comparison of posting directly via AT Protocol versus using Publora.

Direct AT Protocol (50+ lines):

1. Resolve handle → DID via DNS or HTTP
2. Create session with createSession endpoint
3. Parse content for hashtags/URLs
4. Calculate byte offsets for each facet
5. Handle emoji/multi-byte offset shifts
6. Upload image as blob with uploadBlob
7. Get blob reference from response
8. Construct app.bsky.feed.post record
9. Submit via com.atproto.repo.createRecord
10. Handle session refresh on 401

Via Publora (3 lines):

1. POST /create-post with content + platform ID
2. GET /upload-media/{id} for presigned URL (if media)
3. PUT file to presigned URL (if media)

Behind the scenes, Publora performs all of the AT Protocol operations for you:

DID Resolution

Your handle is resolved to a DID during connection. Publora stores and manages the DID so you never need to think about it.

Session Management

AT Protocol sessions expire. Publora handles session creation, refresh, and retry logic automatically.

Rich Text Facets

Hashtags and URLs are detected and converted to facets with correct byte offsets — including multi-byte character handling.

Image Conversion

All image formats are converted to JPEG and compressed to fit within Bluesky's ~1 MB blob limit before upload.

Bluesky Character Limits: What Counts

Bluesky's 300-character limit is strict, and the counting rules matter when you are automating content.

URLs count in full

Unlike Twitter/X which shortens URLs to ~23 characters, Bluesky counts the full URL length toward the 300-character limit. A URL like https://example.com/very/long/path consumes 35 characters. When cross-posting to both platforms, your Bluesky version may need a shorter URL or different phrasing.

Element Bluesky Twitter/X
Character limit 300 280
URL counting Full length ~23 chars (shortened)
Hashtags Count toward limit Count toward limit
Emojis 1 character (display) 2 characters

Cross-Platform Posting: Bluesky + Other Networks

One of the biggest advantages of using Publora is posting to Bluesky alongside other platforms in a single API call. Simply include multiple platform IDs in the platforms array.

// Post to Bluesky, Mastodon, and X simultaneously
const response = await fetch('https://api.publora.com/api/v1/create-post', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-publora-key': 'sk_YOUR_API_KEY'
  },
  body: JSON.stringify({
    content: 'We just open-sourced our API client library! Check it out: https://github.com/example/sdk #opensource',
    platforms: [
      'bluesky-did:plc:abc123xyz',    // Bluesky (300 chars, auto facets)
      'mastodon-123456',               // Mastodon (500 chars)
      'twitter-987654321'              // X/Twitter (280 chars, auto threads)
    ]
  })
});

Publora handles the differences automatically — the 300-character Bluesky limit, Mastodon's 500-character limit, Twitter's 280-character limit with emoji double-counting, and each platform's unique media requirements. If your content exceeds a platform's limit, Publora will either truncate (with a note) or split into threads where supported.

Verifying and Monitoring Posts

After posting, you can verify your Bluesky content through the Publora dashboard or API.

import requests

API_KEY = 'sk_YOUR_API_KEY'
BASE = 'https://api.publora.com/api/v1'

# List recent posts
resp = requests.get(
    f'{BASE}/list-posts?limit=10',
    headers={'x-publora-key': API_KEY}
)

for post in resp.json()['posts']:
    print(f"{post['status']} | {post['content'][:50]}... | {post['platforms']}")

Getting Started in 5 Minutes

Here is the fastest path from zero to your first Bluesky post via the API.

  1. Create a Publora account — free trial available
  2. Generate a Bluesky app password — Settings → App Passwords in the Bluesky app
  3. Connect Bluesky — Publora dashboard → Connections → Add → Bluesky
  4. Get your API key — Settings → API Keys → Create Key (docs)
  5. Send your first post — Use the JavaScript or Python examples above

Post to the decentralized web — the easy way

Skip the AT Protocol complexity. One API call, automatic facets, cross-platform posting.

Get Started Free →

Frequently Asked Questions

What makes Bluesky different from Twitter/X for developers?

Bluesky is built on the AT Protocol (Authenticated Transfer Protocol), a decentralized framework where users own their data and identity. For developers, this means dealing with DIDs (Decentralized Identifiers), facets for rich text with byte offsets, and blob uploads for media — all concepts that do not exist in traditional social APIs. Publora abstracts all of this into a simple REST API.

What is the character limit for Bluesky posts?

Bluesky has a 300-character limit per post. Unlike Twitter/X, URLs count in full toward this limit (Twitter shortens them to ~23 characters). This means a post with a long URL has significantly less room for text on Bluesky. Plan your cross-platform content accordingly.

Do I need to handle AT Protocol facets manually when using Publora?

No. Publora automatically detects hashtags (#tag) and URLs in your content and creates the correct Bluesky facets with proper byte offsets. This includes correct handling of multi-byte characters like emojis and CJK scripts. You write plain text content and Publora handles the facet math.

What image formats does Bluesky accept?

Bluesky only accepts JPEG images natively. Publora automatically converts all uploaded images — PNG, WebP, GIF, TIFF, BMP — to JPEG before sending them to Bluesky via the media upload workflow. Each image must be under approximately 1 MB (976.56 KB) after conversion. You can upload up to 4 images per post.

Can I post videos to Bluesky via the API?

Yes. Bluesky supports video posts with MP4 format, maximum 3 minutes duration, and tiered size limits (50 MB for under 60 seconds, 100 MB for 60s-3min). There is a daily limit of 25 videos or 10 GB. Your Bluesky account must have a verified email before you can upload videos.

Why do I need an app password instead of my regular Bluesky password?

App passwords are scoped credentials designed for third-party integrations. They can be revoked independently without changing your main password, and they limit the permissions granted to the application. Bluesky recommends app passwords for all API integrations. Generate one in Bluesky Settings → App Passwords.

Can I post to Bluesky and other platforms at the same time?

Yes. Publora supports cross-platform posting to 11 platforms including Bluesky, Twitter/X, Mastodon, LinkedIn, Instagram, Threads, Telegram, Facebook, TikTok, YouTube, and Pinterest. Include multiple platform IDs in the platforms array and Publora handles the different APIs, character limits, and media requirements for each platform automatically.

How does Publora handle the AT Protocol complexity?

Publora abstracts the entire AT Protocol stack. You do not need to resolve DIDs, create sessions, calculate byte offsets for facets, upload blobs, or construct AT Protocol records. You send a simple REST request with your content and platform ID, and Publora handles AT Protocol authentication, facet creation, media blob upload, and record creation behind the scenes. See the Bluesky platform docs for full technical details.

Further Reading

Related Articles