Bluesky API (AT Protocol) for Developers, Measured Against Twitter (X) Data

By TwitterAPI.io Team•14 min read
Diagram comparing the Bluesky public AppView read path with reading X posts through TwitterAPI.io, with measured posts per request and bytes per post
Two read paths for the same organizations: Bluesky's unauthenticated AppView and X timelines via TwitterAPI.io (collected in 2026).

This page is about Bluesky's own developer API, the AT Protocol endpoints and streams, and how it sits next to X data in a monitoring stack. It is not another roundup of X (Twitter) data providers: for that, our guide to the best third-party Twitter APIs covers X-only options, and the social media API overview covers platform-by-platform access. Here you get the Bluesky specifics that the search results usually skim over: which host to call, when an app password is enough, the published rate limits, what Jetstream changes, and what nobody can charge you for.

Most teams searching for a Bluesky API already read X data and want to add a second network without doubling their work. So we measured that exact job. We pulled the recent posts of six organizations that publish on both networks, through Bluesky's public AppView and through TwitterAPI.io, on the same machine in the same hour, and compared request counts, payload sizes, latency, fields and engagement. Every number below comes from that study, collected in 2026.

One correction to a common assumption first: there is no such thing as a Bluesky API key. The question behind "bluesky api key" is really "how do I authenticate", and the answer depends on whether you are reading public data (no auth), running a bot (app password) or shipping an app that other people log into (OAuth).

01 — Section

What the Bluesky API actually is: XRPC methods, hosts and streams

Bluesky is built on the AT Protocol, and the Bluesky docs are explicit that there is not one centralized API. Data lives in user repositories on Personal Data Servers (PDS), a Relay aggregates repository updates for the whole network, and an AppView (the Bluesky App service at api.bsky.app) indexes that data into feeds, threads and profiles. Every HTTP call is an XRPC method named by a namespaced ID: app.bsky.* for Bluesky app data, com.atproto.* for protocol-level repository and identity operations, chat.bsky.* for DMs.

The API Hosts and Auth page in the Bluesky docs (docs.bsky.app/docs/advanced-guides/api-directory, which now redirects to bsky.network/docs/api-directory) sorts requests into a few patterns. These are the ones you will use:

Public reads. Many app.bsky.* endpoints need no authentication and should be sent to https://public.api.bsky.app, which adds caching. Profiles (app.bsky.actor.getProfile), author feeds (app.bsky.feed.getAuthorFeed) and threads all work here.

Authenticated app requests. Calls that need a user context go through the user's PDS, which proxies them to the right service using the atproto-proxy header. The docs note it is fine to send even public requests this way once you have a session.

Writes. Every post, like, follow or profile change is a record write to the user's repository on their PDS. It always needs authentication.

Streams. The firehose (com.atproto.sync.subscribeRepos) is a WebSocket of every repository change, from a single PDS or from a Relay for the whole network. Jetstream is the filtered JSON version of the same data.

The practical consequence: for a read-only monitoring job you can start without an account at all. For anything that writes, or for search (more on that below), you need a session.

02 — Section

Bluesky API key: app passwords, sessions and OAuth

Bluesky issues no developer key and has no developer portal sign-up. Authentication is per account, and the docs give two routes.

App passwords for bots and scripts. The getting-started guide logs in with a handle and an app password (created under Settings, App passwords in the Bluesky app) against https://bsky.social, using com.atproto.server.createSession. The response carries two tokens: accessJwt, which authenticates requests and expires after a few minutes, and refreshJwt, which lasts longer and is used only to get a new access token. The official SDKs store the session and refresh it for you. The docs say to use an app password rather than your main password.

OAuth for apps with their own login. The OAuth page states that applications with their own end-user login flow should implement OAuth, while single-purpose applications such as bots or command-line tools may use app password authentication instead. atproto OAuth requires DPoP, PKCE and PAR, so the docs point most developers to an SDK rather than a hand-built client, with tutorials for Next.js, Node and Go.

