Getting started with Bluesky XRPC

I’ve done a little work now with the XRPC layer of the AT Protocol, supporting cross-posting to Bluesky from Micro.blog. This post is about what I’ve learned.

(As an aside, there have been questions about whether Micro.blog supporting Bluesky means we believe in everything they’re doing. No, right now I’m mostly interested in the technology. It’s still too early for judgements on the Bluesky leadership, user experience, or ultimately how this is all going to fit together with other social web protocols.)

Bluesky authenticates with a username and password. For third-party apps, the password can be an app-specific password. I hope that eventually Bluesky will support IndieAuth, a flavor of OAuth designed for signing in to web sites that should also work well for a distributed service like Bluesky.

The HTTP POST with JSON for signing in looks like this:

POST /xrpc/com.atproto.server.createSession
Content-Type: application/json

{
  "identifier": "email-address-here",
  "password": "password-here"
}

You’ll get back an access token and refresh token. Sessions do not last very long, only a couple hours last time I checked, so it’s important to keep the refresh token. The response looks like this:

{
  "did": "did:plc:abcdef12345",
  "handle": "manton.org",
  "email": "email-address-here",
  "accessJwt": "abcdefghijklmnopqrstuvxyz",
  "refreshJwt": "zyxvutsrqponmlkjihgfedcba"
}

The DID is a unique identifier for your account that is stored with posts on an AT Protocol server. Even if you change your handle, the DID persists and helps make data portable across servers.

When cross-posting from Micro.blog, I first try to use the auth token and if it fails, I use the refresh token to establish a new session. In this case, we pass the refresh token in the Authorization header:

POST /xrpc/com.atproto.server.refreshSession
Authorization: Bearer zyxvutsrqponmlkjihgfedcba

Sending a simple text post to Bluesky looks like this. For the rest of these requests, we pass the usual access token for authorization:

POST /xrpc/com.atproto.repo.createRecord
Authorization: Bearer abcdefghijklmnopqrstuvxyz
Content-Type: application/json

{
  "repo": "did:plc:abcdef12345",
  "collection": "app.bsky.feed.post",
  "validate": true,
  "record": {
    "text": "Hello world.",
    "createdAt": "2023-04-20T16:46:32+00:00"
  }
}

It can get more complicated. To include a photo with the post, first upload it to storage as a blob. In my early testing, there were low limits for photo file size, so Micro.blog scales photos down quite a bit before sending them over to Bluesky.

Here’s uploading the photo, passing the raw JPEG bytes in the content body:

POST /xrpc/com.atproto.repo.uploadBlob
Authorization: Bearer abcdefghijklmnopqrstuvxyz
Content-Type: image/jpeg

image-data-here

You’ll get back a media CID (Content ID) in the ref field that can be used to attach the photo to a new post. The response after uploading a photo looks like this:

{
  "blob": {
    "$type": "blob",
    "ref": {
      "$link": "abcdefgh"
    },
    "mimeType": "image/jpeg",
    "size": 200000
  }
}

Then when posting, use the embed field with an array of the uploaded media CIDs:

POST /xrpc/com.atproto.repo.createRecord
Authorization: Bearer abcdefghijklmnopqrstuvxyz
Content-Type: application/json

{
  "repo": "did:plc:abcdef12345",
  "collection": "app.bsky.feed.post",
  "record": {
    "text": "Hello world with photo.",
    "createdAt": "2023-03-08T16:46:32+00:00",
    "embed": {
      "$type": "app.bsky.embed.images",
      "images": [
        {
          "image": {
            "cid": "abcdefgh",
            "mimeType": "image/jpeg"
          },
          "alt": ""
        }
      ]
    }
  }
}

Bluesky also supports inline hyperlinks in the post text through “facets” that can be added to a post, similar to attaching a photo. I don’t love this because we already have HTML as a perfectly good way to format posts. I strongly believe that the social web should use HTML and HTTP wherever possible.

In Micro.blog, I automatically convert Markdown or HTML inline links to Bluesky’s facets. An example of linking the first word “Hello” in this post would look like this, using the character position and length of the word:

POST /xrpc/com.atproto.repo.createRecord
Authorization: Bearer abcdefghijklmnopqrstuvxyz
Content-Type: application/json

{
  "repo": "did:plc:abcdef12345",
  "collection": "app.bsky.feed.post",
  "validate": true,
  "record": {
    "text": "Hello world with link.",
    "createdAt": "2023-04-20T16:46:32+00:00",
    "facets": [
      {
        "features": [
          {
            "uri": "https://manton.org/",
            "$type": "app.bsky.richtext.facet#link"
          }
        ],
        "index": {
          "byteStart": 0,
          "byteEnd": 5
        }
      }
    ]
  }
}

There is also a growing list of open source libraries for the AT Protocol. Unfortunately I wrote all my code before I realized this, so I stumbled through deciphering the API more than I needed to. Maybe this post will save you some time if you’re rolling your own thing.

Update: HTTP requests go to bsky.social, not bsky.app.

Colin Walker

That's really useful, thanks. I was taking a brief look at the AT Protocol but it's nice to have something lay it all out for you. so much easier than trying to read the spec. The way you go over it reminds me in oart of connecting to the Dropbox API.

Colin Walker

"in part" — grrr typo!

Dave Winer

-- thanks for doing this cheat sheet for calling the Bluesky API. I'm having trouble getting started. I'm sending the initial POST to this URL, but am getting back the source to an HTML page, not a JSON object -- staging.bsky.app/xrpc/com....

Manton Reece

@dave Oh, make sure you're sending to the host bsky.social, not staging. I should have mentioned that in the post.

Dave Winer

-- BING! it worked.

Khürt Williams

I will be interesting to try once I get a BlueSky account.

Manton Reece @manton
Lightbox Image