Bluesky
#Overview
Post to Bluesky, and to any other server that speaks the AT Protocol. There is no OAuth dance: you create an app password in your own Bluesky settings and yeetpost trades it for a session. Delete the app password and every session made from it dies with it.
Bluesky posts are free to send. The account counts as one connected account, like LinkedIn or X. See pricing.
#How to connect
- In Bluesky, open Settings, then App Passwords, and create one. Never use your account password.
- In yeetpost, click Add connection, then Bluesky.
- Paste your handle (
you.bsky.social) and the app password (xxxx-xxxx-xxxx-xxxx). - Running your own PDS? Open On your own server? Set the service URL and give the
host. It has to be
https. The default ishttps://bsky.social.
Your first Bluesky connection gets the slug bluesky. A second account gets a slug built
from its handle, so alex.bsky.social becomes alexbskysocial_bluesky.
GET /connections lists yours, described as @alex.bsky.social (Bluesky).
The account is identified by its DID, not by the handle, so renaming your handle does not break the connection. Reconnecting the same account replaces the stored app password in place instead of adding a second connection.
#What works
| Thing | Value |
|---|---|
| Text limit | 300 graphemes, so a multi-codepoint emoji counts once |
| Images | Up to 4 per post, 1 MB each |
| Image formats | image/png, image/jpeg, image/gif, image/webp |
| Alt text | Yes, sent with each image |
| Threads | Yes, up to 24 replies under the head post |
| First comment | No, LinkedIn only |
| Scheduling | Yes |
| Permalink | https://bsky.app/profile/<handle>/post/<record key> |
| Per-post fee | None |
The 300 is checked before anything is published, and every segment of a thread is measured the same way, so a thread with one segment over the limit is refused whole rather than left half published.
A broken thread is not taken back. Bluesky has no rollback of its own, so if the
platform refuses the third reply, the head and the first two stay up. The item comes back
sent with threadCount at the replies that actually went out. Read the count rather
than assuming the whole thread landed.
Images ride on the head post. A thread with an image per segment is not something yeetpost does yet.
#Refusals
Every one of these is a per-item refusal: the other connections on the same post still go out. See the errors reference for the full vocabulary.
| Code | Status | When |
|---|---|---|
invalid_request | 400 | Text over 300 graphemes, in the post or in any thread segment |
media_unsupported | 400 | More than 4 images, or an image over 1 MB |
shape_unsupported | 400 | A firstComment was given. Bluesky has no first comment |
unauthorized | 401 | The app password was revoked. Create a new one and reconnect |
platform_rejected | 422 | The server said no, for example a duplicate post |
internal_server_error | 500 | Our fault. Quote the req_id when you report it |
#Post from curl
curl -X POST https://api.yeetpost.com/api/v2/posts \
-H "x-api-key: $YEETPOST_API_KEY" \
-H "content-type: application/json" \
-d '{
"text": "we rewrote the scheduler last month. three things we got wrong:",
"connectionSlugs": ["bluesky"],
"thread": [
"1. polling every second. the queue is a table, and postgres is fine with SKIP LOCKED.",
"2. retrying forever. five attempts and a dead letter beats an infinite loop."
]
}'#Post from Node
const response = await fetch("https://api.yeetpost.com/api/v2/posts", {
method: "POST",
headers: {
"x-api-key": process.env.YEETPOST_API_KEY,
"content-type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
text: "shipped the new api docs today",
connectionSlugs: ["bluesky"],
}),
});
const data = await response.json();
console.log(data.results[0].status, data.results[0].url);#FAQ
#Do I need my Bluesky account password?
No, and you should not use it. An app password only posts, it is created under Settings, App Passwords, and you can revoke it at any time without touching your account.
#Does a self-hosted PDS work?
Yes. Give the service URL when you connect. It has to be https and it has to be a public
host. Everything else, including the 300 grapheme limit and image handling, works the same
way.
#Why is my emoji-heavy post accepted when it looks longer than 300?
The limit is graphemes, not code points. A flag or a skin-tone emoji is several code points and one grapheme, so it counts once.
#My thread came back sent but only two replies are there. Why?
Bluesky cannot delete a half-published thread on our behalf, so a refused reply stops the
chain and leaves what already went out. threadCount says how many replies survived.
#Does an image over 1 MB fail the whole post?
No. Only the Bluesky item comes back failed with media_unsupported. Every other
connection on the same request still goes out.