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
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
| Property | Value |
|---|---|
| Character limit | 300 graphemes (rejected if longer) |
| Images | Up to 4 per post |
| Image size | ≤ ~1,000,000 bytes per blob |
| First comment / thread | Yes — native replies |
| Auth | Handle + 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.
| Field | Type | Required | Description |
|---|---|---|---|
identifier | string | Yes* | The account handle (e.g. acme.bsky.social) or the email it was created with. handle is accepted as an alias — supply exactly one. |
handle | string | Yes* | Alias for identifier (the connect_bluesky MCP tool posts this name). If both are present, identifier wins. |
app_password | string | Yes | A Bluesky app password — never the main account password. |
* Supply one of identifier or handle.
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"
}'{
"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
Create a post
A short text post with a single image:
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" }]
}
]
}'{
"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.