Bluesky

Publish text and image posts to Bluesky over the AT Protocol. The platform id is bluesky. Posts are records created in the account’s repo; images are uploaded as blobs and embedded, and links and @handle mentions are detected and rendered as rich text. First-comment and thread containers are posted as native replies with proper root/parent references.

App password, not OAuth

Unlike every other network, Bluesky does not use OAuth. You connect an account with its handle and an app password generated in the Bluesky app settings; PostFuze mints a short-lived session per publish from that credential.

Supported content

  • Text — up to 300 graphemes, with auto-detected link and mention facets.
  • Image — up to 4 images per post, each embedded as a blob with optional alt text.
  • First comment / thread — reply containers are posted as native replies chained off the previous post.

Limits

PropertyValue
Character limit300 graphemes (rejected if longer)
ImagesUp to 4 per post
Image size≤ ~1,000,000 bytes per blob
First comment / threadYes — native replies
AuthHandle + app password (no OAuth)

Configuration fields

Bluesky has no per-post configuration fields — omit config.blueskyentirely. A self-hosted account’s Personal Data Server (PDS) host is resolved automatically from its handle when the account is connected, so it never needs to be set on a post.

The PDS host is not a config field

service / pds / serviceUrl are notaccepted on a post. The PDS host is fixed server-side from the account’s connect-time identity (DID document); accepting it per-post would let a request redirect the credential-bearing session call to an arbitrary host. To use a custom PDS, just connect the account by its handle — it resolves on its own.

Connecting an account

There is no hosted OAuth redirect for Bluesky. Connect an account by supplying its handle and an app password — the MCP server exposes a connect_bluesky tool for exactly this. When you register the BYOK network record with POST /social-networks, the client_id and client_secret fields accept placeholder values, since the per-account app password is what actually authenticates publishing.

Connect via REST

POST /social-accounts/bluesky/connect

Connect a Bluesky account in a single API-key-authenticated call — no redirect, no callback. Post the account’s handle (or email) and an app password; PostFuze opens a session against the PDS to validate the credentials and read the account identity, then stores the app password as a non-expiring credential so the publish pipeline can re-mint a session on each post.

FieldTypeRequiredDescription
identifierstringYes*The account handle (e.g. acme.bsky.social) or the email it was created with. handle is accepted as an alias — supply exactly one.
handlestringYes*Alias for identifier (the connect_bluesky MCP tool posts this name). If both are present, identifier wins.
app_passwordstringYesA Bluesky app password — never the main account password.

* Supply one of identifier or handle.

curl — connect Bluesky
curl https://api.postfuze.com/api/v1/social-accounts/bluesky/connect \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "identifier": "acme.bsky.social",
    "app_password": "xxxx-xxxx-xxxx-xxxx"
  }'
201 Created
{
  "id": "a1b2c3d4-…",
  "network": "bluesky",
  "username": "acme.bsky.social",
  "account_type": "profile",
  "status": "active"
}

The response id is the social account ID you publish to. If Bluesky rejects the credentials the call fails with 422 Unprocessable Entity — regenerate the app password and try again. The credential is stored encrypted at rest and is never returned by any read endpoint.

Use an app password, never your main password

Generate a dedicated app password under Settings → Privacy and Security → App Passwords in the Bluesky app (or at bsky.app/settings/app-passwords). It is scoped, revocable, and can be rotated without touching your account password. A self-hosted account on a custom PDS works automatically — its host is resolved from the handle at connect time, with no extra configuration. This is the same flow described in the connecting-accounts guide.

Create a post

A short text post with a single image:

curl
curl https://api.postfuze.com/api/v1/posts \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "accounts": ["acct_bsky_01"],
    "containers": [
      {
        "content": "Crossposting from one API to the whole fediverse-adjacent world 🦋",
        "media": [{ "id": "media_butterfly", "alt_text": "PostFuze logo on a blue background" }]
      }
    ]
  }'
201 Created
{
	"id": "post_b551",
	"status": "queued",
	"containers": [
		{ "id": "ctr_b1", "position": 0, "role": "main", "content": "Crossposting from one API to the whole fediverse-adjacent world 🦋" }
	],
	"targets": [
		{ "id": "tgt_bsky1", "social_account_id": "acct_bsky_01", "platform": "bluesky", "status": "queued", "platform_post_id": null }
	]
}

Keep posts within 300 graphemes — content beyond the limit is rejected by Bluesky. Once published, platform_post_url links to the post on bsky.app.