# Bluesky

Source: https://postgale.com/docs/bluesky

Publish posts, threads, replies, images, videos and link cards to Bluesky (AT Protocol), and delete them, through the Postgale API and MCP server.

Postgale publishes to Bluesky accounts: text posts with links, mentions and hashtags, up to 4 images or one video, link cards, threads
and replies, and deletes of what was published. Everything is available over the [REST API](https://postgale.com/docs/api) (paths relative to `https://postgale.com/api/v1`)
and the [MCP server](https://postgale.com/docs/mcp) (`connect_account`, `create_post`, `manage_published_post`). Each connected Bluesky account has platform
`bluesky`; its `platformAccountId` is the account's DID (`did:plc:…`).

## Connect an account

Bluesky accounts connect with their **handle** and an **app password**. Postgale never needs the account password.

1. In Bluesky, open **Settings → Privacy and security → App passwords** and choose **Add App Password**. Name it (e.g. *Postgale*),
   leave direct-message access off, and copy the password: it looks like `xxxx-xxxx-xxxx-xxxx`.
2. In Postgale open **Dashboard → Accounts → Bluesky**, enter the handle (e.g. `yourname.bsky.social` or your own domain) and paste the
   app password.

Or connect from your code (the answer is the connected account; `account.connected` is sent as well):

POST /api/v1/connect/bluesky:
```bash
curl -X POST https://postgale.com/api/v1/connect/bluesky \
  -H 'Authorization: Bearer sk_…' \
  -H 'Content-Type: application/json' \
  -d '{"handle":"yourname.bsky.social","appPassword":"xxxx-xxxx-xxxx-xxxx","externalId":"customer-42"}'

# {"platform":"bluesky","account":{"id":"…","platform":"bluesky","accountName":"@yourname.bsky.social","platformAccountId":"did:plc:…","externalId":"customer-42"}}
```

Postgale resolves the handle to its DID and personal data server (PDS), works on any PDS (bsky.social or self-hosted), and signs in
there. The session tokens and the app password are stored encrypted; sessions are renewed automatically, and if the renewal fails
Postgale signs in again with the app password. A wrong handle or password answers 400 with the reason; anything other than an app
password is refused.

If you delete the app password in Bluesky (or change the account password), the next call fails: the account becomes
`needs_reconnect` and `account.disconnected` is sent (reason `token_invalid`). Connect it again with a new app password; the same
account is updated, its post history stays.

## Publish

The post content is the text, up to **300 characters** as Bluesky counts them (graphemes: an emoji with modifiers counts once).
Links, `@handle` mentions and `#hashtags` in the text become clickable: mentions are resolved to the person's DID and left as plain text
when the handle does not exist. Without media, the first link gets a **link card** (title, description and image from the page) when
the page can be read; a card that cannot be built never fails the post.

POST /api/v1/posts:
```bash
curl -X POST https://postgale.com/api/v1/posts \
  -H 'Authorization: Bearer sk_…' \
  -H 'Content-Type: application/json' \
  -d '{"content":"Our spring release is out 🌱 https://example.com/blog/spring #release","platforms":[{"accountId":"acc_bsky","platformSpecificData":{"mediaUrls":["https://media.postgale.com/…/hero.jpg"],"altTexts":["The new dashboard in dark mode"],"langs":["en"]}}],"publishNow":true}'
```

| platformSpecificData | What it does |
| --- | --- |
| `mediaUrl` / `mediaUrls` | Up to 4 images (JPEG, PNG, WebP or GIF, each ≤1,000,000 bytes) or **one** MP4 video (≤100 MB, ≤3 minutes). Images and a video cannot be mixed. Public https URLs, e.g. an uploaded media `publicUrl` |
| `altText` / `altTexts` | Alt text for the image(s) or the video (≤2000 characters each) |
| `threadItems` | A thread: up to 25 posts of `{content, mediaUrl?, mediaUrls?, altText?, altTexts?}`, each a reply to the previous one. Replaces the post text |
| `replyTo` | Reply to an existing post: its `at://` URI or a `https://bsky.app/profile/<handle>/post/<id>` link |
| `langs` | Up to 3 language tags, e.g. `["en"]` |
| `disableLinkPreview` | No link card |

Limits are checked when the post is created; file sizes, types and the video length are checked again when the files are fetched,
before anything is posted. The target's `platformPostId` is the post's `at://` URI and `url` its `https://bsky.app/profile/<handle>/post/<id>`
link. For a thread they are those of the first post; if a later post of the thread fails, the thread stops there and the target gets a
warning (the published posts are not repeated).

## Delete

| Method | Path | What it does |
| --- | --- | --- |
| POST | `/posts/{id}/unpublish` | Delete the post from Bluesky (for a thread: the first post) |
| GET | `/accounts/{id}/health` | Whether the session still works |

Deletes are queued for a person while approval applies to the API key, the organization or the account (202 `pending_approval`; MCP
`manage_published_post` returns the queued action). Bluesky posts cannot be edited.

## Bluesky's rules to know

- **Rate limits**: when Bluesky answers 429, Postgale retries after the reset time Bluesky gives.
- **Revoked app passwords**: an app password Bluesky refuses marks the account `needs_reconnect` (`reconnect_required` on the API) and the post is not retried automatically.
- **Analytics**: likes, reposts and views are not collected yet, so post and account analytics answer `not_supported`.
