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.

Manton Reece @manton