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>
The NSID (Namespaced Identifier) of the method to call (e.g., com.atproto.repo.getRecord)
Query parameters for the request
Request body data (for procedures)
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)
xrpc.setHeader('Authorization', 'Bearer token123')
// Dynamic headers
xrpc.setHeader('Authorization', () => `Bearer ${getToken()}`)
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.
xrpc.unsetHeader('Authorization')
Remove a previously set header
Remove all custom headers
Response Types
XRPCResponse
class XRPCResponse {
success: true
data: any
headers: HeadersMap
}
Always true for successful responses
The response body, parsed according to the content type
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
}
Numeric response type enum value
Error code from the server (e.g., InvalidRequest, RateLimitExceeded)
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')
}
}
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,
}
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