Quickstart

Log in, post, and read something back. Five minutes.

If you have not installed the SDK yet, see Installation.

Create a client

Everything starts with a client. There are two, differing only in the import and in whether you await the calls. If you are not already using asyncio, use the sync one.

from atproto import Client

client = Client()
from atproto import AsyncClient

client = AsyncClient()

By default the client talks to bsky.social. Pass a base URL to point it somewhere else:

client = Client('https://pds.example.com')

You rarely need to. After you log in, the client reads your accountโ€™s PDS out of its DID document and repoints itself there automatically.

The rest of this page shows the sync version. The async one is identical with await in front.

Log in

client = Client()
profile = client.login('my-handle.bsky.social', 'my-app-password')
print('Welcome,', profile.display_name)

Warning

Use an app password, not your account password. And do not hardcode it: read it from the environment or a secrets store.

createSession is also rate limited by handle at 30 requests per 5 minutes. If your program runs repeatedly, save and reuse the session instead of logging in each time. See Authentication.

Send a post

client.send_post(text='Hello World from the Python SDK!')

Links, mentions, and hashtags are byte ranges over the text rather than markup, so building them by hand is fiddly. TextBuilder does it for you:

from atproto import Client, client_utils

client = Client()
client.login('my-handle.bsky.social', 'my-app-password')

text = client_utils.TextBuilder().text('Hello World from ').link('Python SDK', 'https://atproto.blue')
post = client.send_post(text)

Like what you just posted

send_post returns a reference to the record it created. Most write methods do, and you need that reference to act on the record later:

post = client.send_post(text='Hello World from the Python SDK!')
client.like(post.uri, post.cid)

Read a timeline

timeline = client.get_timeline(algorithm='reverse-chronological')
for feed_view in timeline.feed:
    print(feed_view.post.author.handle, '-', feed_view.post.record.text)

All together

from atproto import Client, client_utils


def main() -> None:
    client = Client()
    profile = client.login('my-handle', 'my-password')
    print('Welcome,', profile.display_name)

    text = client_utils.TextBuilder().text('Hello World from ').link('Python SDK', 'https://atproto.blue')
    post = client.send_post(text)
    client.like(post.uri, post.cid)


if __name__ == '__main__':
    main()
import asyncio

from atproto import AsyncClient, client_utils


async def main() -> None:
    client = AsyncClient()
    profile = await client.login('my-handle', 'my-password')
    print('Welcome,', profile.display_name)

    text = client_utils.TextBuilder().text('Hello World from ').link('Python SDK', 'https://atproto.blue')
    post = await client.send_post(text)
    await client.like(post.uri, post.cid)


if __name__ == '__main__':
    asyncio.run(main())

What you just used, and what is underneath it

send_post, like, get_timeline and their siblings are convenience methods. They are not the AT Protocol API. They are a small hand-written layer over it, covering the things people do most often.

Underneath sits the real thing: every lexicon the network publishes, generated into typed namespaces and Pydantic models. When the convenience layer does not cover what you need, you drop into it:

client.com.atproto.repo.create_record(...)
client.app.bsky.graph.get_follows(...)
client.chat.bsky.convo.list_convos(...)

Nothing is hidden from you. See Records and repositories and Working with models.

Where to go next

  • Concepts: the protocol vocabulary you will meet in every method name.

  • Guides: posting, reading, streaming, error handling, and the rest.

  • Examples: every runnable example in the repository.

  • Custom lexicons: if your project has lexicons of its own.