Header menu logo FSharp.ATProto

Raw XRPC

The convenience API (Bluesky.*, Chat.*) covers common operations with domain types like PostRef, Profile, and FeedItem. For anything it doesn't cover, all 237 Bluesky API endpoints are available as typed F# wrappers generated directly from the AT Protocol Lexicon schemas. These wrappers give you full access to every parameter and response field.

Query Endpoints

Query (GET) endpoints use a query function that takes an AtpAgent and a Params record, and returns a typed Output record:

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

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

    let! result =
        AppBskyFeed.GetActorFeeds.query agent
            { Actor = "alice.bsky.social"; Cursor = None; Limit = Some 10L }

    for feed in result.Feeds do
        printfn "%s" feed.DisplayName
}

Every field on the Params and Output records is fully typed. Optional protocol fields are option types, so the compiler ensures you handle them.

Procedure Endpoints

Procedure (POST) endpoints use a call function that takes an AtpAgent and an Input record. Some return a typed Output record; others return unit for void operations:

taskResult {
    let! _result =
        AppBskyFeed.SendInteractions.call agent
            { Interactions = [ interaction ] }

    return ()
}

The return type tells you which kind you're dealing with -- check the generated Output type or look at the AT Protocol reference for the endpoint.

Finding the Right Endpoint

The generated wrappers follow the Lexicon namespace structure exactly. Convert the dot-separated NSID to PascalCase modules:

Lexicon NSID

F# module

Function

app.bsky.feed.getTimeline

AppBskyFeed.GetTimeline

.query

app.bsky.actor.getProfile

AppBskyActor.GetProfile

.query

com.atproto.repo.createRecord

ComAtprotoRepo.CreateRecord

.call

chat.bsky.convo.sendMessage

ChatBskyConvo.SendMessage

.call

Each module also exposes a TypeId string literal (e.g., AppBskyFeed.GetTimeline.TypeId = "app.bsky.feed.getTimeline") which is useful when you need the raw endpoint name for Xrpc.paginate or logging.

For the full list of endpoints, see the AT Protocol HTTP Reference.

Custom Pagination

For paginated endpoints that don't have a pre-built paginator, use Xrpc.paginate directly. It returns an IAsyncEnumerable that fetches pages lazily:

let pages =
    Xrpc.paginate<AppBskyFeed.GetActorFeeds.Params, AppBskyFeed.GetActorFeeds.Output>
        AppBskyFeed.GetActorFeeds.TypeId
        { Actor = "alice.bsky.social"; Cursor = None; Limit = Some 50L }
        (fun output -> output.Cursor)
        (fun cursor input -> { input with Cursor = cursor })
        agent

The five arguments are: the endpoint type ID, initial parameters (with Cursor = None), a function to extract the cursor from the response, a function to inject a new cursor into the parameters, and the agent. The paginator stops automatically when the server returns no cursor.

See the Pagination guide for patterns on consuming the IAsyncEnumerable result.

Chat Endpoints

Chat endpoints (chat.bsky.*) require a proxy header. Create a chat-proxied agent with AtpAgent.withChatProxy before calling them:

taskResult {
    let chatAgent = AtpAgent.withChatProxy agent

    let! result =
        ChatBskyConvo.ListConvos.query chatAgent
            { Limit = Some 20L; Cursor = None; ReadState = None; Status = None }

    for convo in result.Convos do
        printfn "Conversation %s with %d members" convo.Id convo.Members.Length
}

The Chat.* convenience functions handle this automatically, but when using raw XRPC wrappers for chat endpoints, you need to set up the proxy yourself.

Mixing Convenience and Raw

You can freely mix convenience functions and raw XRPC calls in the same taskResult block. Both use the same AtpAgent and return Task<Result<'T, XrpcError>>, so they compose naturally:

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

    // Convenience: post with automatic rich text detection
    let! postRef = Bluesky.post agent "Check out my custom feeds!"

    // Raw XRPC: list the actor's custom feeds (no convenience wrapper for this)
    let! feeds =
        AppBskyFeed.GetActorFeeds.query agent
            { Actor = "handle.bsky.social"; Cursor = None; Limit = Some 5L }

    for feed in feeds.Feeds do
        printfn "%s" feed.DisplayName

    // Convenience: like the post we just created
    let! _like = Bluesky.like agent postRef

    return ()
}

