Header menu logo FSharp.ATProto

Account

FSharp.ATProto provides functions for authentication, session management, account lifecycle, and agent configuration. These are the entry points for all interaction with the AT Protocol.

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

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

Authentication

Function

Returns

Description

Bluesky.login

Task<Result<AtpAgent, XrpcError>>

Authenticate with base URL, identifier, and app password

Bluesky.loginWithClient

Task<Result<AtpAgent, XrpcError>>

Login with a custom HttpClient

Bluesky.resumeSession

AtpAgent

Resume from saved session data (no network call)

Bluesky.resumeSessionWithClient

AtpAgent

Resume with a custom HttpClient

Bluesky.logout

Task<Result<unit, XrpcError>>

Terminate the session on the server

Basic Login

taskResult {
    let! agent = Bluesky.login "https://bsky.social" "handle.bsky.social" "app-password"
    printfn "Logged in as %s" (Did.value agent.Session.Value.Did)
}

Session Persistence

The AtpSession record contains the JWTs and identity needed to restore a session without re-authenticating:

type AtpSession =
    { AccessJwt : string
      RefreshJwt : string
      Did : Did
      Handle : Handle }

Save the session after login, then restore it later:

// Save after login
taskResult {
    let! agent = Bluesky.login "https://bsky.social" "handle.bsky.social" "app-password"
    let session = agent.Session.Value
    // Persist session.AccessJwt, session.RefreshJwt, session.Did, session.Handle
    return agent
}
// Restore later (no network call)
let restoredAgent = Bluesky.resumeSession "https://bsky.social" savedSession

The access JWT expires after a short period. The library automatically refreshes it using the refresh JWT on the first 401 response, so a restored session remains functional as long as the refresh JWT is valid.

Custom HttpClient

Use loginWithClient or resumeSessionWithClient when you need a custom HTTP handler (e.g., for testing with a mock handler or custom timeouts):

let client = new System.Net.Http.HttpClient()
client.Timeout <- System.TimeSpan.FromSeconds(30.0)

taskResult {
    let! agent = Bluesky.loginWithClient client "https://bsky.social" "handle.bsky.social" "app-password"
    return agent
}

Account Lifecycle

Function

Description

Bluesky.createAccount

Create a new account on a PDS

Bluesky.requestAccountDelete

Request deletion (sends confirmation email)

Bluesky.deleteAccount

Confirm deletion with token from email

Creating an Account

taskResult {
    let handle = Handle.parse "new-user.my-pds.example" |> Result.defaultWith failwith

    let! agent =
        Bluesky.createAccount
            "https://my-pds.example"
            handle
            (Some "user@example.com")
            (Some "strong-password")
            None // invite code, if required by the PDS

    printfn "Account created: %s" (Did.value agent.Session.Value.Did)
}

Deleting an Account

Account deletion is a two-step process. First, request deletion (which sends a confirmation email), then confirm with the token from the email:

taskResult {
    // Step 1: Request deletion -- sends confirmation email
    do! Bluesky.requestAccountDelete agent

    // Step 2: After receiving the email, confirm with the token
    do! Bluesky.deleteAccount agent "account-password" "token-from-email"
    // Agent session is cleared after successful deletion
}

Agent Configuration

Labeler Subscriptions

AtpAgent.configureLabelers returns a new agent configured with the atproto-accept-labelers header. This tells the server which labeler services to include labels from in responses:

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

    // Subscribe to a custom labeler (redact=false means labels are informational)
    let agent = AtpAgent.configureLabelers [ "did:plc:my-labeler", false ] agent

    // Subscribe with redact=true (labeler's labels can hide content entirely)
    let agent = AtpAgent.configureLabelers [ "did:plc:my-labeler", true ] agent

    // Multiple labelers
    let agent =
        AtpAgent.configureLabelers
            [ "did:plc:labeler-one", false
              "did:plc:labeler-two", true ]
            agent

    return agent
}