Service auth between services. When a PDS calls another service on a user's behalf (the video upload flow is the docs' example), it mints a short-lived JWT signed with the user's DID signing key, normally valid for under 60 seconds. You only touch this if you run your own service.

Compare that with X. The official X API requires a developer account, an app and a bearer token or OAuth credentials, and bills per resource. TwitterAPI.io reads use one X-API-Key header and need no X account. Bluesky sits at the other end: reads need nothing, and writes need an account you control.

03 — Section

Bluesky API rate limits, from the Bluesky docs

Bluesky publishes its limits per service, and warns that other providers on the open network may use different numbers. Requests over a limit usually get HTTP 429, and many services return rate-limit headers you can use for backoff.

Bluesky limitScopeValue
Record writes (create 3 pts, update 2, delete 1)per account5,000 points/hour, 35,000 points/day
All API requests to a Bluesky PDSper IP3,000 per 5 minutes
com.atproto.server.createSession (log-ins)per account30 per 5 minutes, 300 per day
Blob upload sizeper upload52,428,800 bytes (50 MB)
api.bsky.app / public.api.bsky.appunauthenticatedno number published, described as generous

The write budget is the one bots hit. A like or a post is a CREATE at 3 points, so one account can create at most about 1,666 records an hour and 11,666 a day. The docs add that bulk or spammy interactions break the Community Guidelines regardless of the point budget, and that moderation and app-specific limits can apply on top.

Two more numbers matter if you run infrastructure. The Bluesky Relay admits new self-hosted PDS instances at 50 repository events per second, 2,600 per hour and 21,000 per day, with a 100-account maximum by default. Labeling services (Ozone moderation) are capped at 5 per second, 10,000 per hour and 100,000 per day.

For comparison, the TwitterAPI.io read side is a request-rate question, not a write-points one: 0.2 requests per second before the first payment, 20 once paid, 50 on Scale, 500 on Business and up to 1,000 on request.

04 — Section

Firehose vs Jetstream: real-time Bluesky data without polling

Bluesky's biggest difference from X for monitoring work is that the whole network's public activity is a stream anyone can open. There are two ways in.

The firehose. com.atproto.sync.subscribeRepos on a Relay, for example wss://relay1.us-east.bsky.network/xrpc/com.atproto.sync.subscribeRepos, needs no authentication. Messages are binary CBOR with CAR slices of repository data, so you need an SDK to decode them (the docs call the Go SDK, indigo, the most feature-complete). Every event carries a sequence cursor; reconnect with ?cursor=<seq> to resume, but Relays keep only a limited backlog. For durable per-repo cursors the docs recommend Tap.

Jetstream. Jetstream consumes the firehose and serves plain JSON over one WebSocket, filtered server-side. The docs list v2 instances at wss://jetstream.us-west.bsky.network and wss://jetstream.us-east.bsky.network (recommended for new projects) and v1 at jetstream1.us-west and jetstream2.us-east. Three filters combine: collections (for example app.bsky.feed.post), dids (specific accounts) and kinds (commit, identity, account, sync). One subscription accepts up to 100 collections and 10,000 DIDs; going over is rejected before the WebSocket upgrade. The live tail needs no auth.

Resuming and history. Jetstream events carry a seq; reconnect with cursor=N and delivery is at-least-once and inclusive, so key your handler on each record's at:// URI. Network Replay serves history over HTTP and cuts over to live, using a few authenticated HTTP calls.

Jetstream is the right tool when you want every post from a list of accounts, or every post on the network that you will filter yourself. It is not a keyword search: there is no server-side text filter among the three filters, so matching a phrase means receiving all posts in the collection and matching client-side.

python
# pip install websockets
import asyncio, json, websockets

