PostZen

Bluesky

Connect Bluesky with an app password, then publish or schedule text and image posts through PostZen.

PostZen publishes and schedules text and image posts to Bluesky. Bluesky is the only PostZen platform that connects with an app password instead of OAuth.

Quick reference

PropertyBluesky via PostZen
Platform valuebluesky
AuthenticationBluesky handle and app password on a PostZen-hosted page; no OAuth
Post typesText post, image post
Character limit300 characters, hard limit
Images per post1–4
Maximum image size1 MB per image, hard limit
PostZen image upload typesJPEG/JPG, PNG, WebP, GIF
VideoNot yet supported; coming soon
SchedulingSupported
Rich textLinks, @mentions, and #hashtags become clickable facets automatically
Link previewGenerated for the first URL by default

Before you start

You need a PostZen profile and a Bluesky app password. In the Bluesky app, open Settings → Privacy and Security → App Passwords, then create an app password. It uses the format xxxx-xxxx-xxxx-xxxx.

Use the app password when connecting Bluesky, never your main Bluesky account password. This integration does not use OAuth.

Custom-domain handles and accounts on self-hosted PDS servers are supported.

Keep these publishing limits in mind:

  • Bluesky rejects posts over 300 characters. This hard limit is the most common cross-posting failure.
  • A post can contain up to 4 images, and every image must be 1 MB or smaller. Bluesky returns a hard error for larger images, so resize or compress them first.
  • PostZen does not yet support Bluesky video posts.

Media URLs must be publicly accessible direct links to the files. Google Drive, Dropbox, OneDrive, and iCloud share links return HTML pages instead of files. Use a direct URL or upload the file through POST /v1/media/presign.

The free tier allows 2 connected accounts before a payment method is required. Connecting another account returns a 402.

Connect your account

Start the connection with GET /v1/connect/bluesky?profileId=.... PostZen returns { authUrl, state }; the state expires after 10 minutes.

const { data } = await postzen.connect.createConnectUrl({
  path: { platform: 'bluesky' },
  query: { profileId: 'jh72r5nqk9wx3v8m1t4cz6bs0fy5dg3e' },
});

console.log(data.authUrl); // open this PostZen-hosted page
console.log(data.state);   // expires after 10 minutes
response = client.connect.create_connect_url(
    "bluesky",
    profile_id="jh72r5nqk9wx3v8m1t4cz6bs0fy5dg3e",
)

print(response.authUrl)  # open this PostZen-hosted page
print(response.state)    # expires after 10 minutes
curl "https://api.postzen.dev/v1/connect/bluesky?profileId=jh72r5nqk9wx3v8m1t4cz6bs0fy5dg3e" \
  -H "Authorization: Bearer $POSTZEN_API_KEY"

Send the user to the returned authUrl. The URL opens PostZen's hosted Bluesky connection page, where the user enters their Bluesky handle and app password. No Bluesky credentials belong in the API request, and there is no OAuth grant for this integration.

Quick start

Create a text post by targeting the connected account with the bluesky platform value. The post below publishes immediately and stays under the 300-character hard limit.

const { data } = await postzen.posts.createPost({
  body: {
    content: 'Hello from Bluesky!',
    publishNow: true,
    platforms: [
      {
        platform: 'bluesky',
        accountId: 'jh72r5nqk9wx3v8m1t4cz6bs0fy5dg3e',
      },
    ],
  },
});

console.log(data.post._id);
response = client.posts.create_post(
    content="Hello from Bluesky!",
    publish_now=True,
    platforms=[
        {
            "platform": "bluesky",
            "account_id": "jh72r5nqk9wx3v8m1t4cz6bs0fy5dg3e",
        },
    ],
)

print(response.post.field_id)
curl -X POST https://api.postzen.dev/v1/posts \
  -H "Authorization: Bearer $POSTZEN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Hello from Bluesky!",
    "publishNow": true,
    "platforms": [
      {
        "platform": "bluesky",
        "accountId": "jh72r5nqk9wx3v8m1t4cz6bs0fy5dg3e"
      }
    ]
  }'

Set exactly one of publishNow, scheduledFor, or isDraft. For a scheduled post, replace publishNow with an ISO-8601 scheduledFor value at least 60 seconds in the future.

Content types

Text posts

Text posts can contain up to 300 characters. For cross-posts, use customContent on the Bluesky target when the shared text is too long:

{
  platform: 'bluesky',
  accountId: 'jh72r5nqk9wx3v8m1t4cz6bs0fy5dg3e',
  customContent: 'A Bluesky-specific version under 300 characters.',
}

Image posts

Attach 1–4 images through mediaItems. Each image must be no larger than 1 MB, even though PostZen can download larger external files. Set settings.altTexts in the same order as the images.

const { data } = await postzen.posts.createPost({
  body: {
    content: 'Two views from today.',
    publishNow: true,
    mediaItems: [
      { url: 'https://cdn.example.com/images/lake.jpg' },
      { url: 'https://cdn.example.com/images/trail.jpg' },
    ],
    platforms: [
      {
        platform: 'bluesky',
        accountId: 'jh72r5nqk9wx3v8m1t4cz6bs0fy5dg3e',
        settings: {
          altTexts: [
            'A calm lake under a cloudy sky.',
            'A narrow trail through evergreen trees.',
          ],
        },
      },
    ],
  },
});

