Bluesky's AT Protocol API requires understanding DIDs, CIDs, and record keys. Pulse wraps all of that into one GET request — give it a Bluesky post URL, get back engagement metrics as clean JSON. No authentication, no API key, no protocol knowledge required.
curl "https://pulse.walls.sh/metrics?url=https://bsky.app/profile/bsky.app/post/3mnslrkd6ok2g"
{
"platform": "bluesky",
"views": null,
"likes": 10522,
"comments": 715,
"shares": 2131,
"quotes": 1308,
"publishedAt": "2026-06-08T21:10:01.530Z",
"title": "v1.123 is live! You can now attach up to 10 photos in posts…",
"author": "@bsky.app"
}
Those are live numbers — fetched at time of writing. Pulse returns
likes, comments (replies), shares (reposts),
quotes, publishedAt, title, and author
for any public Bluesky post. Bluesky doesn't have view counts at the protocol level —
the field comes back null, which is accurate.
# Bluesky lets users set a domain as their handle — both forms resolve curl "https://pulse.walls.sh/metrics?url=https://bsky.app/profile/pfrazee.com/post/3mnxs3ci5eu2d" curl "https://pulse.walls.sh/metrics?url=https://bsky.app/profile/did:plc:ragtjsm2j2vknwkz3zp4oxrd/post/3mnxs3ci5eu2d"
Whether the profile URL uses a .bsky.social handle, a custom
domain handle, or a raw DID — Pulse normalises them to the same post.
curl "https://pulse.walls.sh/profile?url=https://bsky.app/profile/pfrazee.com"
{
"platform": "bluesky",
"handle": "pfrazee.com",
"name": "Paul Frazee",
"followers": 308681,
"following": 632,
"posts": 37934
}
Profile URLs return follower count, following count, and post count for any
public Bluesky account. Both handle.bsky.social and custom-domain handles work.
JavaScript / Node
const postUrl = "https://bsky.app/profile/bsky.app/post/3mnslrkd6ok2g";
const res = await fetch(
"https://pulse.walls.sh/metrics?url=" + encodeURIComponent(postUrl)
);
const { likes, comments, shares, quotes, publishedAt } = await res.json();
console.log({ likes, comments, shares, quotes, publishedAt });
Python
import requests
post_url = "https://bsky.app/profile/bsky.app/post/3mnslrkd6ok2g"
data = requests.get(
"https://pulse.walls.sh/metrics",
params={"url": post_url}
).json()
print(data["likes"], data["comments"], data["shares"])
GET /metrics/batch?url=BSKY_URL_A&url=BSKY_URL_B&url=BSKY_URL_C
Up to 50 Bluesky URLs per batch request, order preserved. Mix Bluesky with
YouTube, X, TikTok, Mastodon, and Instagram URLs in the same batch — same endpoint, same
response shape. A deleted post returns { url, error: "content_unavailable" }
without failing the rest.
curl "https://pulse.walls.sh/metrics/batch?url=BSKY_URL_1&url=BSKY_URL_2"
# → { "count": 2, "results": [ { "likes": …, "shares": … }, { "likes": …, "shares": … } ] }
Every /metrics call saves a timestamped snapshot. After the
second fetch, /history returns the full series plus computed growth stats:
curl "https://pulse.walls.sh/history?url=https://bsky.app/profile/bsky.app/post/3mnslrkd6ok2g"
# → {
# "points": [ { "t": "…", "likes": 10400, "shares": 2100 }, … ],
# "latest": { "likes": 10522, "shares": 2131 },
# "delta": { "hours_elapsed": 48.1, "likes": 122, "shares": 31 },
# "velocity": { "likes_per_hour": 2.54, "shares_per_hour": 0.64 }
# }
Add &since=<ISO-timestamp> to poll only new snapshots. The
velocity field tells you the current engagement rate per hour without computing
it yourself — useful for "is this post still spreading?" queries.
Free: 120 calls/minute, no account or signup needed. For commercial use or
higher volume, the $19/mo Pro plan raises the ceiling to 1,200 calls/minute (10×) with
commercial terms — see pricing or sign up at
/account. All endpoints set RateLimit-*
response headers so your code can self-throttle before hitting a 429.
More: full API docs · OpenAPI spec · all supported platforms · X/Twitter metrics without the API · TikTok metrics without an invite.