Bluesky
Publish and schedule Bluesky posts with text, links, hashtags, mentions and up to four images.
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.
- In the Bluesky app, open Settings → Privacy and security → App passwords and create one (it looks like
xxxx-xxxx-xxxx-xxxx). - Open Connections in the dashboard, or a connect link (
POST /api/v1/platform-inviteswith"platform": "bluesky", see 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
| 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
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
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
Checked before the post is stored (400, see 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). 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 |