This page mainly serves as a reference point for myself when making stuff using Bluesky’s API. Their API is simple and nice to work with to the point that I rarely bother using any kind of framework (at least when accessing the public API, which requires no auth)

This tends to use the public Bluesky AppView: https://public.api.bsky.app/

Listing all posts from a user

This can be achieved by using app.bsky.feed.getAuthorFeed

For instance: https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor=HANDLE

e.g. https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor=yernemm.xyz

Or for no replies: https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?filter=posts_no_replies&actor=HANDLE

Filtering out reposts

There doesn’t seem to be a way to filter out reposts directly in the API call so in cases where I want only my own posts, I tend to manually filter out posts containing a reason.

Here’s a simplified snippet of how I filter that for my Bluesky feed on yernemm.xyz and limit it to 15 posts max:

const handle = "YOUR HANDLE";
 
fetch("https://api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor=" + handle + "&filter=posts_no_replies&limit=30&includePins=false")
.then(response => response.json())
.then(data => {
 
  const posts = data.feed.filter((item: any) => item.post && !item.reason).slice(0, 15);
 
  //Process posts here
 
});

I initially fetch 30 posts to account for the filter removing some of them, this leaves me with up to 15 posts to embed depending on how many were initially removed. You could iteratively fetch more posts using cursor to account for this but I did not bother.

Note: I have now decided to include reposts in my own feed on my front page so this snippet is lost media 👻

Getting a post with replies

For this, I am using app.bsky.feed.getPostThread.

Specifically, this API endpoint can be used like this:

https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=at://DID/app.bsky.feed.post/RKEY

But to do this, we need to find the DID and the RKEY.

A Bluesky post URL is structured like this: https://bsky.app/profile/DID/post/RKEY

Example extraction code:

const postUrl = "https://bsky.app/profile/yernemm.xyz/post/3mgif3pvge22f"
const urlParts = postUrl.split("/");
const DID = urlParts[urlParts.length - 3];
const RKEY = urlParts[urlParts.length - 1];

For instance, with this post: https://bsky.app/profile/yernemm.xyz/post/3mgif3pvge22f

DID = yernemm.xyz RKEY = 3mgif3pvge22f

So we can plug those into the API endpoint like so: https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=at://yernemm.xyz/app.bsky.feed.post/3mgif3pvge22f

This returns a JSON object which is structured something like this:

thread: {
	post: {
		author: {...},
		record: {
			text: "Post text here",
			...
		},
		...
	},
	replies: [
		post: {...},
		...
	]
}

so we can grab the post text from thread.post.record.text, author information from thread.post.record.author. Then if you want to render a whole comment chain, you can recursively traverse over the posts in thread.post.replies.

Observations

Getting Image Attachments

One strange thing I noticed is how the JSON response for posts handles attached images.

For most posts, it looks something like this:

post: {
	...
	embed: {
		...
		images: [
			{
				fullsize: "...",
				thumb: "...",
				alt: "..."
			}, ...
		]
	}
}

But I’ve also come across some posts where instead, the images object is nested inside a media object, e.g.:

post: {
	...
	embed: {
		...
		media:{
			...
			images: [
				{
					fullsize: "...",
					thumb: "...",
					alt: "..."
				}, ...
			]
		}
	}
}

I’m not sure what causes this, right now I’m just having to check both cases to reliably get attached images.