Getting started

atproto_client is transport-agnostic: every function takes a Client, which is nothing more than a send function you provide. You pick the HTTP backend (erlang httpc, JS fetch, a test stub); the package never performs IO on its own.

1. Wire a Client

On the Erlang target, gleam_httpc works well:

import atproto/xrpc
import atproto_core/xrpc as core_xrpc
import gleam/httpc
import gleam/result
import gleam/string

fn client() -> xrpc.Client {
  xrpc.Client(send: fn(req) {
    httpc.send_bits(req)
    |> result.map_error(fn(error) {
      case error {
        httpc.ResponseTimeout -> core_xrpc.Timeout
        httpc.FailedToConnect(_, _) ->
          core_xrpc.ConnectionFailed(string.inspect(error))
        _ -> core_xrpc.Other(string.inspect(error))
      }
    })
  })
}

Transport failures are structured (TransportError, defined in atproto_core/xrpc): your backend maps its own error type onto InvalidUrl/Timeout/ConnectionFailed/Other, and callers can branch on them instead of string-matching.

For a browser app, use atproto_browser instead: it is the same shapes rebuilt around async fetch and WebCrypto, not a drop-in swap of the send function.

2. Resolve an identity

atproto/identity resolves a handle or DID to its PDS via a Slingshot-compatible resolver (identity.default_resolver points at slingshot.microcosm.blue):

import atproto/identity

let assert Ok(pds) =
  identity.resolve_pds(client(), identity.default_resolver, "someone.bsky.social")

3. Call XRPC

atproto/xrpc has get, post_json, and BitArray variants for blobs. For repo CRUD, atproto/repo wraps the com.atproto.repo.* endpoints, including cursor-following list_records and upload_blob:

import atproto/repo
import gleam/dynamic/decode

let assert Ok(rows) =
  repo.list_records(
    client(),
    pds,
    access_token,
    did,
    "app.bsky.feed.post",
    decode.dynamic,
  )

4. Authenticate

Two options:

Going further

Search Document