Bluesky API: Access, Auth, Posting, and Rate Limits
The Bluesky API is the open AT Protocol. There's no developer app to register, no review, and no API key: you sign in with an app password (createSession) or OAuth, then post with com.atproto.repo.createRecord. Each account can spend 5,000 write points an hour, and a post costs 3. Adeli runs the OAuth flow and publishes Bluesky posts through the same endpoint as X, Instagram, and TikTok.
The Bluesky API is the AT Protocol (atproto): an open protocol where every account is a data repository and a post is a record you write into it. There's no developer application, no review, and no fee. You authenticate as the account, with an app password or with OAuth, and create a post by calling com.atproto.repo.createRecord with an app.bsky.feed.post record.
Below is how access, auth, posting, Python, and rate limits work, all from Bluesky's developer docs, plus how Adeli's Bluesky support works.
Is the Bluesky API free?
Yes. Bluesky doesn't sell API access, and there is no paid tier or per-request price in its developer docs. That's also how Adeli's API docs treat it: "Bluesky's API is free: nothing is billed per post."
Compare that with X, which bills every post at its published rates. On Bluesky, rate limits are the only cap on how much you post.
How do I get Bluesky API access?
You already have it if you have a Bluesky account. There is no developer portal sign-up and no app approval. The access you get comes with conditions:
- The developer guidelines apply. Bluesky's developer guidelines ban spam, including "automated or bulk interactions" that notify users, and require apps with user-generated content to offer reporting and blocking.
- Deletion requests must be honored. All services must have a way to delete content a user asks to have deleted.
- Bluesky can cut access. Failing to respond to known violations "may result in suspension of access" to Bluesky's infrastructure.
How does Bluesky API authentication work?
Bluesky has two ways to sign in, and neither uses an API key. Bluesky's OAuth page says apps with their own end-user login should implement OAuth, while "bots or command-line tools may use app password authentication instead."
App password + createSession |
AT Protocol OAuth | |
|---|---|---|
| Best for | Bots, scripts, your own account | Apps where other people sign in |
| What the user gives you | Handle and an app password | Consent on their server's sign-in page |
| How your app is identified | It isn't; you act as the account | client_id is a URL to a public metadata document |
| Tokens | accessJwt (expires after a few minutes) and refreshJwt |
Access tokens under 30 minutes, plus refresh tokens |
| Extra requirements | None | PKCE, PAR, and DPoP on every client |
Sources: Bluesky API get started and the atproto OAuth spec.
App passwords are created in the Bluesky app's settings and are separate from your main password. Bluesky's docs tell you to use one "rather than your main account password" when you call createSession.
OAuth has no client registration. The atproto spec says the client_id "is a fully-qualified web URL pointing to the public client metadata." PKCE, PAR, and DPoP are all mandatory, and refresh tokens for public clients should last at most 2 weeks. Bluesky recommends an SDK that handles all of this rather than a hand-built client.
Not every account lives on bsky.social. A user on a self-hosted server signs in at that server, so your login has to resolve the handle to the right host.
How do I post with the Bluesky API?
Create a session, then write an app.bsky.feed.post record with createRecord. A post needs two fields, text and createdAt. This is the curl flow from Bluesky's get started guide:
- Create a session. POST your handle and app password to your PDS host (for most accounts,
https://bsky.social):
curl -X POST $PDSHOST/xrpc/com.atproto.server.createSession \
-H "Content-Type: application/json" \
-d '{"identifier": "'"$BLUESKY_HANDLE"'", "password": "'"$BLUESKY_PASSWORD"'"}'- Copy
accessJwtfrom the response. You send it as a bearer token. KeeprefreshJwtto get a new access token when it expires. - Create the post record:
curl -X POST $PDSHOST/xrpc/com.atproto.repo.createRecord \
-H "Authorization: Bearer $ACCESS_JWT" \
-H "Content-Type: application/json" \
-d "{\"repo\": \"$BLUESKY_HANDLE\", \"collection\": \"app.bsky.feed.post\", \"record\": {\"text\": \"Hello world! I posted this via the API.\", \"createdAt\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}}"- Save the response. It returns the post's
uri(anat://URI) andcid, a hash of the content. You need the URI to delete the post or reply to it.
Links and mentions don't turn into links on their own. They're facets, annotations that point at byte ranges of the text, and Bluesky's creating a post guide shows how its SDK's RichText detects them for you.
How do I use the Bluesky API with Python?
Use the atproto package. It's the Python SDK Bluesky's docs use, and it handles the session for you:
# pip install atproto
from atproto import Client
client = Client()
client.login("handle.example.com", "your-app-password")
post = client.send_post("Hello world! I posted this via the API.")
print(post.uri) # at://did:plc:.../app.bsky.feed.post/...
print(post.cid)To tag the post's language, pass langs, for example client.send_post("Hello", langs=["en-US"]). Bluesky says this helps custom feeds filter posts. TypeScript (@bsky/sdk) and Go (indigo) SDKs follow the same pattern.
What are the Bluesky API rate limits?
Writes are limited per account with a points system: 5,000 points an hour and 35,000 a day. Creating a record costs 3 points, updating 2, and deleting 1, so one account can create at most 1,666 records an hour and 11,666 a day. Likes and follows count as records too.
| Limit | Value | Measured per |
|---|---|---|
| Record writes | 5,000 points/hour, 35,000 points/day | Account |
| All API requests to a Bluesky PDS | 3,000 per 5 minutes | IP |
createSession |
30 per 5 minutes, 300 per day | Account |
updateHandle |
10 per 5 minutes, 50 per day | Account |
| Blob upload size | 50 MB per blob | Upload |
Source: Bluesky rate limits, fetched 2026-10-12. Requests over a limit usually get HTTP 429, and many responses carry rate-limit headers you can use for backoff.
Two practical notes:
- Reuse sessions. At 300
createSessioncalls a day, a script that logs in before every post will run out. Refresh withrefreshJwtinstead. - Other servers set their own limits. These numbers are for Bluesky-run hosts. Accounts on self-hosted servers follow whatever their operator sets.
How does Adeli's Bluesky API work?
Adeli connects your users' Bluesky accounts with AT Protocol OAuth and publishes through the same POST /api/v1/posts endpoint it uses for every network. Bluesky is live on Adeli, with connect and publish only: there are no Bluesky comments, messages, or analytics endpoints yet.
What Adeli covers, from the Bluesky publish docs and connect docs:
- Connect. Your server starts a connect session (
POST /api/v1/profiles/{profileId}/connectwithplatform: "bluesky") and sends your user to the returned URL. For an account on a self-hosted server, pass itshandleand sign-in starts at that server. - Publish. Text up to 300 characters, with up to 4 images (JPEG, PNG, or WebP) or one MP4 up to 100 MB. Images over 2 MB are resized to fit Bluesky's limit, and the response lists them in
resizedImages. - Threads. Send
threadwith up to 24 more posts and Adeli publishes each as a reply to the one before. A thread that fails part-way returns207withstatus: "partial". - Delete.
DELETE /api/v1/posts/bluesky/{postId}takes the record key or the full AT URI (delete docs). - List. The 10 most recent top-level posts per account, with engagement counts.
A publish call looks like this:
curl --fail-with-body -X POST "$ADELI_URL/api/v1/posts" \
-H "Authorization: Bearer $ADELI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"platform": "bluesky",
"accountId": "00000000-0000-0000-0000-000000000000",
"text": "Herons fish at dawn. A thread:",
"thread": [{ "text": "They stand still for minutes at a time." }]
}'Self-hosted accounts shape the connect flow. Most Bluesky accounts sign in at bsky.social, but an account on another server won't be found there, which is why Adeli takes an optional handle. The consent screen also asks for broad access, because Bluesky's permissions aren't finer-grained than that today. Adeli uses it to publish, list, and delete posts, and doesn't read direct messages.
When should you use Adeli instead of the Bluesky API directly?
Call Bluesky directly if Bluesky is the only network and the accounts are yours. An app password and the atproto package get a bot posting in minutes, at no cost.
Use Adeli's Bluesky API when your customers connect their own accounts, or when Bluesky is one of several networks. You skip building DPoP-bound OAuth and handle resolution, and the same request shape reaches X, Instagram, TikTok, YouTube, and Facebook through the social media posting API. Bluesky's API is free, so there's no per-post fee: one Bluesky account is one connected account on Adeli's pricing, and your first 3 are free.