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 |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
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.
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> 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> 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>
<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 -> output.Cursor) (fun cursor p -> { p with Cursor = cursor }) agent </code></example>
<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>
<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>
<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>