Header menu logo FSharp.ATProto

Media

All examples use taskResult {}. See the Error Handling guide for details.

FSharp.ATProto provides convenience methods for uploading images and attaching them to posts. The high-level API handles blob upload, embed construction, and rich text detection in a single call.

Size limit: Bluesky enforces a 1 MB maximum per image blob. If your images may exceed this, resize them before uploading. The library sends bytes directly to the PDS without resizing, and oversized uploads will be rejected by the server. Video: See the Video section below for video upload support.

open FSharp.ATProto.Core
open FSharp.ATProto.Bluesky

Posting with Images

Bluesky.postWithImages uploads each image, constructs the embed record, auto-detects rich text facets, and creates the post -- all in one call:

taskResult {
    let! agent = Bluesky.login "https://bsky.social" "handle.bsky.social" "app-password"

    let imageBytes = System.IO.File.ReadAllBytes "/path/to/photo.jpg"

    let images =
        [ { Data = imageBytes
            MimeType = Jpeg
            AltText = "A sunset over the mountains" } ]

    let! postRef = Bluesky.postWithImages agent "Evening view from the trail" images
    printfn "Posted: %s" (AtUri.value postRef.Uri)
    return postRef
}

Each image is described by an ImageUpload record:

type ImageUpload =
    { Data : byte[]       // raw image bytes
      MimeType : ImageMime // Jpeg, Png, Gif, Webp, or Custom of string
      AltText : string }   // accessibility text (required)

The MimeType field uses the ImageMime discriminated union for type-safe MIME type selection:

type ImageMime =
    | Png
    | Jpeg
    | Gif
    | Webp
    | Custom of string

Use DU cases directly -- Jpeg, not "image/jpeg".

Alt text is required. It describes the image content for screen readers. Write it as if describing the image to someone who cannot see it.

Multiple Images

Bluesky supports up to 4 images per post. Each gets its own ImageUpload with independent alt text:

taskResult {
    let! agent = Bluesky.login "https://bsky.social" "handle.bsky.social" "app-password"

    let! postRef =
        Bluesky.postWithImages agent "Before and after"
            [ { Data = System.IO.File.ReadAllBytes "before.png"
                MimeType = Png
                AltText = "The garden before planting, bare soil" }
              { Data = System.IO.File.ReadAllBytes "after.jpg"
                MimeType = Jpeg
                AltText = "The garden three months later, full of tomatoes" } ]

    return postRef
}

Images are uploaded sequentially. If any upload fails, the entire operation short-circuits and returns the error without creating the post.

Rich Text with Images

postWithImages auto-detects mentions, links, and hashtags in the text, just like Bluesky.post. See the Rich Text guide for how facet detection works.

taskResult {
    let! agent = Bluesky.login "https://bsky.social" "handle.bsky.social" "app-password"

    let images =
        [ { Data = System.IO.File.ReadAllBytes "screenshot.png"
            MimeType = Png
            AltText = "Screenshot of the FSharp.ATProto test suite passing" } ]

    let! postRef =
        Bluesky.postWithImages agent "All tests passing! @friend.bsky.social #fsharp" images

    return postRef
}

The @friend.bsky.social mention is resolved to a DID, the #fsharp hashtag becomes a clickable facet, and the image is attached as an embed.

Uploading Blobs Directly

Bluesky.uploadBlob is the lower-level API for uploading binary data to the PDS. It returns a typed BlobRef:

taskResult {
    let! agent = Bluesky.login "https://bsky.social" "handle.bsky.social" "app-password"
    let data = System.IO.File.ReadAllBytes "diagram.png"
    let! blobRef = Bluesky.uploadBlob agent data Png
    printfn "Uploaded: CID=%s, size=%d bytes" (Cid.value blobRef.Ref) blobRef.Size
    return blobRef
}

The BlobRef type:

type BlobRef =
    { Json : JsonElement   // raw blob reference for embedding in custom records
      Ref : Cid            // content identifier
      MimeType : string    // e.g. "image/png"
      Size : int64 }       // size in bytes

This is useful when postWithImages is not flexible enough -- for example, uploading a blob once and referencing it in multiple records, or constructing a custom embed type via the raw XRPC layer.