The convenience layer and the raw wrappers are complementary. Use Bluesky.* and Chat.* for the operations they cover, and drop to the generated wrappers when you need parameters or endpoints the convenience layer doesn't expose.

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 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 result: AppBskyFeed.GetActorFeeds.Output
module AppBskyFeed from FSharp.ATProto.Bluesky
module GetActorFeeds from FSharp.ATProto.Bluesky.AppBskyFeed
val query: agent: AtpAgent -> parameters: AppBskyFeed.GetActorFeeds.Params -> System.Threading.Tasks.Task<Result<AppBskyFeed.GetActorFeeds.Output,XrpcError>>
union case Option.None: Option<'T>
union case Option.Some: Value: 'T -> Option<'T>
val feed: AppBskyFeed.Defs.GeneratorView
AppBskyFeed.GetActorFeeds.Output.Feeds: AppBskyFeed.Defs.GeneratorView list
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
AppBskyFeed.Defs.GeneratorView.DisplayName: string
val interaction: AppBskyFeed.Defs.Interaction
module Defs from FSharp.ATProto.Bluesky.AppBskyFeed
type Interaction = { Event: InteractionEvent option FeedContext: string option Item: AtUri option ReqId: string option }
val _result: unit
module SendInteractions from FSharp.ATProto.Bluesky.AppBskyFeed
val call: agent: AtpAgent -> input: AppBskyFeed.SendInteractions.Input -> System.Threading.Tasks.Task<Result<unit,XrpcError>>
val pages: System.Collections.Generic.IAsyncEnumerable<Result<AppBskyFeed.GetActorFeeds.Output,XrpcError>>
module Xrpc from FSharp.ATProto.Core
<summary> XRPC transport layer for AT Protocol API calls. Provides functions for executing queries (HTTP GET) and procedures (HTTP POST) against an authenticated <see cref="AtpAgent" />. </summary>
<remarks> All public functions in this module automatically handle: <list type="bullet"><item><description>Bearer token authentication from the agent's session.</description></item><item><description>Automatic session refresh on 401 <c>ExpiredToken</c> responses (retries once with a new access token).</description></item><item><description>Rate-limit retry on 429 responses (waits for the <c>Retry-After</c> duration, then retries once).</description></item><item><description>JSON serialization/deserialization using <see cref="Json.options" />.</description></item><item><description>Extra headers from <see cref="AtpAgent.ExtraHeaders" /> (e.g. proxy headers).</description></item></list></remarks>
val paginate: nsid: string -> initialParams: 'P -> getCursor: ('O -> string option) -> setCursor: (string option -> 'P -> 'P) -> agent: AtpAgent -> System.Collections.Generic.IAsyncEnumerable<Result<'O,XrpcError>>
<summary> Paginates through a cursor-based XRPC query, returning an <c>IAsyncEnumerable</c> of pages. </summary>
<param name="nsid">The NSID of the XRPC query method (e.g. <c>"app.bsky.feed.getTimeline"</c>).</param>
<param name="initialParams">The initial query parameters (typically with cursor set to <c>None</c>).</param>
<param name="getCursor"> A function that extracts the next-page cursor from a response. Return <c>None</c> to signal that there are no more pages. </param>
<param name="setCursor"> A function that produces updated parameters with the given cursor value set. </param>
<param name="agent">The <see cref="AtpAgent" /> to send requests through.</param>
<typeparam name="P">The parameter record type.</typeparam>
<typeparam name="O">The output type for each page of results.</typeparam>
<returns> An <c>IAsyncEnumerable</c> that yields one <c>Result</c> per page. Enumeration stops when <paramref name="getCursor" /> returns <c>None</c> or when an error occurs. On error, the error result is yielded as the final element. </returns>
<remarks> Each page is fetched lazily as the caller iterates the async enumerable. The underlying <see cref="query" /> function handles rate limiting and token refresh automatically. </remarks>
<example><code> let pages = Xrpc.paginate "app.bsky.feed.getTimeline" { Limit = Some 50; Cursor = None } (fun output -&gt; output.Cursor) (fun cursor p -&gt; { p with Cursor = cursor }) agent </code></example>
type Params = { Actor: string Cursor: string option Limit: int64 option }
type Output = { Cursor: string option Feeds: GeneratorView list }
[<Literal>] val TypeId: string = "app.bsky.feed.getActorFeeds"
val output: AppBskyFeed.GetActorFeeds.Output
AppBskyFeed.GetActorFeeds.Output.Cursor: string option
val cursor: string option
val input: AppBskyFeed.GetActorFeeds.Params
val chatAgent: AtpAgent
val withChatProxy: agent: AtpAgent -> AtpAgent
<summary> Returns a copy of the agent configured to proxy requests through the Bluesky Chat service. Adds the <c>atproto-proxy: did:web:api.bsky.chat#bsky_chat</c> header. </summary>
<param name="agent">The agent to copy with chat proxy configuration.</param>
<returns>A new <see cref="AtpAgent" /> with the chat proxy header prepended to <see cref="AtpAgent.ExtraHeaders" />.</returns>
<remarks> The returned agent shares the same <see cref="System.Net.Http.HttpClient" /> as the original but has an independent <see cref="AtpAgent.Session" /> field (it is a record copy). If you need chat functionality, prefer using the <see cref="FSharp.ATProto.Bluesky.Chat" /> module functions directly — they handle the proxy header automatically and always use the current session from the original agent. </remarks>
val result: ChatBskyConvo.ListConvos.Output
module ChatBskyConvo from FSharp.ATProto.Bluesky
module ListConvos from FSharp.ATProto.Bluesky.ChatBskyConvo
val query: agent: AtpAgent -> parameters: ChatBskyConvo.ListConvos.Params -> System.Threading.Tasks.Task<Result<ChatBskyConvo.ListConvos.Output,XrpcError>>
val convo: ChatBskyConvo.Defs.ConvoView
ChatBskyConvo.ListConvos.Output.Convos: ChatBskyConvo.Defs.ConvoView list
ChatBskyConvo.Defs.ConvoView.Id: string
ChatBskyConvo.Defs.ConvoView.Members: ChatBskyActor.Defs.ProfileViewBasic list
property List.Length: int with get
val postRef: PostRef
val post: agent: AtpAgent -> text: string -> System.Threading.Tasks.Task<Result<PostRef,XrpcError>>
<summary> Create a post with automatic rich text detection. Mentions, links, and hashtags are automatically detected and resolved to facets. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="text">The post text. Mentions (<c>@handle</c>), links (<c>https://...</c>), and hashtags (<c>#tag</c>) are auto-detected.</param>
<returns>A <see cref="PostRef" /> with the AT-URI and CID on success, or an <see cref="XrpcError" />.</returns>
<remarks> Internally calls <see cref="RichText.parse" /> to detect and resolve facets before creating the post. Unresolvable mentions are silently omitted from facets. For pre-resolved facets, use <see cref="postWithFacets" /> instead. </remarks>
<example><code> let! result = Bluesky.post agent "Hello @my-handle.bsky.social! Check out https://example.com #atproto" </code></example>
val feeds: AppBskyFeed.GetActorFeeds.Output
val _like: LikeRef
val like: agent: AtpAgent -> target: 'a -> System.Threading.Tasks.Task<Result<LikeRef,XrpcError>> (requires member ToPostRef)
<summary> Like a post or other record. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="target">A <see cref="PostRef" /> or <see cref="TimelinePost" /> identifying the record to like.</param>
<returns>A <see cref="LikeRef" /> on success, or an <see cref="XrpcError" />. Pass the <c>LikeRef</c> to <see cref="unlike" /> to undo.</returns>

Type something to start searching.