Jeremy Davis
Jeremy Davis
Sitecore, C# and web development
Article printed from: https://blog.jermdavis.dev/posts/2025/posting-images-bluesky

Posting images to BlueSky

Notes so I can remember how later...

Published 02 June 2025
C# ~3 min. read

I was tinkering with some some code that could post to BlueSky recently, and it took me a couple of goes to make the process of submitting a message with an image work. So in case it's of any use to anyone else, here's one way it can work:

The basic process url copied!

There are four things to do:

  1. Get your app password
  2. Authenticate
  3. Post the image blob and get some reference data for it
  4. Post your message with the blob reference

Sounds easy, huh? All the code for this is posted in a gist, and the rest of this post is a broad explanation of what's happening.

Get your app password url copied!

This first bit isn't code. You don't use the account password to post via the API - you use an App Password instead. To get one of those you need to log in to the UI, and click the "Settings" cog from the left menu and then "Privacy and security" to find the App Passwords option:

The BlueSky Privacy & Security settings page, showing the App Passwords option

That page will list any app passwords you've created, and let you add new ones:

The BlueSky's App Passwords page, showing the add button and existing passwords

Clicking the "Add" button here will let you create a new App Password entry by supplying a name:

The dialog allowing you to set the name for a new app password

And then it will give you a random password to use:

The data for the newly created app password, which you need to record to use later.

Note that you have to copy this and save it, as there is no way to view this again once you close this dialog. (Also, I deleted this key before posting this - sorry hacker type people)

Also, note that when you come to authenticate, the username is your account name plus the server it's hosted on. So if your name is @mybotaccount then the username you need to authenticate is mybotaccount.bsky.social.

Authenticate url copied!

The rest of the calls require a JWT passed in their request headers, so the first API call is to get that token. The code needs to pass the username and the app password wrapped up in some simple json.

var usr = "yourusername.bsky.social";
var pwd = "aaaa-1111-bbbb-2222";

var authContent = JsonContent.Create(new {
    identifier = usr,
    password = pwd
});

					

Sending the request will require an HttpClient object to interact with BlueSky's API. You should avoid creating and destroying these per request, so you likely want to create this centrally and reuse it for the rest of the operations:

private HttpClient _client = new();

					

(That's an IDisposable object, so make sure it's disposed once it's no longer needed) But with that in place, the json content prepared above can be posted to the relevant endpoint:

var authDataResp = await _client.PostAsync("https://bsky.social/xrpc/com.atproto.server.createSession", authContent);

					

If that request fails, it's likely indicating the credentials are no good - a condition which will need handling instead of proceeding. But if it succeeds then the response is some json that includes the token you need and some other data. So that needs parsing to retrieve the token:

var authJson = await authDataResp.Content.ReadAsStringAsync();
var authData = JsonNode.Parse(authJson);

var jwt = authData["accessJwt"]?.GetValue<string>();

					

And the easiest way to handle that token is to add the Bearer header as a default to the client - so it's sent automatically for the remaining requests:

_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", jwt);

					

Post the image blob url copied!

To attach an image to your post, you need to upload it and get back a data structure that describes the stored image blob. The image is supplied as a raw data stream in the body of another post, which has to specify the MIME type for the image format as a header. (As well as the token set up previously)

var imageData = new StreamContent(data);
imageData.Headers.Add("Content-Type", format);

var resp = await _client.PostAsync("https://bsky.social/xrpc/com.atproto.repo.uploadBlob", imageData);

					

The data variable to pass as the StreamContent object is the stream returned by a call like File.OpenRead(@"C:\temp\your-test-image.jpg").

If this succeeds, the response from this is a chunk of json which describes the storage of the data - which will be required later.

{
  "blob": {
    "$type": "blob",
    "ref": {
      "$link": "baqkreieuv2ih2jk3ritnfarlmjq6psv2weifchape76bju6geadjoryktu"
    },
    "mimeType": "image/jpeg",
    "size": 452753
  }
}

					

That can be read from the response and parsed into a POCO to reuse later:

var storageData = await resp.Content.ReadAsStringAsync();
var storageNode = JsonObject.Parse(storageData);
var blob = Blob.Deserialize(storageNode);

					

The POCO includes a static method to do that deserialisation, just to make the example a bit prettier: (There is probably a prettier way to do this - but I was hacking about...)

public static Blob Deserialize(JsonNode n)
{
    var json = n.ToString();

    var b = System.Text.Json.JsonSerializer.Deserialize<BlobWrapper>(n);

    if (b == null || b.Blob == null)
    {
        throw new ArgumentException("Deserialising blob data returned a null");
    }

    return b.Blob;
}

					

Post the message url copied!

Posting a message with an image requires building up a json structure that respresents the message and (optionally) the attached image:

var post = new PostData
{
    Repo = usr,
    Collection = "app.bsky.feed.post",
    Record = new PostRecordData
    {
        Text = message,
        CreatedAt = DateTime.Now
    }
};

if (blob != null)
{
    altText = altText ?? string.Empty;
    post.Record.Embed = Embed.Generate(blob, altText);
}

					

So it creates the basic data with the message we want to send, and then adds in Blob from earlier as an embed if we have one.

And then that structure can get posted to the API:

var postContent = JsonContent.Create(post);
var postResponse = await _client.PostAsync("https://bsky.social/xrpc/com.atproto.repo.createRecord", postContent);

					

As before the code can check the .IsSuccessStatusCode property of the response to verify the message got accepted correctly.

And with that, your message should appear in the user's feed.

↑ Back to top