"Bluesky API" in search logs is often someone who wants posts and follower counts, not a full AppView client. These two routes cover that read. They are SocialScrape endpoints.
They are not the Bluesky protocol docs, and they do not issue a Bluesky session. You authenticate with your SocialScrape key.
The key goes in x-api-key. See authentication.
Profile
GET /v1/bluesky/profile takes handle. That value is a handle (alice.bsky.social) or a DID (did:plc:...). username is an alias. The parameter is required.
The recorded request uses the bsky.app handle.
1curl -G "https://api.socialscrape.dev/v1/bluesky/profile" \2 -H "x-api-key: $SOCIALSCRAPE_API_KEY" \3 --data-urlencode "handle=bsky.app"1import os2import requests34params = {5 "handle": "bsky.app",6}7res = requests.get(8 "https://api.socialscrape.dev/v1/bluesky/profile",9 headers={"x-api-key": os.environ["SOCIALSCRAPE_API_KEY"]},10 params=params,11)12body = res.json()1314if body["ok"]:15 print(body["data"])16else:17 print(body["error"]["code"], body["error"]["message"])1const url = new URL("https://api.socialscrape.dev/v1/bluesky/profile");2url.search = new URLSearchParams({3 "handle": "bsky.app"4}).toString();5const res = await fetch(url, {6 headers: { "x-api-key": process.env.SOCIALSCRAPE_API_KEY! },7});8const body = await res.json();910if (body.ok) {11 console.log(body.data);12} else {13 console.error(body.error.code, body.error.message);14}data- is a profile.
platformisbluesky. You getid,handle,name,bio,avatar_url,banner_url,url,verified,is_private,external_urlandlinks.created_atis set when the profile has one. stats.followers,stats.followingandstats.posts- are the counts.
stats.likesandstats.viewsare null when the profile has no such totals. extra- on the recorded profile includes
did,associated,indexed_at,pinned_post_uri,statusandverification. Keepextra.didif you will call the posts endpoint next. The reference says a DID is the faster lookup.user_idis an alias ofhandleon the posts route, so you can pass the DID inhandle(or asuser_id) without inventing a second parameter.
Errors are invalid_params and not_found. An unknown handle is not_found. Do not retry it.
Sample. The recorded profile is shortened. Follower counts in that file are the sample, not a number to publish as current.
Posts
GET /v1/bluesky/profile/posts is a paged feed of that user's posts. handle is required, same shape as the profile call. cursor pages forward. Changing nothing but the cursor continues the same feed.
1curl -G "https://api.socialscrape.dev/v1/bluesky/profile/posts" \2 -H "x-api-key: $SOCIALSCRAPE_API_KEY" \3 --data-urlencode "handle=bsky.app"1import os2import requests34params = {5 "handle": "bsky.app",6}7res = requests.get(8 "https://api.socialscrape.dev/v1/bluesky/profile/posts",9 headers={"x-api-key": os.environ["SOCIALSCRAPE_API_KEY"]},10 params=params,11)12body = res.json()1314if body["ok"]:15 print(body["data"])16else:17 print(body["error"]["code"], body["error"]["message"])1const url = new URL("https://api.socialscrape.dev/v1/bluesky/profile/posts");2url.search = new URLSearchParams({3 "handle": "bsky.app"4}).toString();5const res = await fetch(url, {6 headers: { "x-api-key": process.env.SOCIALSCRAPE_API_KEY! },7});8const body = await res.json();910if (body.ok) {11 console.log(body.data);12} else {13 console.error(body.error.code, body.error.message);14}data.items- are posts. Each has
id,url,text,published_at,author,media,hashtags,mentionsandstats.stats.likesandstats.commentsare filled when Bluesky sends them.stats.viewsis commonly null.stats.sharesis the shared field. Bluesky's own counters also land inextra:repost_count,quote_count,cid,uriandimage_alts. Useextra.uriandextra.cidif you store the AT Protocol identity of the post.urlis the bsky.app link. cursorandhas_more- tell you whether to continue. Send the cursor back unchanged. Stop when
has_moreis false. Each page costs 1 credit. Pagination describes the loop.parentis the profile the feed belongs to.
A single post, including embed content and threaded replies, is GET /v1/bluesky/post with the post URL (bsky.app/profile/<handle>/post/<rkey>). Replies come back in extra.replies on that call. The feed endpoint is the list. The post endpoint is one record.
Cost
Profile: 1 credit. Each page of posts: 1 credit. Both are cacheable for 30 days. max_age_days between 0 and 30 returns a cached copy for 0 credits. Failures cost 0. A fresh account starts with 100 credits and no card.
Packs and monthly plans are on pricing, and the spend order is in credits.
Limits
You get the profile and the posts the feed returns page by page. You do not get a private account's posts. You do not get Bluesky's full firehose, and this is not a websocket.
If you need one post's reply tree, call the post endpoint. Do not expect extra.replies on the feed rows. Those rows carry counts, not the thread.
stats.views being null is normal here. Do not coerce null to 0 if your report distinguishes "unknown" from "none".
There are no rate limits on packs or plans, and free accounts have a lower limit. A 429 is retryable and free. See rate limits.
Next
The Bluesky profile API page has the sample and the parameter table. Posts are the profile posts API. Tutorials: how to scrape a Bluesky profile and how to scrape Bluesky profile posts. Docs: profile and posts. The hub is the Bluesky API.
The same pattern on other networks is Threads and Truth Social.