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
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
The DID of the authenticated user, or undefined if not authenticated
Gets the authenticated user’s DID, or throws an error if not authenticated
The session manager instance used by this agent
Feed & Content Methods
getTimeline
(params?, opts?) => Promise<Response>
Fetch the authenticated user’s home timelineconst timeline = await agent.getTimeline({
limit: 50,
cursor: undefined,
})
getAuthorFeed
(params, opts?) => Promise<Response>
Fetch posts from a specific userconst feed = await agent.getAuthorFeed({
actor: 'alice.bsky.social',
limit: 30,
})
getPostThread
(params, opts?) => Promise<Response>
Fetch a post thread with repliesconst 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 URIconst posts = await agent.getPosts({
uris: ['at://...', 'at://...']
})
post
(record: Partial<PostRecord>) => Promise<Response>
Create a new postconst result = await agent.post({
text: 'Hello, world!',
createdAt: new Date().toISOString(),
})
deletePost
(postUri: string) => Promise<void>
Delete a postawait agent.deletePost('at://...')
Social Interactions
like
(uri: string, cid: string, via?) => Promise<Response>
Like a postawait agent.like(post.uri, post.cid)
deleteLike
(likeUri: string) => Promise<void>
Remove a likeawait agent.deleteLike(likeUri)
repost
(uri: string, cid: string, via?) => Promise<Response>
Repost a postawait agent.repost(post.uri, post.cid)
deleteRepost
(repostUri: string) => Promise<void>
Remove a repostawait agent.deleteRepost(repostUri)
follow
(did: string, via?) => Promise<Response>
Follow a userawait agent.follow('did:plc:...')
deleteFollow
(followUri: string) => Promise<void>
Unfollow a userawait agent.deleteFollow(followUri)
Profile Management
getProfile
(params, opts?) => Promise<Response>
Fetch a user profileconst profile = await agent.getProfile({
actor: 'alice.bsky.social'
})
getProfiles
(params, opts?) => Promise<Response>
Fetch multiple user profilesconst profiles = await agent.getProfiles({
actors: ['alice.bsky.social', 'bob.bsky.social']
})
upsertProfile
(updateFn) => Promise<void>
Update the authenticated user’s profileawait 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 userconst notifications = await agent.listNotifications({
limit: 50
})
countUnreadNotifications
(params?, opts?) => Promise<Response>
Get the count of unread notificationsconst { count } = await agent.countUnreadNotifications()
updateSeenNotifications
(seenAt?: string) => Promise<Response>
Mark notifications as seenawait agent.updateSeenNotifications()
Moderation
mute
(actor: string) => Promise<Response>
Mute a userawait agent.mute('did:plc:...')
unmute
(actor: string) => Promise<Response>
Unmute a userawait agent.unmute('did:plc:...')
Preferences
getPreferences
() => Promise<BskyPreferences>
Fetch user preferences including moderation settingsconst prefs = await agent.getPreferences()
addMutedWord
(mutedWord) => Promise<void>
Add a muted wordawait agent.addMutedWord({
value: 'spam',
targets: ['content', 'tag'],
actorTarget: 'all',
})
removeMutedWord
(mutedWord) => Promise<void>
Remove a muted wordawait agent.removeMutedWord(mutedWord)
Labelers
configureLabelers
(labelerDids: string[]) => void
Configure labeler DIDs for this agent instanceagent.configureLabelers(['did:plc:...'])
getLabelDefinitions
(prefs) => Promise<LabelDefinitions>
Fetch label definitions from configured labelersconst prefs = await agent.getPreferences()
const labelDefs = await agent.getLabelDefinitions(prefs)
Proxying
configureProxy
(value: string | null) => void
Configure AT Protocol proxy headeragent.configureProxy('did:plc:...#atproto_labeler')
withProxy
(serviceType, did) => Agent
Create a cloned agent with proxy configurationconst 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)
Optional pre-existing facets (mentions, links, tags)
Whether to clean up excessive newlines
Properties
The UTF-16 encoded text string
The UTF-8 byte length of the text
The grapheme cluster length (visual characters)
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
Auto-detect facets without resolving mentions to DIDsconst 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
delete
(startIndex: number, endIndex: number) => RichText
Delete text in a range, updating facet indices
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
)
The AT Protocol service URL (e.g., https://bsky.social)
Custom fetch implementation
Callback for persisting session data
Methods
login
(opts: AtpAgentLoginOpts) => Promise<Response>
Login with identifier and passwordawait session.login({
identifier: 'alice.bsky.social',
password: 'xxxx-xxxx-xxxx-xxxx',
})
Logout and clear the session
resumeSession
(session: AtpSessionData) => Promise<Response>
Resume a previously saved sessionawait session.resumeSession(savedSession)
createAccount
(data) => Promise<Response>
Create a new accountawait 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
})
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