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/api package is the primary client library for building applications on AT Protocol and Bluesky. It provides high-level APIs for authentication, content management, social features, and moderation.

Installation

npm install @atproto/api

Core Concepts

The package is built around three main classes:
  • Agent - The primary client for making API calls
  • RichText - For handling rich text with mentions, links, and hashtags
  • Moderation APIs - For implementing content moderation

Quick Start

Basic Setup with Credentials

import { Agent, CredentialSession } from '@atproto/api'

const session = new CredentialSession(new URL('https://bsky.social'))
await session.login({
  identifier: 'your.bsky.social',
  password: 'xxxx-xxxx-xxxx-xxxx', // App password
})

const agent = new Agent(session)

OAuth-Based Authentication

For production applications, OAuth is recommended:
import { Agent } from '@atproto/api'
import { OAuthClient } from '@atproto/oauth-client'

const oauthClient = new OAuthClient({
  // OAuth configuration
})

const oauthSession = await oauthClient.restore('did:plc:123')
const agent = new Agent(oauthSession)

Agent

The Agent class is the main interface for interacting with AT Protocol services.

Constructor

new Agent(sessionManager: SessionManager | FetchHandler | FetchHandlerOptions)
sessionManager
SessionManager | FetchHandler | FetchHandlerOptions
required
Session manager instance or fetch handler configuration

Authentication Properties

did
string | undefined
The DID of the authenticated user, or undefined if not authenticated
assertDid
string
Gets the authenticated user’s DID, or throws an error if not authenticated
sessionManager
SessionManager
The session manager instance used by this agent

Feed & Content Methods

getTimeline
(params?, opts?) => Promise<Response>
Fetch the authenticated user’s home timeline
const timeline = await agent.getTimeline({
  limit: 50,
  cursor: undefined,
})
getAuthorFeed
(params, opts?) => Promise<Response>
Fetch posts from a specific user
const feed = await agent.getAuthorFeed({
  actor: 'alice.bsky.social',
  limit: 30,
})
getPostThread
(params, opts?) => Promise<Response>
Fetch a post thread with replies
const thread = await agent.getPostThread({
  uri: 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3l4yhqsgvzc2g',
  depth: 6,
})
getPosts
(params, opts?) => Promise<Response>
Fetch multiple posts by URI
const posts = await agent.getPosts({
  uris: ['at://...', 'at://...']
})
post
(record: Partial<PostRecord>) => Promise<Response>
Create a new post
const result = await agent.post({
  text: 'Hello, world!',
  createdAt: new Date().toISOString(),
})
deletePost
(postUri: string) => Promise<void>
Delete a post
await agent.deletePost('at://...')

Social Interactions

like
(uri: string, cid: string, via?) => Promise<Response>
Like a post
await agent.like(post.uri, post.cid)
deleteLike
(likeUri: string) => Promise<void>
Remove a like
await agent.deleteLike(likeUri)
repost
(uri: string, cid: string, via?) => Promise<Response>
Repost a post
await agent.repost(post.uri, post.cid)
deleteRepost
(repostUri: string) => Promise<void>
Remove a repost
await agent.deleteRepost(repostUri)
follow
(did: string, via?) => Promise<Response>
Follow a user
await agent.follow('did:plc:...')
deleteFollow
(followUri: string) => Promise<void>
Unfollow a user
await agent.deleteFollow(followUri)

Profile Management

getProfile
(params, opts?) => Promise<Response>
Fetch a user profile
const profile = await agent.getProfile({
  actor: 'alice.bsky.social'
})
getProfiles
(params, opts?) => Promise<Response>
Fetch multiple user profiles
const profiles = await agent.getProfiles({
  actors: ['alice.bsky.social', 'bob.bsky.social']
})
upsertProfile
(updateFn) => Promise<void>
Update the authenticated user’s profile
await agent.upsertProfile((existing) => ({
  ...existing,
  displayName: 'Alice',
  description: 'Software developer',
}))

Blob Upload

uploadBlob
(data: BlobData, opts?) => Promise<Response>
Upload a blob (image, video, etc.)
const blobResponse = await agent.uploadBlob(imageData, {
  encoding: 'image/jpeg'
})

const { blob } = blobResponse.data

Notifications

listNotifications
(params?, opts?) => Promise<Response>
List notifications for the authenticated user
const notifications = await agent.listNotifications({
  limit: 50
})
countUnreadNotifications
(params?, opts?) => Promise<Response>
Get the count of unread notifications
const { count } = await agent.countUnreadNotifications()
updateSeenNotifications
(seenAt?: string) => Promise<Response>
Mark notifications as seen
await agent.updateSeenNotifications()

Moderation

mute
(actor: string) => Promise<Response>
Mute a user
await agent.mute('did:plc:...')
unmute
(actor: string) => Promise<Response>
Unmute a user
await agent.unmute('did:plc:...')

Preferences

getPreferences
() => Promise<BskyPreferences>
Fetch user preferences including moderation settings
const prefs = await agent.getPreferences()
addMutedWord
(mutedWord) => Promise<void>
Add a muted word
await agent.addMutedWord({
  value: 'spam',
  targets: ['content', 'tag'],
  actorTarget: 'all',
})
removeMutedWord
(mutedWord) => Promise<void>
Remove a muted word
await agent.removeMutedWord(mutedWord)

