Identity Resolution
All examples use taskResult {}. See the Error Handling guide for details.
The AT Protocol uses two kinds of identifiers for accounts:
- *Handles* -- human-readable names like
my-handle.bsky.social - *DIDs* -- stable, cryptographic identifiers like
did:plc:z72i7hdynmk6r22z27h6tvur
Handles can change; DIDs cannot. The Identity module provides functions to resolve between them, with optional bidirectional verification to ensure they agree.
Resolving an Identity (Recommended)
resolveIdentity is the recommended entry point. It accepts a plain string -- either a handle or a DID -- performs forward and reverse resolution, and checks that both sides agree.
open FSharp.ATProto.Core
open FSharp.ATProto.Bluesky
open FSharp.ATProto.Syntax
taskResult {
let! identity = Identity.resolveIdentity agent "my-handle.bsky.social"
printfn "Verified DID: %s" (Did.value identity.Did)
match identity.Handle with
| Some h -> printfn "Verified handle: %s" (Handle.value h)
| None -> printfn "Handle could not be verified"
return identity
}
Unlike resolveHandle and resolveDid which take typed Handle and Did respectively, resolveIdentity accepts a plain string -- it figures out whether you gave it a handle or a DID and does the right thing.
The verification works as follows:
- Input is a handle: resolve handle to DID, then fetch the DID document and check that it claims the same handle in its
alsoKnownAsfield. - Input is a DID: fetch the DID document to get the claimed handle, then resolve that handle back to a DID and check they match.
If the bidirectional check fails, the returned AtprotoIdentity will have Handle = None rather than an unverified value.
Typed Alternatives
If you already have a typed Handle or Did, you can use the single-direction functions directly. These perform one lookup without bidirectional verification.
Resolve a Handle to a DID:
// Handle.parse returns Result<Handle, string> -- handle the error first
match Handle.parse "my-handle.bsky.social" with
| Ok handle ->
taskResult {
let! did = Identity.resolveHandle agent handle
printfn "DID: %s" (Did.value did)
}
|> ignore
| Error msg -> printfn "Invalid handle: %s" msg
Resolve a DID to full identity info:
match Did.parse "did:plc:z72i7hdynmk6r22z27h6tvur" with
| Ok did ->
taskResult {
let! identity = Identity.resolveDid agent did
printfn "Handle: %s" (identity.Handle |> Option.map Handle.value |> Option.defaultValue "(none)")
printfn "PDS: %s" (identity.PdsEndpoint |> Option.map string |> Option.defaultValue "(none)")
}
|> ignore
| Error msg -> printfn "Invalid DID: %s" msg
resolveDid fetches the DID document and extracts the handle from alsoKnownAs, the PDS endpoint from the service entries, and the signing key from the verificationMethod entries. Both did:plc: (via the PLC directory) and did:web: (via .well-known/did.json) methods are supported.
The AtprotoIdentity Type
All fields use the library's typed identifiers, not raw strings:
type AtprotoIdentity =
{ Did: Did // Typed DID -- use Did.value to get the string
Handle: Handle option // Typed Handle -- present only if verified
PdsEndpoint: Uri option // FSharp.ATProto.Syntax.Uri
SigningKey: string option }
To print or display these values, use the accessor functions:
let showIdentity (id: Identity.AtprotoIdentity) =
printfn "DID: %s" (Did.value id.Did)
printfn "Handle: %s" (id.Handle |> Option.map Handle.value |> Option.defaultValue "(unverified)")
printfn "PDS: %s" (id.PdsEndpoint |> Option.map string |> Option.defaultValue "(unknown)")
printfn "Key: %s" (id.SigningKey |> Option.defaultValue "(none)")
Error Handling
Identity resolution functions return errors via the IdentityError discriminated union:
type IdentityError =
| XrpcError of XrpcError // An XRPC call failed (network, auth, etc.)
| DocumentParseError of string // The DID document couldn't be fetched or parsed
You can pattern match on this to handle each case differently:
match result with
| Ok identity ->
printfn "Resolved: %s" (Did.value identity.Did)
| Error (IdentityError.XrpcError xrpcErr) ->
printfn "Network or API error: %A" xrpcErr
| Error (IdentityError.DocumentParseError msg) ->
printfn "Malformed DID document: %s" msg
XrpcError covers failures during XRPC calls -- authentication issues, network problems, rate limiting. DocumentParseError covers cases where a DID document was retrieved but couldn't be parsed into a valid identity (missing required fields, malformed JSON, etc.).
Parsing DID Documents Directly
If you already have a DID document as a JsonElement, you can parse it without making any network calls:
let! response = agent.HttpClient.GetAsync("https://plc.directory/did:plc:z72i7hdynmk6r22z27h6tvur")
let! json = response.Content.ReadAsStringAsync()
let doc = System.Text.Json.JsonSerializer.Deserialize<System.Text.Json.JsonElement>(json)
match Identity.parseDidDocument doc with
| Ok identity -> printfn "Parsed: %s" (Did.value identity.Did)
| Error msg -> printfn "Invalid document: %s" msg
Note that parseDidDocument returns Result<AtprotoIdentity, string> (not IdentityError), since there's no network call involved -- the only failure mode is a malformed document.
When to Use Which Function
Function |
Input |
Returns |
Network Calls |
Verification |
|---|---|---|---|---|
|
|
|
2 (forward + reverse) |
Bidirectional |
|
|
|
1 (XRPC) |
None |
|
|
|
1 (HTTP) |
None |
|
|
|
0 |
None |
Use resolveIdentity when you need confidence that a handle and DID actually belong together -- it's the safest choice for displaying user identity in your application. Use resolveHandle or resolveDid when you only need a quick lookup and trust the input. Use parseDidDocument when you already have the raw DID document in hand.
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>
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>
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>
<summary> AT Protocol identity resolution: DID documents, handle resolution, and bidirectional verification. Supports both <c>did:plc</c> (via PLC directory) and <c>did:web</c> (via .well-known) methods. </summary>
<summary> Fully resolve an AT Protocol identity with bidirectional verification. Accepts either a DID or a handle and performs the forward + reverse resolution needed to confirm the handle-DID binding. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="identifier"> A DID (starting with <c>did:</c>) or a handle (e.g., <c>my-handle.bsky.social</c>). </param>
<returns><c>Ok</c> with the resolved <see cref="AtprotoIdentity" />. If bidirectional verification fails (the reverse lookup does not match), the <c>Handle</c> field is set to <c>None</c> but the identity is still returned. Returns <c>Error</c> on resolution failure. </returns>
<remarks> Bidirectional verification ensures that both directions of the DID-handle binding agree: the DID document must list the handle in <c>alsoKnownAs</c>, and resolving that handle must return the same DID. If either direction fails, the handle is cleared but the identity (DID, PDS endpoint, signing key) is still returned. </remarks>
<example><code> let! identity = Identity.resolveIdentity agent "my-handle.bsky.social" match identity with | Ok id -> printfn "DID: %s, Handle verified: %b" (Did.value id.Did) id.Handle.IsSome | Error msg -> printfn "Resolution failed: %A" msg </code></example>
<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>
<summary>The decentralized identifier (e.g., <c>did:plc:z72i7hdynmk6r22z27h6tvur</c>).</summary>
<summary>The handle claimed in the DID document's <c>alsoKnownAs</c> field, if present and verified.</summary>
<summary> Extract the string representation of a handle. </summary>
<param name="handle">The handle to extract the value from.</param>
<returns>The full handle string (e.g. <c>"my-handle.bsky.social"</c>).</returns>
<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 -> printfn "Valid: %s" (Handle.value handle) | Error e -> printfn "Invalid: %s" e </code></example>
<summary> Resolve a handle to its DID via <c>com.atproto.identity.resolveHandle</c>. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="handle">The handle to resolve (e.g., <c>my-handle.bsky.social</c>).</param>
<returns><c>Ok</c> with the resolved <see cref="Did" /> on success, or <c>Error</c> with an <see cref="IdentityError" /> if the handle cannot be resolved. </returns>
<summary> Parse and validate a DID string. </summary>
<param name="s"> A DID string in the format <c>did:<method>:<method-specific-id></c> (e.g. <c>"did:plc:z72i7hdynmk6r22z27h6tvur"</c> or <c>"did:web:example.com"</c>). </param>
<returns><c>Ok</c> with a validated <see cref="Did" />, or <c>Error</c> with a message describing the validation failure. Validation failures include: null input, exceeding the 2048-character limit, invalid DID syntax, or invalid percent-encoding. </returns>
<example><code> match Did.parse "did:plc:z72i7hdynmk6r22z27h6tvur" with | Ok did -> printfn "Valid: %s" (Did.value did) | Error e -> printfn "Invalid: %s" e </code></example>
<summary> Resolve a DID to an <see cref="AtprotoIdentity" /> by fetching its DID document. </summary>
<param name="agent">An <see cref="AtpAgent" /> whose <c>HttpClient</c> is used for the HTTP request.</param>
<param name="did"> The DID to resolve. Must start with <c>did:plc:</c> (resolved via PLC directory) or <c>did:web:</c> (resolved via <c>.well-known/did.json</c>). </param>
<returns><c>Ok</c> with the parsed identity on success, or <c>Error</c> with an <see cref="IdentityError" /> on HTTP failure or unsupported DID method. </returns>
<summary>The PDS (Personal Data Server) endpoint URL from the DID document's service entries.</summary>
val string: value: 'T -> string
--------------------
type string = System.String
<summary> A resolved AT Protocol identity containing the DID and optional metadata extracted from the DID document. </summary>
<summary>The atproto signing key in multibase encoding from the DID document's verification methods.</summary>
module Result from Microsoft.FSharp.Core
--------------------
[<Struct>] type Result<'T,'TError> = | Ok of ResultValue: 'T | Error of ErrorValue: 'TError
<summary> Errors that can occur during identity resolution. </summary>
<summary>An XRPC call failed (e.g., handle resolution).</summary>
<summary>A DID document could not be fetched or parsed.</summary>
(+0 other overloads)
[<RequiresDynamicCodeAttribute ("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications."); RequiresUnreferencedCodeAttribute ("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")>] System.Text.Json.JsonSerializer.Deserialize<'TValue>(reader: byref<System.Text.Json.Utf8JsonReader>, ?options: System.Text.Json.JsonSerializerOptions) : 'TValue
(+0 other overloads)
System.Text.Json.JsonSerializer.Deserialize<'TValue>(node: System.Text.Json.Nodes.JsonNode, jsonTypeInfo: System.Text.Json.Serialization.Metadata.JsonTypeInfo<'TValue>) : 'TValue
(+0 other overloads)
[<RequiresDynamicCodeAttribute ("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications."); RequiresUnreferencedCodeAttribute ("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")>] System.Text.Json.JsonSerializer.Deserialize<'TValue>(node: System.Text.Json.Nodes.JsonNode, ?options: System.Text.Json.JsonSerializerOptions) : 'TValue
(+0 other overloads)
System.Text.Json.JsonSerializer.Deserialize<'TValue>(element: System.Text.Json.JsonElement, jsonTypeInfo: System.Text.Json.Serialization.Metadata.JsonTypeInfo<'TValue>) : 'TValue
(+0 other overloads)
[<RequiresDynamicCodeAttribute ("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications."); RequiresUnreferencedCodeAttribute ("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")>] System.Text.Json.JsonSerializer.Deserialize<'TValue>(element: System.Text.Json.JsonElement, ?options: System.Text.Json.JsonSerializerOptions) : 'TValue
(+0 other overloads)
System.Text.Json.JsonSerializer.Deserialize<'TValue>(document: System.Text.Json.JsonDocument, jsonTypeInfo: System.Text.Json.Serialization.Metadata.JsonTypeInfo<'TValue>) : 'TValue
(+0 other overloads)
[<RequiresDynamicCodeAttribute ("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications."); RequiresUnreferencedCodeAttribute ("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")>] System.Text.Json.JsonSerializer.Deserialize<'TValue>(document: System.Text.Json.JsonDocument, ?options: System.Text.Json.JsonSerializerOptions) : 'TValue
(+0 other overloads)
System.Text.Json.JsonSerializer.Deserialize<'TValue>([<StringSyntaxAttribute ("Json")>] json: string, jsonTypeInfo: System.Text.Json.Serialization.Metadata.JsonTypeInfo<'TValue>) : 'TValue
(+0 other overloads)
[<RequiresDynamicCodeAttribute ("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications."); RequiresUnreferencedCodeAttribute ("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")>] System.Text.Json.JsonSerializer.Deserialize<'TValue>([<StringSyntaxAttribute ("Json")>] json: string, ?options: System.Text.Json.JsonSerializerOptions) : 'TValue
(+0 other overloads)
<summary>Represents a specific JSON value within a <see cref="T:System.Text.Json.JsonDocument" />.</summary>