URL = ("wss://jetstream.us-east.bsky.network/xrpc/network.bsky.jetstream.subscribeEvents"
       "?collections=app.bsky.feed.post&kinds=commit"
       "&dids=did:plc:REPLACE_WITH_ACCOUNT_DID")

async def main():
    async with websockets.connect(URL) as ws:
        async for msg in ws:
            evt = json.loads(msg)
            p = evt.get("payload", {})
            if p.get("operation") == "create":
                rec = p.get("record", {})
                print(p["did"], p.get("seq"), rec.get("createdAt"), rec.get("text", "")[:80])

asyncio.run(main())
05 — Section

Bluesky API pricing vs X API pricing

Searches for "bluesky api pricing" usually expect a rate card. There is none to find: none of the Bluesky docs pages we read this session (getting started, API hosts, rate limits, OAuth, firehose, Jetstream) mentions a fee, a paid tier or a key, and the public AppView and Jetstream live tail require no authentication. What you pay for is your own compute and bandwidth, plus whatever it costs to store the stream.

X is the opposite. The official X API pay-per-usage rate card at docs.x.com/x-api/getting-started/pricing charges $0.005 per post read, $0.010 per user read and $0.010 per follower or following read, with $0.015 per post created ($0.20 with a URL). Pay-per-usage plans are capped at 3 million post reads per monthly billing cycle, with Enterprise above that, and new accounts get $20 in credits when they save a first card and buy credits, plus a match of the first auto-recharge up to $50.

Read pathPriceRate card
Bluesky AppView, Jetstream, firehoseno fee listed in the docs we readdocs.bsky.app / bsky.network docs
Official X API, post reads$0.005 per post ($5 per 1,000)docs.x.com pay-per-usage
Official X API, user reads$0.010 per userdocs.x.com pay-per-usage
TwitterAPI.io, tweets15 credits per tweet ($0.15 per 1,000)twitterapi.io/pricing
TwitterAPI.io, profiles18 credits per profile ($0.18 per 1,000)twitterapi.io/pricing

In our study the X half (500 timeline posts, 20 search results and 6 profiles) cost 7,908 credits, or $0.0791, on TwitterAPI.io. The same reads priced on the official X API rate card come to $2.66. Per 1,000 tweets that is $0.15 against $5, about 33x. New TwitterAPI.io accounts that pass the automatic bot check get up to $1 of free credits (at most 100,000 credits, no card, one per person or network, expiring after 31 days); purchased credits never expire, and subscriptions run from Starter at $29 a month to Business at $1,499 a month.

06 — Section

Measured: the same organizations on Bluesky and X

We took six organizations with verified domain handles on Bluesky and the matching X accounts: Bluesky itself (bsky.app and @bluesky), The New York Times, The Verge, NPR, The Washington Post and WIRED. For each, we fetched the profile and the most recent posts on both networks, sequentially from one machine.

MeasureBluesky public AppViewX via TwitterAPI.io
Endpoint usedapp.bsky.feed.getAuthorFeed/twitter/user/last_tweets
AuthnoneX-API-Key header
Posts per feed page100 (we asked for limit=100)20
Requests in the study1333
Posts collected598500
Median / p90 feed latency2.7 s / 3.5 s3.2 s / 3.8 s
Response size per post (median)2.6 KB10.4 KB
Top-level fields per post1230
Data cost of the study$0$0.0791 (7,908 credits)

Three things stand out. First, Bluesky hands back five times as many posts per page, so reading 100 recent posts takes one request instead of five. Second, an X post object through TwitterAPI.io is about four times larger, because it embeds the author object, viewCount, bookmarkCount, card and media variants; Bluesky's post view has 12 top-level fields and no view count at all. Third, latency was close: median 2.7 s for a Bluesky feed page and 3.2 s for an X page from our machine, timed after our own rate limiter released each call.

The @bluesky pair is left out of the ratios below: our collection run did not capture its X timeline, and a follow-up from:bluesky search returned only 20 posts spread over 182 days, so the Bluesky team posts on X only occasionally. We left it out rather than mix in a different endpoint.

