Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/bluesky-social/atproto/llms.txt

Use this file to discover all available pages before exploring further.

The @atproto/xrpc package is a low-level TypeScript client library for making XRPC (Cross-organizational RPC) calls to AT Protocol services. It handles request/response encoding, Lexicon validation, and error handling.

Installation

npm install @atproto/xrpc

Overview

XRPC is the RPC protocol used by AT Protocol. It defines how clients and servers communicate over HTTP, with schema validation powered by Lexicons. Key features:
  • Schema-based request/response validation
  • Automatic encoding/decoding of request bodies
  • Built-in error handling
  • Support for custom fetch handlers
  • Type-safe method calls
Most applications should use @atproto/api which provides a higher-level interface. Use @atproto/xrpc directly when you need low-level control or are working with custom Lexicons.

Quick Start

import { XrpcClient } from '@atproto/xrpc'
import { LexiconDoc } from '@atproto/lexicon'

// Define a lexicon
const pingLexicon: LexiconDoc = {
  lexicon: 1,
  id: 'io.example.ping',
  defs: {
    main: {
      type: 'query',
      description: 'Ping the server',
      parameters: {
        type: 'params',
        properties: {
          message: { type: 'string' }
        },
      },
      output: {
        encoding: 'application/json',
        schema: {
          type: 'object',
          required: ['message'],
          properties: {
            message: { type: 'string' }
          },
        },
      },
    },
  },
}

// Create client
const xrpc = new XrpcClient('https://api.example.com', [pingLexicon])

// Make a call
const response = await xrpc.call('io.example.ping', {
  message: 'hello world',
})

console.log(response.data) // { message: 'hello world' }

XrpcClient

The main class for making XRPC calls.

Constructor

new XrpcClient(
  fetchHandlerOpts: FetchHandler | FetchHandlerObject | FetchHandlerOptions,
  lex: Lexicons | Iterable<LexiconDoc>
)
fetchHandlerOpts
FetchHandler | FetchHandlerObject | FetchHandlerOptions
required
Fetch handler configuration. Can be:
  • A string URL (base service URL)
  • A custom fetch function
  • An object with service URL and optional fetch function
  • A FetchHandler function
lex
Lexicons | Iterable<LexiconDoc>
required
Lexicon definitions for schema validation. Can be:
  • A Lexicons instance
  • An array of LexiconDoc objects

Simple URL Constructor

const xrpc = new XrpcClient('https://api.example.com', lexicons)

Custom Fetch Constructor

const customFetch: FetchHandler = async (url: string, init: RequestInit) => {
  console.log('Request:', url)
  const response = await fetch(url, init)
  console.log('Response:', response.status)
  return response
}

const xrpc = new XrpcClient(customFetch, lexicons)

Object Constructor

const xrpc = new XrpcClient(
  {
    service: 'https://api.example.com',
    fetch: customFetch, // optional
  },
  lexicons
)

Making XRPC Calls

call Method

xrpc.call(
  methodNsid: string,
  params?: QueryParams,
  data?: unknown,
  opts?: CallOptions
): Promise<XRPCResponse>
methodNsid
string
required
The NSID (Namespaced Identifier) of the method to call (e.g., com.atproto.repo.getRecord)
params
QueryParams
Query parameters for the request
data
unknown
Request body data (for procedures)
opts
CallOptions
Additional options:
  • encoding - Content-Type for the request body
  • signal - AbortSignal for request cancellation
  • headers - Additional headers

Query Example

const response = await xrpc.call(
  'com.atproto.repo.getRecord',
  {
    repo: 'did:plc:z72i7hdynmk6r22z27h6tvur',
    collection: 'app.bsky.feed.post',
    rkey: '3l4yhqsgvzc2g',
  }
)

console.log(response.data)

Procedure Example

const response = await xrpc.call(
  'com.atproto.repo.createRecord',
  undefined, // no query params
  {
    repo: 'did:plc:z72i7hdynmk6r22z27h6tvur',
    collection: 'app.bsky.feed.post',
    record: {
      $type: 'app.bsky.feed.post',
      text: 'Hello, world!',
      createdAt: new Date().toISOString(),
    },
  }
)

console.log(response.data.uri)

Header Management

Setting Headers

xrpc.setHeader('Authorization', 'Bearer token123')

// Dynamic headers
xrpc.setHeader('Authorization', () => `Bearer ${getToken()}`)
setHeader
(key: string, value: string | (() => string | null)) => void
Set a header that will be included in all requests. The value can be a string or a function that returns a string or null.

Removing Headers

xrpc.unsetHeader('Authorization')
unsetHeader
(key: string) => void
Remove a previously set header

Clearing All Headers

xrpc.clearHeaders()
clearHeaders
() => void
Remove all custom headers

Response Types

XRPCResponse

class XRPCResponse {
  success: true
  data: any
  headers: HeadersMap
}
success
true
Always true for successful responses
data
any
The response body, parsed according to the content type
headers
HeadersMap
Response headers as a key-value object

Error Handling

