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 |
|---|---|
|
Upload video bytes, returns a |
|
Poll the processing status of an uploaded video |
|
Poll until processing completes (with configurable max attempts) |
|
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.
namespace FSharp
--------------------
namespace Microsoft.FSharp
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>
<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>
<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>
<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>
<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>
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://<authority>[/<collection>[/<rkey>]]</c>. Maximum length is 8192 characters. </summary>
<remarks> See the AT Protocol specification: https://atproto.com/specs/at-uri-scheme </remarks>
<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>
<summary>The AT-URI of the post record.</summary>
val byte: value: 'T -> byte (requires member op_Explicit)
--------------------
type byte = System.Byte
--------------------
type byte<'Measure> = byte
val string: value: 'T -> string
--------------------
type string = System.String
<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>
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>
<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>
<summary>The content-addressed link (CID) of the blob.</summary>
<summary>The size of the blob in bytes.</summary>
val int64: value: 'T -> int64 (requires member op_Explicit)
--------------------
type int64 = System.Int64
--------------------
type int64<'Measure> = int64
<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>
<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>
<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>