Header menu logo FSharp.ATProto

Chat / Direct Messages

Send and receive Bluesky direct messages through the Chat module.

All examples use taskResult {} -- see the Error Handling guide for details. The chat proxy header (atproto-proxy: did:web:api.bsky.chat#bsky_chat) is applied automatically -- you use the same agent as for everything else.

Domain Types

ConvoSummary

A summary of a chat conversation.

Field

Type

Description

Id

string

The conversation identifier

Members

ProfileSummary list

Participants in the conversation

LastMessage

LastMessage option

The most recent message summary, if any

UnreadCount

int64

Number of unread messages

IsMuted

bool

Whether notifications are muted for this conversation

LastMessage

Summary of the most recent message in a conversation.

Field

Type

Description

Text

string

Message text

Sender

Did

DID of the sender

SentAt

DateTimeOffset

When the message was sent

ChatMessage

Discriminated union representing a message in a conversation. Uses [<RequireQualifiedAccess>].

Case

Fields

Description

ChatMessage.Message

Id : string, Text : string, Sender : Did, SentAt : DateTimeOffset, Embed : PostEmbed option

A visible message

ChatMessage.Deleted

Id : string, Sender : Did

A deleted message placeholder

Page<'T>

A paginated result containing a list of items and an optional cursor for the next page.

Field

Type

Description

Items

'T list

The items in this page

Cursor

string option

Cursor for the next page, or None if this is the last page

Functions

SRTP: All functions that take a convoId parameter accept either a ConvoSummary or a string conversation ID.

Conversations

Function

Accepts

Returns

Description

Chat.listConvos

agent, limit: int64 option, cursor: string option

Result<Page<ConvoSummary>, XrpcError>

List conversations, most recent first

Chat.getConvoForMembers

agent, members: Did list

Result<ConvoSummary, XrpcError>

Get or create a conversation with the given members

Chat.getConvo

agent, convoId

Result<ConvoSummary, XrpcError>

Get a conversation by ID

Chat.acceptConvo

agent, convoId

Result<unit, XrpcError>

Accept a conversation request

Chat.leaveConvo

agent, convoId

Result<unit, XrpcError>

Leave a conversation

taskResult {
    let! profile = Bluesky.getProfile agent aliceHandle
    let! convo = Chat.getConvoForMembers agent [ profile.Did ]
    printfn "Conversation: %s (members: %d)" convo.Id convo.Members.Length
}

Messages

Function

Accepts

Returns

Description

Chat.sendMessage

agent, convoId, text: string

Result<ChatMessage, XrpcError>

Send a message with auto-detected rich text

Chat.getMessages

agent, convoId, limit: int64 option, cursor: string option

Result<Page<ChatMessage>, XrpcError>

Get messages, most recent first

Chat.deleteMessage

agent, convoId, messageId: string

Result<unit, XrpcError>

Delete a message (for you only)

sendMessage automatically detects links, mentions, and hashtags -- just like Bluesky.post. No extra steps needed.

taskResult {
    let! msg = Chat.sendMessage agent convo "Check out https://atproto.com!"

    match msg with
    | ChatMessage.Message m -> printfn "Sent: %s (id: %s)" m.Text m.Id
    | ChatMessage.Deleted _ -> ()
}
taskResult {
    let! page = Chat.getMessages agent convo (Some 20L) None

    for m in page.Items do
        match m with
        | ChatMessage.Message msg ->
            printfn "[%s] %s" (Did.value msg.Sender) msg.Text
        | ChatMessage.Deleted del ->
            printfn "(deleted: %s)" del.Id
}

Read State

Function

Accepts

Returns

Description

Chat.markRead

agent, convoId

Result<unit, XrpcError>

Mark a conversation as read

Chat.markAllRead

agent

Result<int64, XrpcError>

Mark all conversations as read; returns count updated

taskResult {
    let! _ = Chat.markRead agent convo
    let! updatedCount = Chat.markAllRead agent
    printfn "Marked %d conversations as read" updatedCount
}

Muting

Function

Accepts

Returns

Description

Chat.muteConvo

agent, convoId

Result<unit, XrpcError>

Mute a conversation (no notifications)

Chat.unmuteConvo

agent, convoId

Result<unit, XrpcError>

Unmute a conversation

taskResult {
    let! _ = Chat.muteConvo agent convo
    let! _ = Chat.unmuteConvo agent convo
    return ()
}

Reactions

Function

Accepts

Returns

Description

Chat.addReaction

agent, convoId, messageId: string, emoji: string

Result<unit, XrpcError>

Add an emoji reaction to a message

Chat.removeReaction

agent, convoId, messageId: string, emoji: string

Result<unit, XrpcError>

Remove an emoji reaction from a message

taskResult {
    let! _ = Chat.addReaction agent convo msgId "❤️"
    let! _ = Chat.removeReaction agent convo msgId "❤️"
    return ()
}

Power Users: Raw XRPC

For full control over facets or to include an embed (e.g., sharing a post into a DM), drop to the raw XRPC wrapper. You must apply the chat proxy header manually when using raw wrappers:

task {
    let text = "Check out https://atproto.com!"
    let! facets = RichText.parse agent text

    let! result =
        ChatBskyConvo.SendMessage.call (AtpAgent.withChatProxy agent)
            { ConvoId = convo.Id
              Message =
                { Text = text
                  Facets = if facets.IsEmpty then None else Some facets
                  Embed = None } }

    match result with
    | Ok msg -> printfn "Sent with custom facets"
    | Error err -> printfn "Failed: %A" err
}
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 convo: ConvoSummary
Multiple items
module ConvoSummary from FSharp.ATProto.Bluesky

--------------------
type ConvoSummary = { Id: string Members: ProfileSummary list LastMessage: LastMessage option UnreadCount: int64 IsMuted: bool }
<summary>A summary of a chat conversation.</summary>
val msgId: string
val aliceHandle: 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 taskResult: TaskResultBuilder
val profile: Profile
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 getProfile: agent: AtpAgent -> actor: 'a -> System.Threading.Tasks.Task<Result<Profile,XrpcError>> (requires member ToActorString)
<summary> Get a user's profile. Accepts a <see cref="Handle" />, <see cref="Did" />, <see cref="ProfileSummary" />, or <see cref="Profile" />. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="actor">A <see cref="Handle" />, <see cref="Did" />, <see cref="ProfileSummary" />, or <see cref="Profile" />.</param>
<returns>A <see cref="Profile" /> on success, or an <see cref="XrpcError" />.</returns>
module Chat from FSharp.ATProto.Bluesky
<summary> Convenience methods for Bluesky direct message (DM) and chat operations. Wraps the <c>chat.bsky.convo.*</c> XRPC endpoints with a simplified API. All methods require an authenticated <see cref="AtpAgent" />. The chat proxy header (<c>atproto-proxy: did:web:api.bsky.chat#bsky_chat</c>) is applied automatically -- callers do not need to use <see cref="AtpAgent.withChatProxy" /> manually. </summary>
val getConvoForMembers: agent: AtpAgent -> members: Did list -> System.Threading.Tasks.Task<Result<ConvoSummary,XrpcError>>
<summary> Get an existing conversation with the specified members, or create a new one if none exists. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="members">A list of DIDs of the conversation members (excluding the authenticated user, who is added automatically).</param>
<returns>A <see cref="ConvoSummary" />, or an <see cref="XrpcError" />.</returns>
Profile.Did: Did
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
ConvoSummary.Id: string
ConvoSummary.Members: ProfileSummary list
property List.Length: int with get
val msg: ChatMessage
val sendMessage: agent: AtpAgent -> convoId: 'a -> text: string -> System.Threading.Tasks.Task<Result<ChatMessage,XrpcError>> (requires member ToConvoId)
<summary> Send a message to a conversation. Rich text (links, mentions, hashtags) is automatically detected and resolved, matching the behaviour of <c>Bluesky.post</c>. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="convoId">A <see cref="ConvoSummary" /> or <c>string</c> conversation ID.</param>
<param name="text">The message text content. Links, mentions, and hashtags are auto-detected.</param>
<returns>The sent message as a <see cref="ChatMessage" />, or an <see cref="XrpcError" />.</returns>
Multiple items
module ChatMessage from FSharp.ATProto.Bluesky

--------------------
type ChatMessage = | Message of {| Embed: PostEmbed option; Id: string; Sender: Did; SentAt: DateTimeOffset; Text: string |} | Deleted of {| Id: string; Sender: Did |}
<summary>A message in a chat conversation.</summary>
union case ChatMessage.Message: {| Embed: PostEmbed option; Id: string; Sender: Did; SentAt: System.DateTimeOffset; Text: string |} -> ChatMessage
val m: {| Embed: PostEmbed option; Id: string; Sender: Did; SentAt: System.DateTimeOffset; Text: string |}
anonymous record field Text: string
anonymous record field Id: string
union case ChatMessage.Deleted: {| Id: string; Sender: Did |} -> ChatMessage
val page: Page<ChatMessage>
val getMessages: agent: AtpAgent -> convoId: 'a -> limit: int64 option -> cursor: string option -> System.Threading.Tasks.Task<Result<Page<ChatMessage>,XrpcError>> (requires member ToConvoId)
<summary> Get messages in a conversation, ordered by most recent first. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="convoId">A <see cref="ConvoSummary" /> or <c>string</c> conversation ID.</param>
<param name="limit">Maximum number of messages to return. Pass <c>None</c> for the server default.</param>
<param name="cursor">Pagination cursor from a previous response. Pass <c>None</c> for the most recent messages.</param>
<returns>A page of <see cref="ChatMessage" /> with an optional cursor, or an <see cref="XrpcError" />.</returns>
union case Option.Some: Value: 'T -> Option<'T>
union case Option.None: Option<'T>
val m: ChatMessage
Page.Items: ChatMessage list
val msg: {| Embed: PostEmbed option; Id: string; Sender: Did; SentAt: System.DateTimeOffset; Text: string |}
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>
anonymous record field Sender: Did
val del: {| Id: string; Sender: Did |}
val markRead: agent: AtpAgent -> convoId: 'a -> System.Threading.Tasks.Task<Result<unit,XrpcError>> (requires member ToConvoId)
<summary> Mark a conversation as read up to the latest message. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="convoId">A <see cref="ConvoSummary" /> or <c>string</c> conversation ID.</param>
<returns><c>Ok ()</c> on success, or an <see cref="XrpcError" />.</returns>
val updatedCount: int64
val markAllRead: agent: AtpAgent -> System.Threading.Tasks.Task<Result<int64,XrpcError>>
<summary> Mark all conversations as read. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<returns>The number of conversations updated, or an <see cref="XrpcError" />.</returns>
val muteConvo: agent: AtpAgent -> convoId: 'a -> System.Threading.Tasks.Task<Result<unit,XrpcError>> (requires member ToConvoId)
<summary> Mute a conversation. Muted conversations do not generate notifications. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="convoId">A <see cref="ConvoSummary" /> or <c>string</c> conversation ID.</param>
<returns><c>Ok ()</c> on success, or an <see cref="XrpcError" />.</returns>
val unmuteConvo: agent: AtpAgent -> convoId: 'a -> System.Threading.Tasks.Task<Result<unit,XrpcError>> (requires member ToConvoId)
<summary> Unmute a previously muted conversation, restoring notifications. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="convoId">A <see cref="ConvoSummary" /> or <c>string</c> conversation ID.</param>
<returns><c>Ok ()</c> on success, or an <see cref="XrpcError" />.</returns>
val addReaction: agent: AtpAgent -> convoId: 'a -> messageId: string -> emoji: string -> System.Threading.Tasks.Task<Result<unit,XrpcError>> (requires member ToConvoId)
<summary> Add an emoji reaction to a message in a conversation. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="convoId">A <see cref="ConvoSummary" /> or <c>string</c> conversation ID.</param>
<param name="messageId">The ID of the message to react to.</param>
<param name="emoji">The emoji reaction value (e.g., a Unicode emoji string).</param>
<returns><c>Ok ()</c> on success, or an <see cref="XrpcError" />.</returns>
val removeReaction: agent: AtpAgent -> convoId: 'a -> messageId: string -> emoji: string -> System.Threading.Tasks.Task<Result<unit,XrpcError>> (requires member ToConvoId)
<summary> Remove an emoji reaction from a message in a conversation. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="convoId">A <see cref="ConvoSummary" /> or <c>string</c> conversation ID.</param>
<param name="messageId">The ID of the message to remove the reaction from.</param>
<param name="emoji">The emoji reaction value to remove.</param>
<returns><c>Ok ()</c> on success, or an <see cref="XrpcError" />.</returns>
val task: TaskBuilder
val text: string
val facets: AppBskyRichtext.Facet.Facet list
module RichText from FSharp.ATProto.Bluesky
<summary> Rich text processing for Bluesky posts. Detects mentions, links, and hashtags in text and resolves them to facets with correct UTF-8 byte offsets as required by the AT Protocol. </summary>
val parse: agent: AtpAgent -> text: string -> System.Threading.Tasks.Task<AppBskyRichtext.Facet.Facet list>
<summary> Detect and resolve all rich text facets in a single step. Combines <see cref="detect" /> and <see cref="resolve" />: scans the text for mentions, links, and hashtags, then resolves mentions to DIDs. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="text">The text to scan and resolve.</param>
<returns>A list of resolved <see cref="AppBskyRichtext.Facet.Facet" /> records.</returns>
<example><code> let! facets = RichText.parse agent "Hello @my-handle.bsky.social! Check https://example.com #atproto" </code></example>
val result: Result<ChatBskyConvo.SendMessage.Output,XrpcError>
module ChatBskyConvo from FSharp.ATProto.Bluesky
module SendMessage from FSharp.ATProto.Bluesky.ChatBskyConvo
val call: agent: AtpAgent -> input: ChatBskyConvo.SendMessage.Input -> System.Threading.Tasks.Task<Result<ChatBskyConvo.SendMessage.Output,XrpcError>>
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>
property List.IsEmpty: bool with get
union case Result.Ok: ResultValue: 'T -> Result<'T,'TError>
val msg: ChatBskyConvo.SendMessage.Output
union case Result.Error: ErrorValue: 'TError -> Result<'T,'TError>
val err: XrpcError

Type something to start searching.