XRPCError

All XRPC errors are instances of XRPCError:
class XRPCError extends Error {
  success: false
  status: ResponseType
  error?: string
  message: string
  headers?: HeadersMap
}
success
false
Always false for errors
status
ResponseType
Numeric response type enum value
error
string | undefined
Error code from the server (e.g., InvalidRequest, RateLimitExceeded)
message
string
Error message
headers
HeadersMap | undefined
Response headers if available

ResponseType Enum

enum ResponseType {
  Unknown = 1,
  InvalidResponse = 2,
  Success = 200,
  InvalidRequest = 400,
  AuthenticationRequired = 401,
  Forbidden = 403,
  XRPCNotSupported = 404,
  NotAcceptable = 406,
  PayloadTooLarge = 413,
  UnsupportedMediaType = 415,
  RateLimitExceeded = 429,
  InternalServerError = 500,
  MethodNotImplemented = 501,
  UpstreamFailure = 502,
  NotEnoughResources = 503,
  UpstreamTimeout = 504,
}

Error Handling Example

import { XRPCError, ResponseType } from '@atproto/xrpc'

try {
  const response = await xrpc.call('io.example.method', params)
  console.log(response.data)
} catch (error) {
  if (error instanceof XRPCError) {
    console.error('XRPC Error:', error.status, error.error, error.message)
    
    switch (error.status) {
      case ResponseType.RateLimitExceeded:
        // Wait and retry
        break
      case ResponseType.AuthenticationRequired:
        // Refresh auth token
        break
      case ResponseType.InvalidRequest:
        // Handle validation error
        break
      default:
        // Handle other errors
    }
  } else {
    // Handle non-XRPC errors
    console.error('Unknown error:', error)
  }
}

Custom Fetch Handlers

Fetch handlers allow you to customize request behavior, add authentication, implement retries, etc.

Basic Fetch Handler

type FetchHandler = (input: string, init?: RequestInit) => Promise<Response>

const myFetch: FetchHandler = async (url, init) => {
  console.log('Making request to:', url)
  return fetch(url, init)
}

const xrpc = new XrpcClient(myFetch, lexicons)

Authenticated Fetch Handler

const session = {
  serviceUrl: 'https://api.example.com',
  token: 'initial-token',
  async refreshToken() {
    const response = await fetch('https://auth.example.com/refresh', {
      method: 'POST',
      headers: { Authorization: `Bearer ${this.token}` },
    })
    const { token } = await response.json()
    this.token = token
    return token
  },
}

const authenticatedFetch: FetchHandler = async (url, init) => {
  const headers = new Headers(init?.headers)
  headers.set('Authorization', `Bearer ${session.token}`)
  
  const response = await fetch(new URL(url, session.serviceUrl), {
    ...init,
    headers,
  })
  
  // Handle token refresh on 401
  if (response.status === 401) {
    const newToken = await session.refreshToken()
    headers.set('Authorization', `Bearer ${newToken}`)
    return fetch(new URL(url, session.serviceUrl), { ...init, headers })
  }
  
  return response
}

const xrpc = new XrpcClient(authenticatedFetch, lexicons)

Retry Fetch Handler

const retryFetch: FetchHandler = async (url, init) => {
  const maxRetries = 3
  let lastError: Error | undefined
  
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fetch(url, init)
    } catch (error) {
      lastError = error as Error
      if (i < maxRetries - 1) {
        await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)))
      }
    }
  }
  
  throw lastError
}

const xrpc = new XrpcClient(retryFetch, lexicons)

Call Options

Encoding

Specify the content type for request bodies:
await xrpc.call(
  'io.example.upload',
  undefined,
  imageData,
  { encoding: 'image/jpeg' }
)

Abort Signals

Cancel requests using AbortController:
const controller = new AbortController()

const promise = xrpc.call(
  'io.example.method',
  params,
  undefined,
  { signal: controller.signal }
)

// Cancel the request
controller.abort()

try {
  await promise
} catch (error) {
  if (error.name === 'AbortError') {
    console.log('Request was cancelled')
  }
}

Per-Request Headers

await xrpc.call(
  'io.example.method',
  params,
  undefined,
  {
    headers: {
      'X-Custom-Header': 'value',
    }
  }
)

Working with Lexicons

Lexicon Structure

A lexicon defines the schema for an XRPC method:
import { LexiconDoc } from '@atproto/lexicon'

const lexicon: LexiconDoc = {
  lexicon: 1,
  id: 'io.example.createPost',
  defs: {
    main: {
      type: 'procedure',
      description: 'Create a new post',
      input: {
        encoding: 'application/json',
        schema: {
          type: 'object',
          required: ['text'],
          properties: {
            text: { type: 'string', maxLength: 300 },
            createdAt: { type: 'string', format: 'datetime' },
          },
        },
      },
      output: {
        encoding: 'application/json',
        schema: {
          type: 'object',
          required: ['uri', 'cid'],
          properties: {
            uri: { type: 'string' },
            cid: { type: 'string' },
          },
        },
      },
    },
  },
}

