"Bluesky API" in search logs is often someone who wants public 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.
Both routes are in beta. 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.
curl -G "https://api.socialscrape.io/v1/bluesky/profile" \
-H "x-api-key: $SOCIALSCRAPE_API_KEY" \
--data-urlencode "handle=bsky.app"import os
import requests
params = {
"handle": "bsky.app",
}
res = requests.get(
"https://api.socialscrape.io/v1/bluesky/profile",
headers={"x-api-key": os.environ["SOCIALSCRAPE_API_KEY"]},
params=params,
)
body = res.json()
if body["ok"]:
print(body["data"])
else:
print(body["error"]["code"], body["error"]["message"])const url = new URL("https://api.socialscrape.io/v1/bluesky/profile");
url.search = new URLSearchParams({
"handle": "bsky.app"
}).toString();
const res = await fetch(url, {
headers: { "x-api-key": process.env.SOCIALSCRAPE_API_KEY! },
});
const body = await res.json();
if (body.ok) {
console.log(body.data);
} else {
console.error(body.error.code, body.error.message);
}data is a profile. platform is bluesky. You get id, handle, name, bio, avatar_url, banner_url, url, verified, is_private, external_url and links. created_at is set when the profile has one.
stats.followers, stats.following and stats.posts are the public counts. stats.likes and stats.views are null when the profile has no such totals.
extra on the recorded profile includes did, associated, indexed_at, pinned_post_uri, status and verification. Keep extra.did if you will call the posts endpoint next. The reference says a DID is the faster lookup. user_id is an alias of handle on the posts route, so you can pass the DID in handle (or as user_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.
curl -G "https://api.socialscrape.io/v1/bluesky/profile/posts" \
-H "x-api-key: $SOCIALSCRAPE_API_KEY" \
--data-urlencode "handle=bsky.app"import os
import requests
params = {
"handle": "bsky.app",
}
res = requests.get(
"https://api.socialscrape.io/v1/bluesky/profile/posts",
headers={"x-api-key": os.environ["SOCIALSCRAPE_API_KEY"]},
params=params,
)
body = res.json()
if body["ok"]:
print(body["data"])
else:
print(body["error"]["code"], body["error"]["message"])const url = new URL("https://api.socialscrape.io/v1/bluesky/profile/posts");
url.search = new URLSearchParams({
"handle": "bsky.app"
}).toString();
const res = await fetch(url, {
headers: { "x-api-key": process.env.SOCIALSCRAPE_API_KEY! },
});
const body = await res.json();
if (body.ok) {
console.log(body.data);
} else {
console.error(body.error.code, body.error.message);
}data.items are posts. Each has id, url, text, published_at, author, media, hashtags, mentions and stats. stats.likes and stats.comments are filled when Bluesky sends them. stats.views is commonly null. stats.shares is the shared field. Bluesky's own counters also land in extra: repost_count, quote_count, cid, uri and image_alts. Use extra.uri and extra.cid if you store the AT Protocol identity of the post. url is the bsky.app link.
cursor and has_more tell you whether to continue. Send the cursor back unchanged. Stop when has_more is false. Each page costs 1 credit. Pagination describes the loop. parent is 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 public 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".
Rate limits are the team's, documented on rate limits: 30 a minute on free credits, 300 with any paid pack, 120 to 1,200 on monthly plans. A 429 is retryable and free.
The API is in early access. Shapes can gain fields while the status stays beta.
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.