console.log(data.post._id);
response = client.posts.create_post(
    content="Two views from today.",
    publish_now=True,
    media_items=[
        {"url": "https://cdn.example.com/images/lake.jpg"},
        {"url": "https://cdn.example.com/images/trail.jpg"},
    ],
    platforms=[
        {
            "platform": "bluesky",
            "account_id": "jh72r5nqk9wx3v8m1t4cz6bs0fy5dg3e",
            "settings": {
                "alt_texts": [
                    "A calm lake under a cloudy sky.",
                    "A narrow trail through evergreen trees.",
                ],
            },
        },
    ],
)

print(response.post.field_id)
curl -X POST https://api.postzen.dev/v1/posts \
  -H "Authorization: Bearer $POSTZEN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Two views from today.",
    "publishNow": true,
    "mediaItems": [
      { "url": "https://cdn.example.com/images/lake.jpg" },
      { "url": "https://cdn.example.com/images/trail.jpg" }
    ],
    "platforms": [
      {
        "platform": "bluesky",
        "accountId": "jh72r5nqk9wx3v8m1t4cz6bs0fy5dg3e",
        "settings": {
          "altTexts": [
            "A calm lake under a cloudy sky.",
            "A narrow trail through evergreen trees."
          ]
        }
      }
    ]
  }'

Rich text

PostZen detects links, @mentions, and #hashtags in Bluesky post text and creates clickable rich-text facets automatically. Write normal text; you do not need to add markup or provide facet data.

By default, PostZen also generates a preview card for the first URL. Set settings.disableLinkCard: true on the Bluesky target to skip that card.

const { data } = await postzen.posts.createPost({
  body: {
    content: 'Release notes for @postzen.dev #PostZen https://example.com/releases',
    publishNow: true,
    platforms: [
      {
        platform: 'bluesky',
        accountId: 'jh72r5nqk9wx3v8m1t4cz6bs0fy5dg3e',
        settings: {
          languages: ['en'],
          disableLinkCard: true,
        },
      },
    ],
  },
});

console.log(data.post._id);
response = client.posts.create_post(
    content="Release notes for @postzen.dev #PostZen https://example.com/releases",
    publish_now=True,
    platforms=[
        {
            "platform": "bluesky",
            "account_id": "jh72r5nqk9wx3v8m1t4cz6bs0fy5dg3e",
            "settings": {
                "languages": ["en"],
                "disable_link_card": True,
            },
        },
    ],
)

print(response.post.field_id)
curl -X POST https://api.postzen.dev/v1/posts \
  -H "Authorization: Bearer $POSTZEN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Release notes for @postzen.dev #PostZen https://example.com/releases",
    "publishNow": true,
    "platforms": [
      {
        "platform": "bluesky",
        "accountId": "jh72r5nqk9wx3v8m1t4cz6bs0fy5dg3e",
        "settings": {
          "languages": ["en"],
          "disableLinkCard": true
        }
      }
    ]
  }'

The 300-character hard limit includes post text containing links, mentions, and hashtags. Shorten the Bluesky target with customContent before publishing a longer cross-post.

Media requirements

Images

RequirementValue
Images per post1–4
Maximum file size1 MB per image; hard limit
PostZen media upload typesJPEG/JPG, PNG, WebP, GIF
Media sourcePublicly accessible direct URL or a publicUrl returned by /v1/media/presign
Alt textOne altTexts entry per image, matched by order; up to 2,000 characters per entry

PostZen's create-post endpoint accepts up to 10 mediaItems, but a Bluesky target can contain no more than 4 images. External media URLs can be up to 100 MB when PostZen downloads and re-hosts them; the final Bluesky image must still be 1 MB or smaller.

Videos

RequirementValue
SupportNot yet supported; coming soon
Video formatsNone for Bluesky posts through PostZen
Maximum file sizeNot applicable
DurationNot applicable

Do not include a video when targeting Bluesky. Video publishing is not yet supported.

Platform settings

Place Bluesky settings in the target object's settings field.

KeyTypeNotes
altTextsstring[]Alt text for each image, matched by order. Each entry can contain up to 2,000 characters.
languagesstring[]Up to 3 BCP-47 language codes, such as en or pt-BR.
disableLinkCardbooleanSkips the external preview card for the first URL when true.
{
  platform: 'bluesky',
  accountId: 'jh72r5nqk9wx3v8m1t4cz6bs0fy5dg3e',
  settings: {
    altTexts: ['A product screenshot with the navigation open.'],
    languages: ['en'],
    disableLinkCard: true,
  },
}

PostZen analytics is live for Bluesky, including post metrics, follower counts, and best-time-to-post suggestions. Bluesky does not count impressions or reach anywhere in the protocol, so those metrics will never be available — expect likes, replies, reposts, and quotes only.

What you can't do

  • Publish video posts. Video support is coming soon.
  • Use OAuth to connect Bluesky. PostZen requires a Bluesky handle and app password on its hosted connection page.
  • Read or send DMs, or manage an inbox.
  • Read or manage comments.
  • Create or manage lists, starter packs, or custom feeds.
  • Pin posts or add content warnings and labels.
  • Edit a published post.
  • Manage ads or receive engagement webhooks.

Common errors

ErrorMeaningFix
Text exceeds 300 charactersBluesky enforces a 300-character hard limit.Shorten content, or set a shorter customContent on the Bluesky target.
Image exceeds 1 MBThe image exceeds Bluesky's hard blob limit.Resize or compress every image to 1 MB or smaller before posting.
Invalid or revoked app passwordBluesky cannot authenticate the saved app password.Start the connect flow again and enter a current app password on the hosted page.
Invalid language codeA languages entry is not a BCP-47 code, or the array contains more than 3 codes.Use up to 3 codes such as en or pt-BR.
402 while connectingYour PostZen account has reached the free-tier limit of 2 connected accounts without a payment method.Add a payment method before connecting another account.

On this page