Labelers

configureLabelers
(labelerDids: string[]) => void
Configure labeler DIDs for this agent instance
agent.configureLabelers(['did:plc:...'])
getLabelDefinitions
(prefs) => Promise<LabelDefinitions>
Fetch label definitions from configured labelers
const prefs = await agent.getPreferences()
const labelDefs = await agent.getLabelDefinitions(prefs)

Proxying

configureProxy
(value: string | null) => void
Configure AT Protocol proxy header
agent.configureProxy('did:plc:...#atproto_labeler')
withProxy
(serviceType, did) => Agent
Create a cloned agent with proxy configuration
const proxiedAgent = agent.withProxy('atproto_labeler', 'did:plc:...')

Direct Lexicon Access

The agent provides direct access to all AT Protocol and Bluesky lexicons:
// ATProto methods
await agent.com.atproto.repo.createRecord({
  repo: agent.did!,
  collection: 'app.bsky.feed.post',
  record: {
    $type: 'app.bsky.feed.post',
    text: 'Hello, world!',
    createdAt: new Date().toISOString(),
  },
})

// Bluesky methods
await agent.app.bsky.feed.getTimeline({
  limit: 50
})

// Chat methods (if available)
await agent.chat.bsky.convo.listConvos({})

// Moderation tools
await agent.tools.ozone.moderation.queryStatuses({})

RichText

The RichText class handles rich text with proper UTF-8/UTF-16 conversion, facet detection for mentions and links, and text manipulation.

Constructor

new RichText(props: RichTextProps, opts?: RichTextOpts)
props.text
string
required
The text content
props.facets
Facet[]
Optional pre-existing facets (mentions, links, tags)
opts.cleanNewlines
boolean
Whether to clean up excessive newlines

Properties

text
string
The UTF-16 encoded text string
length
number
The UTF-8 byte length of the text
graphemeLength
number
The grapheme cluster length (visual characters)
facets
Facet[] | undefined
The facets (mentions, links, tags) in the text

Facet Detection

detectFacets
(agent: Agent) => Promise<void>
Auto-detect and resolve facets (mentions and links)
const rt = new RichText({
  text: 'Hello @alice.bsky.social, check out https://example.com',
})

await rt.detectFacets(agent)

// Now rt.facets contains detected mentions and links
detectFacetsWithoutResolution
() => void
Auto-detect facets without resolving mentions to DIDs
const rt = new RichText({
  text: 'Hello @alice.bsky.social',
})

rt.detectFacetsWithoutResolution()
// Facets detected but mention DIDs are not resolved

Text Manipulation

insert
(insertIndex: number, text: string) => RichText
Insert text at a specific position, updating facet indices
rt.insert(5, ' there')
delete
(startIndex: number, endIndex: number) => RichText
Delete text in a range, updating facet indices
rt.delete(0, 5)

Iteration

segments
() => Generator<RichTextSegment>
Iterate over text segments (plain text and facets)
for (const segment of rt.segments()) {
  if (segment.isLink()) {
    console.log(`Link: ${segment.link?.uri}`)
  } else if (segment.isMention()) {
    console.log(`Mention: ${segment.mention?.did}`)
  } else if (segment.isTag()) {
    console.log(`Tag: ${segment.tag?.tag}`)
  } else {
    console.log(`Text: ${segment.text}`)
  }
}

Creating Posts with Rich Text

import { Agent, RichText } from '@atproto/api'

const rt = new RichText({
  text: 'Hello @alice.bsky.social! Check out https://example.com #atproto',
})

await rt.detectFacets(agent)

await agent.post({
  text: rt.text,
  facets: rt.facets,
})

Rendering Rich Text

let markdown = ''
for (const segment of rt.segments()) {
  if (segment.isLink()) {
    markdown += `[${segment.text}](${segment.link?.uri})`
  } else if (segment.isMention()) {
    markdown += `[@${segment.text}](https://bsky.app/profile/${segment.mention?.did})`
  } else if (segment.isTag()) {
    markdown += `[#${segment.tag?.tag}](https://bsky.app/hashtag/${segment.tag?.tag})`
  } else {
    markdown += segment.text
  }
}

Moderation

The moderation system helps you apply content filtering, labeling, and user preferences.

Moderating Content

import { moderatePost, moderateProfile } from '@atproto/api'

// Get user preferences
const prefs = await agent.getPreferences()
const labelDefs = await agent.getLabelDefinitions(prefs)

// Moderate a post
const postMod = moderatePost(postView, {
  userDid: agent.did,
  moderationPrefs: prefs.moderationPrefs,
  labelDefs,
})

// Check moderation decisions for feeds
if (postMod.ui('contentList').filter) {
  // Don't show in feeds
}

if (postMod.ui('contentList').blur) {
  // Show behind a cover
  console.log('Reasons:', postMod.ui('contentList').blurs)
}

if (postMod.ui('contentList').alert) {
  // Show warnings
  console.log('Alerts:', postMod.ui('contentList').alerts)
}