Video

Upload and post video content. Video processing is asynchronous -- the server transcodes the video after upload.

Functions

Function

Description

Bluesky.uploadVideo

Upload video bytes, returns a JobStatus with processing state

Bluesky.getVideoJobStatus

Poll the processing status of an uploaded video

Bluesky.awaitVideoProcessing

Poll until processing completes (with configurable max attempts)

Bluesky.postWithVideo

Upload, wait for processing, and create a post -- all in one call

Quick Example

taskResult {
    let! agent = Bluesky.login "https://bsky.social" "handle" "app-password"
    let videoBytes = System.IO.File.ReadAllBytes("clip.mp4")
    let! post = Bluesky.postWithVideo agent "Check out this video!" videoBytes Mp4 (Some "A short clip")
    printfn "Posted video: %s" (AtUri.value post.Uri)
}

Step-by-Step

For more control over the upload process:

taskResult {
    let! agent = Bluesky.login "https://bsky.social" "handle" "app-password"
    let videoBytes = System.IO.File.ReadAllBytes("clip.mp4")

    // 1. Upload the video
    let! jobStatus = Bluesky.uploadVideo agent videoBytes Mp4

    // 2. Wait for server-side processing (max 60 poll attempts)
    let! completed = Bluesky.awaitVideoProcessing agent jobStatus.JobId (Some 60)

    // 3. Create a post with the processed video
    // (use raw XRPC with the blob ref from the completed job)
    ()
}

Note: Videos have server-enforced size limits. MP4 is the supported format.

Supported Formats

Bluesky accepts JPEG, PNG, GIF, and WebP images. Use the corresponding ImageMime case (Jpeg, Png, Gif, Webp) or Custom of string for anything else.

Multiple items
namespace FSharp

--------------------
namespace Microsoft.FSharp
namespace FSharp.ATProto
namespace FSharp.ATProto.Syntax
namespace FSharp.ATProto.Core
namespace FSharp.ATProto.Bluesky
val agent: AtpAgent
module Unchecked from Microsoft.FSharp.Core.Operators
val defaultof<'T> : 'T
Multiple items
module AtpAgent from FSharp.ATProto.Core
<summary> Functions for creating and authenticating <see cref="AtpAgent" /> instances. </summary>