Multiple Lexicons

import { Lexicons } from '@atproto/lexicon'

const lex = new Lexicons([
  lexicon1,
  lexicon2,
  lexicon3,
])

const xrpc = new XrpcClient('https://api.example.com', lex)

Adding Lexicons at Runtime

const xrpc = new XrpcClient('https://api.example.com', [])

// Access the lexicons instance
xrpc.lex.add(newLexicon)

Types

QueryParams

type QueryParams = Record<string, any>
Query parameters can be any serializable values:
const params: QueryParams = {
  limit: 50,
  cursor: 'abc123',
  tags: ['tag1', 'tag2'],
  includeDeleted: false,
}

HeadersMap

type HeadersMap = Record<string, string | undefined>

CallOptions

interface CallOptions {
  encoding?: string
  signal?: AbortSignal
  headers?: HeadersMap
}

Gettable

type Gettable<T> = T | (() => T)
A value that can be static or computed via a function.

Client (Deprecated)

The Client class is deprecated. Use XrpcClient instead.
For backwards compatibility, the legacy Client class is still available:
import { Client } from '@atproto/xrpc'

const client = new Client()

const response = await client.call(
  'https://api.example.com',
  'io.example.method',
  params
)

Advanced Usage

Streaming Requests

For streaming request bodies (e.g., file uploads):
const stream = new ReadableStream({
  start(controller) {
    // Push data to stream
    controller.enqueue(chunk1)
    controller.enqueue(chunk2)
    controller.close()
  },
})

await xrpc.call(
  'io.example.upload',
  undefined,
  stream,
  { encoding: 'application/octet-stream' }
)

Binary Responses

The client automatically handles different response types based on Content-Type headers:
const response = await xrpc.call('io.example.download', { id: '123' })

// response.data is automatically parsed based on Content-Type:
// - application/json -> parsed JSON
// - text/* -> string
// - application/octet-stream -> Uint8Array
// - image/* -> Uint8Array

Validation Errors

When response validation fails:
import { XRPCInvalidResponseError } from '@atproto/xrpc'

try {
  await xrpc.call('io.example.method', params)
} catch (error) {
  if (error instanceof XRPCInvalidResponseError) {
    console.error('Lexicon NSID:', error.lexiconNsid)
    console.error('Validation error:', error.validationError)
    console.error('Response body:', error.responseBody)
  }
}

Best Practices

1. Reuse Client Instances

Create one client instance and reuse it:
// Good
const xrpc = new XrpcClient('https://api.example.com', lexicons)

// Use xrpc for all calls
await xrpc.call('method1', params1)
await xrpc.call('method2', params2)

2. Handle Errors Appropriately

try {
  const response = await xrpc.call('method', params)
  return response.data
} catch (error) {
  if (error instanceof XRPCError) {
    // Handle XRPC-specific errors
    if (error.status === ResponseType.RateLimitExceeded) {
      // Implement backoff
    }
  }
  throw error // Re-throw unexpected errors
}

3. Use TypeScript

Leverage TypeScript for type safety:
import { XRPCResponse } from '@atproto/xrpc'

interface PostOutput {
  uri: string
  cid: string
}

const response = await xrpc.call('io.example.createPost', undefined, {
  text: 'Hello!',
})

const data = response.data as PostOutput
console.log(data.uri)

Examples

Complete Example with Authentication

import { XrpcClient, XRPCError, ResponseType } from '@atproto/xrpc'
import { Lexicons } from '@atproto/lexicon'

// Setup
const lexicons = new Lexicons([/* your lexicons */])

class AuthenticatedClient {
  private xrpc: XrpcClient
  private accessToken?: string
  
  constructor(private serviceUrl: string) {
    this.xrpc = new XrpcClient(
      async (url, init) => this.fetchWithAuth(url, init),
      lexicons
    )
  }
  
  private async fetchWithAuth(url: string, init?: RequestInit): Promise<Response> {
    const headers = new Headers(init?.headers)
    
    if (this.accessToken) {
      headers.set('Authorization', `Bearer ${this.accessToken}`)
    }
    
    return fetch(new URL(url, this.serviceUrl), {
      ...init,
      headers,
    })
  }
  
  async login(identifier: string, password: string) {
    const response = await this.xrpc.call(
      'com.atproto.server.createSession',
      undefined,
      { identifier, password }
    )
    
    this.accessToken = response.data.accessJwt
  }
  
  async createPost(text: string) {
    return this.xrpc.call(
      'com.atproto.repo.createRecord',
      undefined,
      {
        repo: 'did:plc:...',
        collection: 'app.bsky.feed.post',
        record: {
          $type: 'app.bsky.feed.post',
          text,
          createdAt: new Date().toISOString(),
        },
      }
    )
  }
}

// Usage
const client = new AuthenticatedClient('https://bsky.social')
await client.login('alice.bsky.social', 'app-password')
await client.createPost('Hello from XRPC!')

See Also

Build docs developers (and LLMs) love