Bluesky API
Postproxy publishes feed posts and post chains to Bluesky over the AT Protocol, supports text direct messages through the Bluesky chat service, and syncs replies to your posts as read-only comments.
Every request below uses the base URL https://api.postproxy.dev and an Authorization: Bearer YOUR_API_KEY header. Replace YOUR_API_KEY and the example IDs with your own.
Profiles and profile groups
Section titled “Profiles and profile groups”Every post targets one or more profiles. A profile is one connected account — for Bluesky, a Bluesky account. Reference one in a request by its id (the prof_abc123 in the examples below) or by the platform name "bluesky", which selects the group’s Bluesky profile (a group holds at most one profile per platform). List what’s connected with GET /api/profiles.
Profiles live in profile groups — containers that organize the accounts for one brand, client, or project. List groups with GET /api/profile_groups, and connect a new Bluesky profile with the Initialize Connection endpoint (Bluesky uses an app-password flow — see Connect Bluesky).
Example: get profiles and profile groups
List the profiles you can post to — GET /api/profiles:
curl -X GET "https://api.postproxy.dev/api/profiles" \ -H "Authorization: Bearer YOUR_API_KEY"{ "data": [ { "id": "prof_abc123", "name": "mycompany.bsky.social", "platform": "bluesky", "status": "active", "profile_group_id": "grp_xyz789", "expires_at": null, "post_count": 72, "avatar_url": "https://cdn.postproxy.dev/uploads/avatar_prof_abc123.jpg" } ]}List your profile groups — GET /api/profile_groups:
curl -X GET "https://api.postproxy.dev/api/profile_groups" \ -H "Authorization: Bearer YOUR_API_KEY"{ "data": [ { "id": "grp_xyz789", "name": "Main Brand", "profiles_count": 5 }, { "id": "grp_def456", "name": "Client Project", "profiles_count": 3 } ]}At a glance
Section titled “At a glance”| Platform ID | bluesky |
| Formats | post (default) |
| Character limit | 300 (graphemes) |
| Media | Optional |
| Comments | Read-only (synced replies) |
| Direct messages | Yes — text only |
| Post chains | Yes |
Publishing
Section titled “Publishing”Parameters
Section titled “Parameters”Bluesky has no custom parameters — pass an empty object (or omit it).
| Media | Max size | Formats | Count | Duration |
|---|---|---|---|---|
| Image | 1 MB | jpg, png, webp, gif | 4 | — |
| Video | 50 MB | mp4, mov | 1 | 1 s – 60 s |
- Text-only posts are allowed; media is optional.
- Images and video cannot be mixed.
- The 1 MB per-image limit is strict — compress images before uploading.
# Simple postcurl -X POST "https://api.postproxy.dev/api/posts" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "post": { "body": "Shipping a new feature today 🚀 https://example.com/launch" }, "profiles": ["prof_abc123"], "media": ["https://example.com/image.jpg"], "platforms": { "bluesky": {} } }'Post chains
Section titled “Post chains”Bluesky supports thread conversations. Pass a thread array of follow-up posts; each child can carry its own media.
# A 2-post threadcurl -X POST "https://api.postproxy.dev/api/posts" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "post": { "body": "1/ Quick thread on what we shipped 🧵" }, "profiles": ["prof_abc123"], "thread": [ { "body": "2/ Faster sync and a new webhook.", "media": ["https://example.com/shot.jpg"] } ] }'Comments
Section titled “Comments”Replies to your Bluesky posts are synced as comments and exposed read-only through the Comments API. Postproxy polls each post’s thread — roughly every 10 minutes while the post is fresh, tapering off as it ages — and walks nested replies up to 6 levels deep.
| Action | Supported |
|---|---|
| List / read | Yes |
| Reply | No |
| Delete | No |
| Hide / unhide | No |
| Like / unlike | No |
- Replies to the post itself have
parent_id: null; nested replies carry their parent comment’s ID. - Comment media comes through on
attachments— image embeds (multiple per comment), video thumbnails / HLS playlists, and external link-preview cards. - Ingestion is polled, so no
comment.createdwebhook fires for Bluesky — list the comments to pick up new ones.
# List comments on a postcurl "https://api.postproxy.dev/api/posts/post_abc123/comments?profile_id=prof_abc123" \ -H "Authorization: Bearer YOUR_API_KEY"Direct messages
Section titled “Direct messages”Bluesky DMs go through the AT Protocol chat service and are supported through the Direct Messages API, with some limits relative to the Meta networks.
| Capability | Supported |
|---|---|
| Send / receive text | Yes |
| Attachments | No |
| Reactions | No |
| Edit outbound message | No |
| Delivery / read receipts | No |
| Archive (mute) chat | Yes |
| Inbound delivery | Poller (~5 min) |
- No webhooks for inbound. A per-profile poller runs every ~5 minutes; new inbound messages still fire
message.received, with up to ~5 minutes of latency. - No messaging window, but the recipient must have DMs enabled and allow messages from you, or the send fails with a Bluesky-side error.
- Archive = mute — archive / unarchive map to
muteConvo/unmuteConvo. - Start a chat with the recipient’s DID as
participant_external_id.
# Create or find a chat by DIDcurl -X POST "https://api.postproxy.dev/api/profiles/prof_abc123/chats" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "participant_external_id": "did:plc:abcdefghijklmnopqrstuvwx" }'# Send a text message (Bluesky is text-only)curl -X POST "https://api.postproxy.dev/api/chats/chat_abc123/messages" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "body": "Thanks for following! Anything we can help with?" }'# Archive (mute) / unarchive a chatcurl -X POST "https://api.postproxy.dev/api/chats/chat_abc123/archive" \ -H "Authorization: Bearer YOUR_API_KEY"
curl -X DELETE "https://api.postproxy.dev/api/chats/chat_abc123/archive" \ -H "Authorization: Bearer YOUR_API_KEY"Postproxy records periodic stat snapshots you can pull as a timeseries; field names pass through from Bluesky unchanged.
Profile stats
Section titled “Profile stats”Account-level metrics, refreshed roughly every 23 hours via the Profile stats endpoint (GET /api/profiles/:id/stats).
Fields: followersCount, followsCount, postsCount
# Fetch the profile stats timeseriescurl "https://api.postproxy.dev/api/profiles/prof_abc123/stats" \ -H "Authorization: Bearer YOUR_API_KEY"Post stats
Section titled “Post stats”Bluesky does not report per-post stats.
Webhooks
Section titled “Webhooks”Subscribe with the Webhooks API:
curl -X POST "https://api.postproxy.dev/api/webhooks" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/webhooks/postproxy", "events": ["platform_post.published", "message.received"] }'Events relevant to Bluesky:
| Event | When |
|---|---|
post.processed | A post is ready to publish |
platform_post.published | A post was published to the platform |
platform_post.failed | A post failed to publish (retries exhausted) |
platform_post.failed_waiting_for_retry | A publish attempt failed; will retry |
platform_post.insights | New analytics snapshot |
message.received | Inbound DM (via the ~5 min poller) |
message.sent | An outbound DM was accepted by the platform |
message.failed_waiting_for_retry | An outbound DM failed; will retry |
message.failed | An outbound DM failed permanently |
profile.connected / .disconnected | Connection state changed |
profile.stats | New profile stats snapshot |
media.failed | A media attachment failed to process |
Bluesky does not expose delivery/read receipts, reactions, edits, or deletion events.
- The character limit is counted in graphemes — emoji and combining sequences count as one character.
- Images are capped at 1 MB each; plan to compress.