# Bluesky (/docs/platforms/bluesky)



Connecting an account [#connecting-an-account]

Bluesky connects with the account's handle and an **app password**, not with an OAuth redirect and never with the account password.

1. In the Bluesky app, open **Settings → Privacy and security → App passwords** and create one (it looks like `xxxx-xxxx-xxxx-xxxx`).
2. Open **Connections** in the dashboard, or a connect link (`POST /api/v1/platform-invites` with `"platform": "bluesky"`, see [Connect links](/docs/guides/connect-links)), and enter the handle and app password.

FeedHorizon stores the app password encrypted and uses it to renew the session, so the connection keeps working until the user revokes the app password. When that happens the account turns `needs_reauth` and the `account.needs_reauth` webhook is sent. Accounts on self-hosted servers (PDS) work too.

The platform id in the API is `bluesky`.

What you can post [#what-you-can-post]

|                | Limit                  |
| -------------- | ---------------------- |
| Text           | 300 characters         |
| Files per post | up to 4 files, or none |
| Images         | JPEG, PNG, WebP or GIF |
| Videos         | not supported          |
| File size      | 1 MB per file          |

Links, `#hashtags` and `@handle.bsky.social` mentions in the text become clickable automatically. A mention of a handle that does not exist stays plain text.

Example [#example]

```bash title="cURL"
curl -X POST https://feedhorizon.com/api/v1/posts \
  -H "Authorization: Bearer fh_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "profileId": "prof_…",
    "content": "Shipping Bluesky support today #buildinpublic https://feedhorizon.com",
    "targets": [{ "accountId": "acc_…" }],
    "scheduledFor": "2026-11-10T09:00:00+01:00"
  }'
```

After publishing [#after-publishing]

The Bluesky target carries the post's AT URI in `platformPostId` (`at://did:plc:…/app.bsky.feed.post/<id>`) and the link in `platformUrl` (`https://bsky.app/profile/<did>/post/<id>`).

Errors [#errors]

Checked before the post is stored (`400`, see [Errors](/docs/api/errors)):

| Code                     | Cause                           | Fix                                                         |
| ------------------------ | ------------------------------- | ----------------------------------------------------------- |
| `content_too_long`       | More than 300 characters        | Shorten the text, or give Bluesky its own text in `targets` |
| `too_many_media`         | More than 4 images              | Remove images                                               |
| `media_too_large`        | An image is larger than 1 MB    | Compress or resize it before uploading                      |
| `media_type_unsupported` | A video or other non-image file | Post images only for now                                    |

While publishing (in `targets[].error`, see [Publishing errors](/docs/api/errors#publishing-errors)). Rate limits and outages are retried automatically:

| Code                   | Cause                                                          | Fix                                           |
| ---------------------- | -------------------------------------------------------------- | --------------------------------------------- |
| `account_needs_reauth` | The app password was revoked                                   | Reconnect with a new app password, then retry |
| `platform_rejected`    | Bluesky refused the post. `error.message` has Bluesky's reason | Change the content, then retry                |