--------------------
type AtpAgent = { HttpClient: HttpClient mutable BaseUrl: Uri mutable Session: AtpSession option ExtraHeaders: (string * string) list AuthenticateRequest: (HttpRequestMessage -> unit) option RefreshAuthentication: (unit -> Task<Result<unit,XrpcError>>) option OnSessionChanged: (unit -> unit) option }
<summary> Client agent for communicating with an AT Protocol Personal Data Server (PDS). Holds the HTTP client, base URL, optional authenticated session, and extra headers. </summary>
<remarks> Create an agent with <see cref="AtpAgent.create" /> or <see cref="AtpAgent.createWithClient" />, then authenticate with <see cref="AtpAgent.login" />. The agent's <see cref="Session" /> field is mutable: it is updated automatically on login and token refresh. </remarks>
<example><code> let agent = AtpAgent.create "https://bsky.social" let! session = AtpAgent.login "my-handle.bsky.social" "app-password-here" agent </code></example>
val postRef: PostRef
type PostRef = { Uri: AtUri Cid: Cid }
<summary> A reference to a specific version of a post record. Contains both the AT-URI (identifying the record) and the CID (identifying the exact version). Accepted by <c>like</c>, <c>repost</c>, and <c>replyTo</c>. </summary>
val taskResult: TaskResultBuilder
module Bluesky from FSharp.ATProto.Bluesky
<summary> High-level convenience methods for common Bluesky operations: posting, replying, liking, reposting, following, blocking, uploading blobs, and deleting records. All methods require an authenticated <see cref="AtpAgent" />. </summary>
val login: baseUrl: string -> identifier: string -> password: string -> System.Threading.Tasks.Task<Result<AtpAgent,XrpcError>>
val imageBytes: byte array
namespace System
namespace System.IO
type File = static member AppendAllBytes: path: string * bytes: byte array -> unit + 1 overload static member AppendAllBytesAsync: path: string * bytes: byte array * ?cancellationToken: CancellationToken -> Task + 1 overload static member AppendAllLines: path: string * contents: IEnumerable<string> -> unit + 1 overload static member AppendAllLinesAsync: path: string * contents: IEnumerable<string> * encoding: Encoding * ?cancellationToken: CancellationToken -> Task + 1 overload static member AppendAllText: path: string * contents: ReadOnlySpan<char> -> unit + 3 overloads static member AppendAllTextAsync: path: string * contents: ReadOnlyMemory<char> * encoding: Encoding * ?cancellationToken: CancellationToken -> Task + 3 overloads static member AppendText: path: string -> StreamWriter static member Copy: sourceFileName: string * destFileName: string -> unit + 1 overload static member Create: path: string -> FileStream + 2 overloads static member CreateSymbolicLink: path: string * pathToTarget: string -> FileSystemInfo ...
<summary>Provides static methods for the creation, copying, deletion, moving, and opening of a single file, and aids in the creation of <see cref="T:System.IO.FileStream" /> objects.</summary>
System.IO.File.ReadAllBytes(path: string) : byte array
val images: ImageUpload list
namespace Microsoft.FSharp.Data
union case ImageMime.Jpeg: ImageMime
val postWithImages: agent: AtpAgent -> text: string -> images: ImageUpload list -> System.Threading.Tasks.Task<Result<PostRef,XrpcError>>
<summary> Create a post with attached images and automatic rich text detection. Uploads each image as a blob, then creates the post with an <c>app.bsky.embed.images</c> embed. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="text">The post text. Mentions, links, and hashtags are auto-detected.</param>
<param name="images"> A list of <see cref="ImageUpload" /> records describing the images to attach. Alt text is required for accessibility. Bluesky supports up to 4 images per post. </param>
<returns>A <see cref="PostRef" /> with the AT-URI and CID on success, or an <see cref="XrpcError" />.</returns>
<remarks> Images are uploaded sequentially. If any image upload fails, the entire operation returns the error without creating the post. </remarks>
<example><code> let imageBytes = System.IO.File.ReadAllBytes("photo.jpg") let! result = Bluesky.postWithImages agent "Check this out!" [ { Data = imageBytes; MimeType = Jpeg; AltText = "A photo" } ] </code></example>
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
Multiple items
module AtUri from FSharp.ATProto.Syntax
<summary> Functions for creating, validating, and extracting data from <see cref="AtUri" /> values. </summary>

--------------------
type AtUri = private | AtUri of string override ToString: unit -> string
<summary> An AT-URI that identifies a resource in the AT Protocol network. AT-URIs use the scheme <c>at://</c> followed by an authority (DID or handle), an optional collection (NSID), and an optional record key. Format: <c>at://&lt;authority&gt;[/&lt;collection&gt;[/&lt;rkey&gt;]]</c>. Maximum length is 8192 characters. </summary>
<remarks> See the AT Protocol specification: https://atproto.com/specs/at-uri-scheme </remarks>
val value: AtUri -> string
<summary> Extract the string representation of an AT-URI. </summary>
<param name="atUri">The AT-URI to extract the value from.</param>
<returns>The full AT-URI string (e.g. <c>"at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3k2la3b"</c>).</returns>
PostRef.Uri: AtUri
<summary>The AT-URI of the post record.</summary>
type ImageUpload = { Data: byte array MimeType: obj AltText: string }
Multiple items
val byte: value: 'T -> byte (requires member op_Explicit)

--------------------
type byte = System.Byte

--------------------
type byte<'Measure> = byte
Multiple items
val string: value: 'T -> string