Four bar panels comparing Bluesky getAuthorFeed and TwitterAPI.io last_tweets: posts per page, KB per post, top-level fields per post and median latency
13 Bluesky requests vs 33 TwitterAPI.io requests for the same organizations, one machine, collected in 2026.
07 — Section

Audience size vs engagement: why monitoring needs both networks

Follower counts make Bluesky look like a rounding error next to X for these publishers. Engagement per post says otherwise.

OrganizationBluesky followers as % of XBluesky posts/dayX posts/dayMedian likes, Bluesky vs X
The New York Times2.5%34.818.862 vs 343
The Verge8.0%24.323.122 vs 28
NPR13.2%25.57.7117.5 vs 252.5
The Washington Post4.5%110.653.036 vs 51
WIRED4.7%22.321.978 vs 19

Across the five news organizations, Bluesky follower counts are a median 4.7% of X follower counts, but the median Bluesky post drew 71% as many likes as the median X post (median of the five per-organization ratios). WIRED's median Bluesky post drew more likes than its median X post (78 vs 19). All five posted at least as often on Bluesky as on X in the sampled window, and The New York Times, NPR and The Washington Post about twice as often or more, even though some X timelines carry more reposts: 36% of The New York Times' sampled X timeline items were reposts, against 13% on Bluesky.

For a monitoring product, that means a Bluesky-only or X-only feed misses real conversation. Bluesky also does not expose impressions, so if your reporting needs reach, only the X side (viewCount) can supply it.

Horizontal bar chart of median likes per original post on Bluesky and X for five news organizations, with Bluesky followers as a percent of X followers
Median likes per original post, Bluesky vs X, with Bluesky followers as a share of X followers (five news organizations, collected in 2026).
08 — Section

Keyword search: the gap in the public Bluesky API

Reading a known account is free and keyless on Bluesky. Searching is not. Our unauthenticated call to app.bsky.feed.searchPosts on public.api.bsky.app came back HTTP 403 with an HTML Forbidden page, while getProfile and getAuthorFeed on the same host answered normally. In practice, Bluesky keyword search means creating a session (app password or OAuth) and sending the request through your PDS, as the API Hosts page describes for authenticated app.bsky.* calls, or consuming Jetstream and matching text yourself.

On X, keyword search is a single read call. TwitterAPI.io's /twitter/tweet/advanced_search takes X search operators ("bluesky api" -is:retweet, from:, since_time:, lang:), returns top-level tweets[] with has_next_page and next_cursor, and bills 15 credits per tweet returned, with a 15-credit minimum when nothing matches. A poll that finds nothing costs $0.00015, so checking every 60 seconds for a month (43,200 checks) has a floor of $6.48, and every 5 minutes about $1.30.

A multi-network monitor therefore splits naturally: Jetstream with a dids filter (or getAuthorFeed polling) for known Bluesky accounts, authenticated searchPosts or client-side matching for Bluesky keywords, and advanced_search or last_tweets for X.

09 — Section

Posting to both networks, and how we collected this

Writing to Bluesky. After createSession, a post is a record created on your repository (com.atproto.repo.createRecord with an app.bsky.feed.post record); the response returns the post's at:// URI and CID. Each create spends 3 of the account's 5,000 hourly write points.

Writing to X through TwitterAPI.io. Writes run through an authenticated session: one /twitter/user_login_v2 login ($0.10, with a proxy you supply) returns login_cookies, and each post, delete, like, retweet or follow (/twitter/create_tweet_v2, /twitter/follow_user_v2, ...) is then 60 credits, $0.0006, billed per HTTP 200 call. The call acts as that account, so X can rate-limit, lock or suspend it; keep a human pace, and expect the session to need a fresh login at times. Reads need no X account.