// Moderate a profile
const profileMod = moderateProfile(profileView, {
  userDid: agent.did,
  moderationPrefs: prefs.moderationPrefs,
  labelDefs,
})

Moderation Functions

moderatePost
(subject, opts) => ModerationDecision
Get moderation decisions for a post
moderateProfile
(subject, opts) => ModerationDecision
Get moderation decisions for a profile
moderateNotification
(subject, opts) => ModerationDecision
Get moderation decisions for a notification

Moderation UI Contexts

The moderation system supports different UI contexts:
  • contentList - For feed views
  • contentView - For detail views
  • contentMedia - For media embeds
  • avatar - For avatar images
  • profileList - For profile lists
  • profileView - For profile detail views

CredentialSession

The CredentialSession class manages username/password-based authentication.

Constructor

new CredentialSession(
  serviceUrl: URL,
  fetch?: typeof globalThis.fetch,
  persistSession?: AtpPersistSessionHandler
)
serviceUrl
URL
required
The AT Protocol service URL (e.g., https://bsky.social)
fetch
typeof globalThis.fetch
Custom fetch implementation
persistSession
AtpPersistSessionHandler
Callback for persisting session data

Methods

login
(opts: AtpAgentLoginOpts) => Promise<Response>
Login with identifier and password
await session.login({
  identifier: 'alice.bsky.social',
  password: 'xxxx-xxxx-xxxx-xxxx',
})
logout
() => Promise<void>
Logout and clear the session
await session.logout()
resumeSession
(session: AtpSessionData) => Promise<Response>
Resume a previously saved session
await session.resumeSession(savedSession)
createAccount
(data) => Promise<Response>
Create a new account
await session.createAccount({
  email: 'alice@example.com',
  handle: 'alice.bsky.social',
  password: 'secure-password',
})

AtpAgent (Legacy)

The AtpAgent class is deprecated. Use Agent with CredentialSession instead.
For backwards compatibility, AtpAgent is still available:
import { AtpAgent } from '@atproto/api'

const agent = new AtpAgent({
  service: 'https://bsky.social',
})

await agent.login({
  identifier: 'alice.bsky.social',
  password: 'xxxx-xxxx-xxxx-xxxx',
})

Type Exports

The package exports comprehensive TypeScript types for all Lexicons:
import {
  // Post types
  AppBskyFeedPost,
  AppBskyFeedDefs,
  
  // Profile types
  AppBskyActorDefs,
  AppBskyActorProfile,
  
  // Graph types
  AppBskyGraphDefs,
  AppBskyGraphFollow,
  
  // Rich text types
  AppBskyRichtextFacet,
  
  // Common types
  BlobRef,
  AtUri,
} from '@atproto/api'

Validation

All record types include validation methods:
import { AppBskyFeedPost } from '@atproto/api'

const post = {
  $type: 'app.bsky.feed.post',
  text: 'Hello, world!',
  createdAt: new Date().toISOString(),
}

// Type guard
if (AppBskyFeedPost.isRecord(post)) {
  // TypeScript knows post is a PostRecord
}

// Validation
const result = AppBskyFeedPost.validateRecord(post)
if (result.success) {
  // Valid record
  const validPost = result.value
} else {
  // Validation error
  console.error(result.error)
}

Configuration

Global Agent Configuration

import { Agent } from '@atproto/api'

Agent.configure({
  appLabelers: ['did:plc:...'], // Default labelers
})

Custom Headers

agent.setHeader('X-Custom-Header', 'value')
agent.setHeader('Authorization', () => `Bearer ${getToken()}`)
agent.unsetHeader('X-Custom-Header')
agent.clearHeaders()

Custom Fetch

const session = new CredentialSession(
  new URL('https://bsky.social'),
  customFetch
)

Error Handling

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

try {
  await agent.post({ text: 'Hello!' })
} catch (error) {
  if (error instanceof XRPCError) {
    console.error('Status:', error.status)
    console.error('Error:', error.error)
    console.error('Message:', error.message)
    
    if (error.status === ResponseType.RateLimitExceeded) {
      // Handle rate limit
    }
  }
}

Examples

Complete Post Creation Example

import { Agent, CredentialSession, RichText } from '@atproto/api'

const session = new CredentialSession(new URL('https://bsky.social'))
await session.login({
  identifier: 'alice.bsky.social',
  password: 'xxxx-xxxx-xxxx-xxxx',
})

const agent = new Agent(session)

// Upload image
const imageData = await fetch('https://example.com/image.jpg')
  .then(r => r.arrayBuffer())
const uploadResult = await agent.uploadBlob(new Uint8Array(imageData), {
  encoding: 'image/jpeg'
})

// Create rich text
const rt = new RichText({
  text: 'Check out this cool website: https://example.com @alice.bsky.social',
})
await rt.detectFacets(agent)

// Create post with image and rich text
await agent.post({
  text: rt.text,
  facets: rt.facets,
  embed: {
    $type: 'app.bsky.embed.images',
    images: [{
      image: uploadResult.data.blob,
      alt: 'A cool image',
    }],
  },
})

See Also

Build docs developers (and LLMs) love