--------------------
type string = System.String
type ImageMime = | Png | Jpeg | Gif | Webp | Custom of string
union case ImageMime.Png: ImageMime
val data: byte array
val blobRef: BlobRef
val uploadBlob: agent: AtpAgent -> data: byte array -> mimeType: ImageMime -> System.Threading.Tasks.Task<Result<BlobRef,XrpcError>>
<summary> Upload a blob (image, video, or other binary data) to the PDS. Returns a typed <see cref="BlobRef" /> containing the blob reference needed to embed the blob in a record. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="data">The raw binary content of the blob.</param>
<param name="mimeType">The MIME type of the blob (e.g., <see cref="ImageMime.Jpeg" />, <see cref="ImageMime.Png" />).</param>
<returns><c>Ok</c> with a <see cref="BlobRef" /> on success, or an <see cref="XrpcError" />. The <see cref="BlobRef.Json" /> field contains the raw JSON for use in embed records, while <see cref="BlobRef.Ref" />, <see cref="BlobRef.MimeType" />, and <see cref="BlobRef.Size" /> provide typed access to individual fields. </returns>
<remarks> Use <see cref="BlobRef.Json" /> when constructing custom embed records, or use <see cref="postWithImages" /> for a higher-level API that handles blob references automatically. </remarks>
Multiple items
module Cid from FSharp.ATProto.Syntax
<summary> Functions for creating, validating, and extracting data from <see cref="Cid" /> values. </summary>

--------------------
type Cid = private | Cid of string override ToString: unit -> string
<summary> A Content Identifier (CID) used to reference content-addressed data in the AT Protocol. CIDs are self-describing content hashes that uniquely identify a piece of data. Only CIDv1 is supported; CIDv0 (starting with <c>Qmb</c>) is rejected. </summary>
<remarks> CIDs in the AT Protocol use CIDv1 with DAG-CBOR codec and SHA-256 hash. This type performs syntactic validation only (base-encoded alphanumeric string of 8-256 characters). See https://github.com/multiformats/cid for the CID specification. </remarks>
val value: Cid -> string
<summary> Extract the string representation of a CID. </summary>
<param name="cid">The CID to extract the value from.</param>
<returns>The CID string in its base-encoded form.</returns>
BlobRef.Ref: Cid
<summary>The content-addressed link (CID) of the blob.</summary>
BlobRef.Size: int64
<summary>The size of the blob in bytes.</summary>
type BlobRef = { Json: obj Ref: obj MimeType: string Size: int64 }
Multiple items
val int64: value: 'T -> int64 (requires member op_Explicit)

--------------------
type int64 = System.Int64

--------------------
type int64<'Measure> = int64
val videoBytes: byte array
val post: PostRef
val postWithVideo: agent: AtpAgent -> text: string -> videoData: byte array -> mimeType: VideoMime -> altText: string option -> System.Threading.Tasks.Task<Result<PostRef,XrpcError>>
<summary> Upload a video, wait for processing, and create a post with the video embedded. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="text">The post text.</param>
<param name="videoData">The raw video bytes.</param>
<param name="mimeType">The video MIME type.</param>
<param name="altText">Optional alt text for the video.</param>
<returns>A <see cref="PostRef" /> on success, or an <see cref="XrpcError" />.</returns>
union case VideoMime.Mp4: VideoMime
union case Option.Some: Value: 'T -> Option<'T>
val jobStatus: AppBskyVideo.Defs.JobStatus
val uploadVideo: agent: AtpAgent -> data: byte array -> mimeType: VideoMime -> System.Threading.Tasks.Task<Result<AppBskyVideo.Defs.JobStatus,XrpcError>>
<summary> Upload a video to the video processing service. Returns a job status that can be polled with <see cref="getVideoJobStatus" />. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="data">The raw video bytes.</param>
<param name="mimeType">The video MIME type.</param>
<returns>A <see cref="AppBskyVideo.Defs.JobStatus" /> on success, or an <see cref="XrpcError" />.</returns>
val completed: BlobRef
val awaitVideoProcessing: agent: AtpAgent -> jobId: string -> maxAttempts: int option -> System.Threading.Tasks.Task<Result<BlobRef,XrpcError>>
<summary> Poll a video processing job until it completes, then return the blob reference. Polls every 1.5 seconds, up to a maximum number of attempts. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="jobId">The job ID returned by <see cref="uploadVideo" />.</param>
<param name="maxAttempts">Maximum number of poll attempts (default 60, ~90 seconds).</param>
<returns>A <see cref="BlobRef" /> on success, or an <see cref="XrpcError" />.</returns>
AppBskyVideo.Defs.JobStatus.JobId: string

Type something to start searching.