How we collected this. Question: what does it take to read the same organizations' recent posts on Bluesky (public AppView) and on X (TwitterAPI.io), and how do audience and engagement compare? Accounts: bsky.app, nytimes.com, theverge.com, npr.org, washingtonpost.com, wired.com on Bluesky; @bluesky, @nytimes, @verge, @NPR, @washingtonpost, @WIRED on X. Window: each account's most recent posts at collection time, collected in 2026. Sample: 598 Bluesky posts in 13 requests (6 profiles, 6 feed pages of limit=100 with filter=posts_no_replies, 1 search probe) and 500 X posts plus 20 search results in 33 requests (6 profiles, 26 last_tweets pages, 1 from:bluesky search). Latency is network time measured after our rate limiter released each call, from a single machine; it is not a benchmark of either service.

What the sample cannot show. Six organizations, mostly news publishers, are not the network. Likes on a recent post are still growing, and the two windows differ in length per account. Follower counts include inactive accounts on both networks, and bsky.app's Bluesky follower count (over 35 million) is not comparable to anything on X, which is one reason that pair is excluded from the ratios.

Privacy. Organization accounts only, aggregates only. Raw pages embed other people's reposts and stayed out of the published study; the committed sample has engagement counts and lengths, no IDs, URLs, text or handles.

Next step. Get a key on the dashboard, run the code below against one organization you follow on both networks, and check the request shapes in the TwitterAPI.io docs before you scale it up.

python
import time
import requests

TWAPI = "https://api.twitterapi.io"
TWAPI_KEY = "YOUR_TWITTERAPI_IO_KEY"          # from twitterapi.io/dashboard
BSKY = "https://public.api.bsky.app/xrpc"     # Bluesky public AppView, no key


def bluesky_posts(handle, limit=100):
    """Recent posts by one Bluesky account (no auth). Returns a common shape."""
    r = requests.get(f"{BSKY}/app.bsky.feed.getAuthorFeed",
                     params={"actor": handle, "limit": limit, "filter": "posts_no_replies"},
                     timeout=30)
    r.raise_for_status()
    out = []
    for item in r.json().get("feed", []):
        post = item["post"]
        is_repost = (item.get("reason") or {}).get("$type", "").endswith("reasonRepost")
        out.append({
            "network": "bluesky",
            "id": post["uri"],                       # at://did:plc:.../app.bsky.feed.post/<rkey>
            "created_at": post["record"].get("createdAt"),
            "text": post["record"].get("text", ""),
            "likes": post.get("likeCount", 0),
            "reposts": post.get("repostCount", 0),
            "replies": post.get("replyCount", 0),
            "views": None,                           # Bluesky exposes no view count
            "is_repost": is_repost,
        })
    return out


def x_posts(user_name, pages=5):
    """Recent posts by one X account via TwitterAPI.io (20 per page, newest first)."""
    out, cursor = [], ""
    for _ in range(pages):
        params = {"userName": user_name}
        if cursor:
            params["cursor"] = cursor
        r = requests.get(f"{TWAPI}/twitter/user/last_tweets", params=params,
                         headers={"X-API-Key": TWAPI_KEY}, timeout=30)
        r.raise_for_status()
        body = r.json()
        for t in (body.get("data") or {}).get("tweets") or []:
            out.append({
                "network": "x",
                "id": t["id"],
                "created_at": t["createdAt"],
                "text": t.get("text", ""),
                "likes": t.get("likeCount", 0),
                "reposts": t.get("retweetCount", 0),
                "replies": t.get("replyCount", 0),
                "views": t.get("viewCount"),
                "is_repost": bool(t.get("retweeted_tweet")),
            })
        if not body.get("has_next_page") or not body.get("next_cursor"):
            break
        cursor = body["next_cursor"]
        time.sleep(0.2)
    return out