Chat Proxy

AtpAgent.withChatProxy returns a new agent with the proxy header needed for Bluesky DM operations:

let chatAgent = AtpAgent.withChatProxy agent

The chat proxy header (atproto-proxy: did:web:api.bsky.chat#bsky_chat) is required for all chat.bsky.* endpoints. See the Chat guide for usage.

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 printfn: format: Printf.TextWriterFormat<'T> -> 'T
Multiple items
module Did from FSharp.ATProto.Syntax
<summary> Functions for creating, validating, and extracting data from <see cref="Did" /> values. </summary>

--------------------
type Did = private | Did of string override ToString: unit -> string
<summary> A decentralized identifier (DID) as defined by the AT Protocol. DIDs are the primary stable identifier for accounts. Two methods are currently supported: <c>did:plc:</c> (hosted, managed by PLC directory) and <c>did:web:</c> (self-hosted, DNS-based). </summary>
<remarks> See the AT Protocol specification: https://atproto.com/specs/did and the W3C DID specification: https://www.w3.org/TR/did-core/ </remarks>
val value: Did -> string
<summary> Extract the string representation of a DID. </summary>
<param name="did">The DID to extract the value from.</param>
<returns>The full DID string (e.g. <c>"did:plc:z72i7hdynmk6r22z27h6tvur"</c>).</returns>
AtpAgent.Session: AtpSession option
<summary> The current authenticated session, or <c>None</c> if not logged in. This field is mutable and is updated automatically by <see cref="AtpAgent.login" /> and by the automatic token refresh logic in <see cref="Xrpc" />. </summary>
property Option.Value: AtpSession with get
AtpSession.Did: Did
<summary>The DID (Decentralized Identifier) of the authenticated user (e.g. "did:plc:xyz123").</summary>
type AtpSession = { AccessJwt: string RefreshJwt: string Did: obj Handle: obj }
Multiple items
val string: value: 'T -> string

--------------------
type string = System.String
val session: AtpSession
val savedSession: AtpSession
type AtpSession = { AccessJwt: string RefreshJwt: string Did: Did Handle: Handle }
<summary> An authenticated session with an AT Protocol Personal Data Server (PDS). Obtained by calling <see cref="AtpAgent.login" /> with valid credentials. </summary>
<remarks> Sessions contain a short-lived access JWT for API calls and a longer-lived refresh JWT for obtaining new access tokens. The <see cref="Xrpc" /> module automatically refreshes expired access tokens using the refresh JWT. </remarks>
val restoredAgent: AtpAgent
val resumeSession: baseUrl: string -> session: AtpSession -> AtpAgent
<summary> Construct an agent from saved session data without making any network calls. Use this to restore a session from persisted tokens. </summary>
<param name="baseUrl">The PDS base URL (e.g. <c>"https://bsky.social"</c>).</param>
<param name="session">A previously obtained <see cref="AtpSession" />.</param>
<returns>An authenticated <see cref="AtpAgent" /> with the given session.</returns>
val client: System.Net.Http.HttpClient
namespace System
namespace System.Net
namespace System.Net.Http
Multiple items
type HttpClient = inherit HttpMessageInvoker new: unit -> unit + 2 overloads member CancelPendingRequests: unit -> unit member DeleteAsync: requestUri: string -> Task<HttpResponseMessage> + 3 overloads member GetAsync: requestUri: string -> Task<HttpResponseMessage> + 7 overloads member GetByteArrayAsync: requestUri: string -> Task<byte array> + 3 overloads member GetStreamAsync: requestUri: string -> Task<Stream> + 3 overloads member GetStringAsync: requestUri: string -> Task<string> + 3 overloads member PatchAsync: requestUri: string * content: HttpContent -> Task<HttpResponseMessage> + 3 overloads member PostAsync: requestUri: string * content: HttpContent -> Task<HttpResponseMessage> + 3 overloads ...
<summary>Provides a class for sending HTTP requests and receiving HTTP responses from a resource identified by a URI.</summary>

--------------------
System.Net.Http.HttpClient() : System.Net.Http.HttpClient
System.Net.Http.HttpClient(handler: System.Net.Http.HttpMessageHandler) : System.Net.Http.HttpClient
System.Net.Http.HttpClient(handler: System.Net.Http.HttpMessageHandler, disposeHandler: bool) : System.Net.Http.HttpClient
property System.Net.Http.HttpClient.Timeout: System.TimeSpan with get, set
<summary>Gets or sets the timespan to wait before the request times out.</summary>
<exception cref="T:System.ArgumentOutOfRangeException">The timeout specified is less than or equal to zero and is not <see cref="F:System.Threading.Timeout.InfiniteTimeSpan" /> -or- The timeout specified is greater than <see cref="F:System.Int32.MaxValue" /> milliseconds.</exception>
<exception cref="T:System.InvalidOperationException">An operation has already been started on the current instance.</exception>
<exception cref="T:System.ObjectDisposedException">The current instance has been disposed.</exception>
<returns>The timespan to wait before the request times out.</returns>
Multiple items
[<Struct>] type TimeSpan = new: hours: int * minutes: int * seconds: int -> unit + 4 overloads member Add: ts: TimeSpan -> TimeSpan member CompareTo: value: obj -> int + 1 overload member Divide: divisor: float -> TimeSpan + 1 overload member Duration: unit -> TimeSpan member Equals: value: obj -> bool + 2 overloads member GetHashCode: unit -> int member Multiply: factor: float -> TimeSpan member Negate: unit -> TimeSpan member Subtract: ts: TimeSpan -> TimeSpan ...
<summary>Represents a time interval.</summary>

--------------------
System.TimeSpan ()
System.TimeSpan(ticks: int64) : System.TimeSpan
System.TimeSpan(hours: int, minutes: int, seconds: int) : System.TimeSpan
System.TimeSpan(days: int, hours: int, minutes: int, seconds: int) : System.TimeSpan
System.TimeSpan(days: int, hours: int, minutes: int, seconds: int, milliseconds: int) : System.TimeSpan
System.TimeSpan(days: int, hours: int, minutes: int, seconds: int, milliseconds: int, microseconds: int) : System.TimeSpan
System.TimeSpan.FromSeconds(seconds: int64) : System.TimeSpan
System.TimeSpan.FromSeconds(value: float) : System.TimeSpan
System.TimeSpan.FromSeconds(seconds: int64, ?milliseconds: int64, ?microseconds: int64) : System.TimeSpan
val loginWithClient: client: System.Net.Http.HttpClient -> baseUrl: string -> identifier: string -> password: string -> System.Threading.Tasks.Task<Result<AtpAgent,XrpcError>>
<summary> Create an agent with a custom <see cref="System.Net.Http.HttpClient" /> and authenticate. Useful for testing with mock HTTP handlers or custom client configuration. </summary>
<param name="client">The HTTP client to use for all requests.</param>
<param name="baseUrl">The PDS base URL (e.g. <c>"https://bsky.social"</c>).</param>
<param name="identifier">A handle (e.g. <c>"my-handle.bsky.social"</c>) or DID.</param>
<param name="password">An app password (not the account password).</param>
<returns>An authenticated <see cref="AtpAgent" /> on success, or an <see cref="XrpcError" />.</returns>
val handle: Handle
Multiple items
module Handle from FSharp.ATProto.Syntax
<summary> Functions for creating, validating, and extracting data from <see cref="Handle" /> values. </summary>

--------------------
type Handle = private | Handle of string override ToString: unit -> string
<summary> A handle (domain name) used as a human-readable identifier in the AT Protocol. Handles are DNS-based names (e.g. <c>my-handle.bsky.social</c>) that resolve to a <see cref="Did" />. They must be valid domain names with at least two segments and a maximum length of 253 characters. </summary>
<remarks> See the AT Protocol specification: https://atproto.com/specs/handle </remarks>
val parse: s: string -> Result<Handle,string>
<summary> Parse and validate a handle string. </summary>
<param name="s"> A handle string in domain-name format (e.g. <c>"my-handle.bsky.social"</c>). Must be a valid hostname with at least two segments, each segment starting and ending with an alphanumeric character, and the TLD starting with a letter. </param>
<returns><c>Ok</c> with a validated <see cref="Handle" />, or <c>Error</c> with a message describing the validation failure. Validation failures include: null input, exceeding the 253-character limit, or invalid hostname syntax. </returns>
<example><code> match Handle.parse "my-handle.bsky.social" with | Ok handle -&gt; printfn "Valid: %s" (Handle.value handle) | Error e -&gt; printfn "Invalid: %s" e </code></example>
Multiple items
module Result from Microsoft.FSharp.Core

--------------------
[<Struct>] type Result<'T,'TError> = | Ok of ResultValue: 'T | Error of ErrorValue: 'TError
val defaultWith: defThunk: ('Error -> 'T) -> result: Result<'T,'Error> -> 'T
val failwith: message: string -> 'T
val createAccount: baseUrl: string -> handle: Handle -> email: string option -> password: string option -> inviteCode: string option -> System.Threading.Tasks.Task<Result<AtpAgent,XrpcError>>
<summary> Create a new account on the given PDS and return an authenticated agent. Only the handle is required; email, password, and invite code are optional. </summary>
<param name="baseUrl">The PDS base URL (e.g. <c>"https://bsky.social"</c>).</param>
<param name="handle">The requested handle for the new account.</param>
<param name="email">Optional email address for the account.</param>
<param name="password">Optional password. May need to meet instance-specific strength requirements.</param>
<param name="inviteCode">Optional invite code, if the PDS requires one.</param>
<returns>An authenticated <see cref="AtpAgent" /> on success, or an <see cref="XrpcError" />.</returns>
union case Option.Some: Value: 'T -> Option<'T>
union case Option.None: Option<'T>
val requestAccountDelete: agent: AtpAgent -> System.Threading.Tasks.Task<Result<unit,XrpcError>>
<summary> Request account deletion. Sends a confirmation email to the account's email address. After receiving the email, call <see cref="deleteAccount" /> with the token from the email. Requires an authenticated session. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<returns><c>Ok ()</c> on success, or an <see cref="XrpcError" />.</returns>
val deleteAccount: agent: AtpAgent -> password: string -> token: string -> System.Threading.Tasks.Task<Result<unit,XrpcError>>
<summary> Delete the authenticated user's account. Requires a token from <see cref="requestAccountDelete" /> (sent via email) and the account password. After successful deletion, the agent's session is cleared. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="password">The account password.</param>
<param name="token">The deletion token received via email after calling <see cref="requestAccountDelete" />.</param>
<returns><c>Ok ()</c> on success, or an <see cref="XrpcError" />.</returns>
val configureLabelers: labelers: (string * bool) list -> agent: AtpAgent -> AtpAgent
<summary> Returns a copy of the agent configured with the <c>atproto-accept-labelers</c> header. This tells the server which labeler services to include labels from in responses. </summary>
<param name="labelers"> A list of labeler DIDs and optional redact flags. Each entry is a tuple of (labeler DID string, redact flag). When <c>redact=true</c>, the labeler's labels can cause content to be entirely removed from responses. </param>
<param name="agent">The agent to copy with labeler configuration.</param>
<returns>A new <see cref="AtpAgent" /> with the <c>atproto-accept-labelers</c> header.</returns>
<remarks> The header format follows IETF RFC-8941 Structured Field Values: <c>did:plc:abc123;redact, did:plc:def456</c></remarks>
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>

Type something to start searching.