{
"path": "/Resources/Bluesky-API-Info",
"site": "at://did:plc:z5udahlpedl5my5bjmvxbz5u/site.standard.publication/3ml7auuhba627",
"tags": [
"Bluesky",
"Resources",
"Web",
"JavaScript",
"ATProto"
],
"$type": "site.standard.document",
"title": "Bluesky API Info",
"content": {
"text": "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)\n\n> \n> This page covers interacting with the Bluesky API specifically, there are other apps and appviews on the AT protocol. The method for accessing any arbitrary PDS records is a bit different, and not covered here.\n\nThis tends to use the [public Bluesky AppView](https://public.api.bsky.app/): `https://public.api.bsky.app/`\n# Listing all posts from a user\n\nThis can be achieved by using [*app.bsky.feed.getAuthorFeed*](https://docs.bsky.app/docs/api/app-bsky-feed-get-author-feed)\n\nFor instance:\n`https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor=HANDLE`\n\ne.g.\nhttps://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor=yernemm.xyz\n\nOr for no replies:\n`https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?filter=posts_no_replies&actor=HANDLE`\n\n## Filtering out reposts\n\nThere 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`.\n\nHere's a simplified snippet of how I filter that for my Bluesky feed on [yernemm.xyz](https://yernemm.xyz) and limit it to 15 posts max:\n\n```ts\nconst handle = \"YOUR HANDLE\";\n\nfetch(\"https://api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor=\" + handle + \"&filter=posts_no_replies&limit=30&includePins=false\")\n.then(response => response.json())\n.then(data => {\n\n const posts = data.feed.filter((item: any) => item.post && !item.reason).slice(0, 15);\n\n //Process posts here\n\n});\n```\n\n*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.*\n\nNote: I have now decided to include reposts in my own feed on my front page so this snippet is lost media 👻\n\n# Getting a post with replies\n\nFor this, I am using [app.bsky.feed.getPostThread](https://docs.bsky.app/docs/api/app-bsky-feed-get-post-thread).\n\nSpecifically, this API endpoint can be used like this:\n\n`https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=at://DID/app.bsky.feed.post/RKEY`\n\nBut to do this, we need to find the `DID` and the `RKEY`.\n\nA Bluesky post URL is structured like this: `https://bsky.app/profile/DID/post/RKEY`\n\nExample extraction code:\n```js\nconst postUrl = \"https://bsky.app/profile/yernemm.xyz/post/3mgif3pvge22f\"\nconst urlParts = postUrl.split(\"/\");\nconst DID = urlParts[urlParts.length - 3];\nconst RKEY = urlParts[urlParts.length - 1];\n```\n\nFor instance, with this post: https://bsky.app/profile/yernemm.xyz/post/3mgif3pvge22f\n\n`DID = yernemm.xyz`\n`RKEY = 3mgif3pvge22f`\n\nSo 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\n\nThis returns a JSON object which is structured something like this:\n```json\nthread: {\n\tpost: {\n\t\tauthor: {...},\n\t\trecord: {\n\t\t\ttext: \"Post text here\",\n\t\t\t...\n\t\t},\n\t\t...\n\t},\n\treplies: [\n\t\tpost: {...},\n\t\t...\n\t]\n}\n```\n\nso 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`.\n\n# Observations\n\n## Getting Image Attachments\n\nOne strange thing I noticed is how the JSON response for posts handles attached images.\n\nFor most posts, it looks something like this:\n\n```json\npost: {\n\t...\n\tembed: {\n\t\t...\n\t\timages: [\n\t\t\t{\n\t\t\t\tfullsize: \"...\",\n\t\t\t\tthumb: \"...\",\n\t\t\t\talt: \"...\"\n\t\t\t}, ...\n\t\t]\n\t}\n}\n```\n\nBut I've also come across some posts where instead, the `images` object is nested inside a `media` object, e.g.:\n\n\n```json\npost: {\n\t...\n\tembed: {\n\t\t...\n\t\tmedia:{\n\t\t\t...\n\t\t\timages: [\n\t\t\t\t{\n\t\t\t\t\tfullsize: \"...\",\n\t\t\t\t\tthumb: \"...\",\n\t\t\t\t\talt: \"...\"\n\t\t\t\t}, ...\n\t\t\t]\n\t\t}\n\t}\n}\n```\n\nI'm not sure what causes this, right now I'm just having to check both cases to reliably get attached images.",
"$type": "at.markpub.markdown",
"flavor": "GFM"
},
"updatedAt": "2026-05-07T22:24:50.319Z",
"description": "A reference page for making stuff using Bluesky's public API.",
"publishedAt": "2026-05-07T22:23:39.224Z",
"textContent": "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)\n\nThis page covers interacting with the Bluesky API specifically, there are other apps and appviews on the AT protocol. The method for accessing any arbitrary PDS records is a bit different, and not covered here.\n\nThis tends to use the public Bluesky AppView: https://public.api.bsky.app/\nListing all posts from a user\n\nThis can be achieved by using app.bsky.feed.getAuthorFeed\n\nFor instance:\nhttps://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor=HANDLE\n\ne.g.\nhttps://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor=yernemm.xyz\n\nOr for no replies:\nhttps://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?filter=posts_no_replies&actor=HANDLE\n\nFiltering out reposts\n\nThere 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.\n\nHere's a simplified snippet of how I filter that for my Bluesky feed on yernemm.xyz and limit it to 15 posts max:\n\nconst handle = \"YOUR HANDLE\";\n\nfetch(\"https://api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor=\" + handle + \"&filter=posts_no_replies&limit=30&includePins=false\")\n.then(response => response.json())\n.then(data => {\n\n const posts = data.feed.filter((item: any) => item.post && !item.reason).slice(0, 15);\n\n //Process posts here\n\n});\n\nI 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.\n\nNote: I have now decided to include reposts in my own feed on my front page so this snippet is lost media 👻\n\nGetting a post with replies\n\nFor this, I am using app.bsky.feed.getPostThread.\n\nSpecifically, this API endpoint can be used like this:\n\nhttps://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=at://DID/app.bsky.feed.post/RKEY\n\nBut to do this, we need to find the DID and the RKEY.\n\nA Bluesky post URL is structured like this: https://bsky.app/profile/DID/post/RKEY\n\nExample extraction code:\nconst postUrl = \"https://bsky.app/profile/yernemm.xyz/post/3mgif3pvge22f\"\nconst urlParts = postUrl.split(\"/\");\nconst DID = urlParts[urlParts.length - 3];\nconst RKEY = urlParts[urlParts.length - 1];\n\nFor instance, with this post: https://bsky.app/profile/yernemm.xyz/post/3mgif3pvge22f\n\nDID = yernemm.xyz\nRKEY = 3mgif3pvge22f\n\nSo 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\n\nThis returns a JSON object which is structured something like this:\nthread: {\n\tpost: {\n\t\tauthor: {...},\n\t\trecord: {\n\t\t\ttext: \"Post text here\",\n\t\t\t...\n\t\t},\n\t\t...\n\t},\n\treplies: [\n\t\tpost: {...},\n\t\t...\n\t]\n}\n\nso 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.\n\nObservations\n\nGetting Image Attachments\n\nOne strange thing I noticed is how the JSON response for posts handles attached images.\n\nFor most posts, it looks something like this:\n\npost: {\n\t...\n\tembed: {\n\t\t...\n\t\timages: [\n\t\t\t{\n\t\t\t\tfullsize: \"...\",\n\t\t\t\tthumb: \"...\",\n\t\t\t\talt: \"...\"\n\t\t\t}, ...\n\t\t]\n\t}\n}\n\nBut I've also come across some posts where instead, the images object is nested inside a media object, e.g.:\n\npost: {\n\t...\n\tembed: {\n\t\t...\n\t\tmedia:{\n\t\t\t...\n\t\t\timages: [\n\t\t\t\t{\n\t\t\t\t\tfullsize: \"...\",\n\t\t\t\t\tthumb: \"...\",\n\t\t\t\t\talt: \"...\"\n\t\t\t\t}, ...\n\t\t\t]\n\t\t}\n\t}\n}\n\nI'm not sure what causes this, right now I'm just having to check both cases to reliably get attached images."
}