def x_keyword(query, max_pages=3):
    """Keyword search on X (Bluesky's searchPosts needs an authenticated session)."""
    out, cursor = [], ""
    for _ in range(max_pages):
        params = {"query": query, "queryType": "Latest"}
        if cursor:
            params["cursor"] = cursor
        r = requests.get(f"{TWAPI}/twitter/tweet/advanced_search", params=params,
                         headers={"X-API-Key": TWAPI_KEY}, timeout=30)
        r.raise_for_status()
        body = r.json()
        out.extend(body.get("tweets", []))
        if not body.get("has_next_page") or not body.get("next_cursor"):
            break
        cursor = body["next_cursor"]
    return out


if __name__ == "__main__":
    feed = bluesky_posts("npr.org") + x_posts("NPR")
    for net in ("bluesky", "x"):
        posts = [p for p in feed if p["network"] == net and not p["is_repost"]]
        likes = sorted(p["likes"] for p in posts)
        print(net, len(posts), "original posts, median likes", likes[len(likes) // 2] if likes else None)
    hits = x_keyword('"bluesky api" -is:retweet', max_pages=1)
    print("X keyword hits on page 1:", len(hits))
    # Cost on the X side: 15 credits per tweet returned ($0.15 per 1,000);
    # 100 timeline posts = 1,500 credits = $0.015. The Bluesky calls need no key.
10 — Questions

Questions readers ask

Does Bluesky have an API key?

No. Public reads on public.api.bsky.app need no authentication. Bots and scripts log in with a handle and an app password via com.atproto.server.createSession, which returns a short-lived accessJwt and a longer refreshJwt. Apps with their own end-user login should use atproto OAuth.

Is the Bluesky API free?

None of the Bluesky docs pages we read lists a fee or a paid tier, and the public AppView, firehose and Jetstream live tail need no authentication. Usage is governed by rate limits instead, such as 5,000 write points per hour per account and 3,000 requests per 5 minutes per IP at a Bluesky PDS.

What are the Bluesky API rate limits?

Per account: 5,000 write points per hour and 35,000 per day (create 3, update 2, delete 1), and 30 createSession calls per 5 minutes or 300 per day. Per IP at a Bluesky PDS: 3,000 requests per 5 minutes. The public AppView has no published number; the docs call its limits generous and ask developers to get in touch if they hit them.

What is the difference between the Bluesky firehose and Jetstream?

The firehose (com.atproto.sync.subscribeRepos) streams every repository change as binary CBOR with CAR data and suits apps that need full network proofs. Jetstream reads the firehose and serves filtered JSON over one WebSocket, with server-side filters for collections, DIDs and event kinds (up to 100 collections and 10,000 DIDs per subscription).

Can I search Bluesky posts without logging in?

In our test, no: app.bsky.feed.searchPosts on public.api.bsky.app returned HTTP 403 without auth, while profile and author-feed calls worked. Create a session first, or stream posts from Jetstream and match keywords yourself.

What is the AT Protocol API?

The AT Protocol (atproto) is the open protocol Bluesky runs on. Its API is a set of XRPC methods: com.atproto.* for repositories, identity and sync, and app.bsky.* for the Bluesky app's feeds, profiles and graph. Data lives on Personal Data Servers, a Relay aggregates it, and an AppView indexes it for reads.

How does the Bluesky API compare with the X API for monitoring?

Bluesky reads are keyless and return up to 100 posts per author-feed page in our test, but there are no view counts and search needs auth. On X, the official API charges $0.005 per post read; TwitterAPI.io charges $0.15 per 1,000 tweets, returns 20 per page with viewCount, and supports keyword search in one call. Our X reads for this study cost $0.0791.

11 — Further reading

Continue

Sources & further reading
More from this series
Build it

Stop reading. Start building.

Starter credits cover real testing on real data. Google sign-in, no card, no application queue.

Get an API key

Already building with TwitterAPI.io? Publish an honest tutorial or build log about it and earn up to $200 a month in API credits.