Basics

The shortest complete programs in the repository. Each one logs in and does a single thing.

For the prose that explains these, see Posting, Reading content, and The social graph.

Send a post

examples/send_post.py
from atproto import Client


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

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


if __name__ == '__main__':
    main()

Reply to a post

Replies need a ReplyRef naming both the post you are replying to and the root of the thread.

examples/send_reply.py
from atproto import Client, models


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

    root_post_ref = models.create_strong_ref(client.send_post('Post from Python SDK'))

    # Reply to the root post. We need to pass ReplyRef with root and parent
    reply_to_root = models.create_strong_ref(
        client.send_post(
            text='Reply to the root post',
            reply_to=models.AppBskyFeedPost.ReplyRef(parent=root_post_ref, root=root_post_ref),
        )
    )

    # To reply on reply, we need to change the "parent" field. Let's reply to our previous reply
    client.send_post(
        text='Reply to the parent reply',
        reply_to=models.AppBskyFeedPost.ReplyRef(parent=reply_to_root, root=root_post_ref),
    )


if __name__ == '__main__':
    main()

Send rich text

Links, mentions, and hashtags are byte ranges over the post text, not markup. TextBuilder computes them for you.

examples/send_rich_text.py
from atproto import Client, client_utils

# To send links as "link card" or "quote post" look at the advanced_usage/send_embed.py example.
# There is a more advanced way to send rich text without helper class in the advanced_usage/send_rich_text.py example.


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

    text_builder = client_utils.TextBuilder()
    text_builder.tag('This is a rich message. ', 'atproto')
    text_builder.text('I can mention ')
    text_builder.mention('account', 'did:plc:kvwvcn5iqfooopmyzvb4qzba')
    text_builder.text(' and add clickable ')
    text_builder.link('link', 'https://atproto.blue/')

    # You can pass instance of TextBuilder instead of str to the "text" argument.
    client.send_post(text_builder)  # same with send_image method

    # Same with chaining:
    client.send_post(client_utils.TextBuilder().text('Test msg using ').link('Python SDK', 'https://atproto.blue/'))


if __name__ == '__main__':
    main()

Send an image

Always set alt text.

examples/send_image.py
from atproto import Client, models


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

    # replace the path to your image file
    with open('cat.jpg', 'rb') as f:
        img_data = f.read()

    # Add image aspect ratio to prevent default 1:1 aspect ratio
    # Replace with your desired aspect ratio
    aspect_ratio = models.AppBskyEmbedDefs.AspectRatio(height=100, width=100)

    client.send_image(
        text='Post with image from Python SDK',
        image=img_data,
        image_alt='Text version of the image (ALT)',
        image_aspect_ratio=aspect_ratio,
    )


if __name__ == '__main__':
    main()

Send several images

examples/send_images.py
from atproto import Client, models


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

    # replace the path to your image file
    paths = ['cat.jpg', 'dog.jpg', 'bird.jpg']
    image_alts = [
        'Text version',
        'of the image (ALT)',
        'This parameter is optional',
    ]

    # Add image aspect ratio to prevent default 1:1 aspect ratio
    # Replace with your desired aspect ratio
    image_aspect_ratios = [
        models.AppBskyEmbedDefs.AspectRatio(height=1, width=1),
        models.AppBskyEmbedDefs.AspectRatio(height=4, width=3),
        models.AppBskyEmbedDefs.AspectRatio(height=16, width=9),
    ]

    images = []
    for path in paths:
        with open(path, 'rb') as f:
            images.append(f.read())

    client.send_images(
        text='Post with image from Python SDK',
        images=images,
        image_alts=image_alts,
        image_aspect_ratios=image_aspect_ratios,
    )


if __name__ == '__main__':
    main()

Send a video

examples/send_video.py
from atproto import Client, models


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

    # replace the path to your video file
    with open('video.mp4', 'rb') as f:
        vid_data = f.read()

    # Add video aspect ratio to prevent default 1:1 aspect ratio
    # Replace with your desired aspect ratio
    aspect_ratio = models.AppBskyEmbedDefs.AspectRatio(height=100, width=100)

    client.send_video(
        text='Post with video from Python SDK',
        video=vid_data,
        video_alt='Text version of the video (ALT)',
        video_aspect_ratio=aspect_ratio,
    )


if __name__ == '__main__':
    main()

Delete a post

examples/delete_post.py
from atproto import Client


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

    # same with the like_post.py example we need to keep a reference to the post
    post_ref = client.send_post('Test send-delete from Python SDK')
    print('Post reference:', post_ref)

    # this method returns True/False depends on the response
    print('Deleted successfully:', client.delete_post(post_ref.uri))


if __name__ == '__main__':
    main()

Read your home timeline

examples/home_timeline.py
from atproto import Client


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

    print('Home (Following):\n')

    # Get "Home" page. Use pagination (cursor + limit) to fetch all posts
    timeline = client.get_timeline(algorithm='reverse-chronological')
    for feed_view in timeline.feed:
        action = 'New Post'
        if feed_view.reason:
            action_by = feed_view.reason.by.handle
            action = f'Reposted by @{action_by}'

        post = feed_view.post.record
        author = feed_view.post.author

        print(f'[{action}] {author.display_name}: {post.text}')


if __name__ == '__main__':
    main()

Read someoneโ€™s posts

examples/profile_posts.py
from atproto import Client


def main(client: Client, handle: str) -> None:
    print(f'\nProfile Posts of {handle}:\n\n')

    # Get profile's posts. Use pagination (cursor + limit) to fetch all
    profile_feed = client.get_author_feed(actor=handle)
    for feed_view in profile_feed.feed:
        print('-', feed_view.post.record.text)


if __name__ == '__main__':
    at_client = Client()
    at_client.login('my-handle', 'my-password')

    while True:
        input_handle = input('\nPlease, enter the handle of the user: ')
        main(at_client, input_handle)

Like and unlike

Liking creates a record. Keep the URI it returns, because undoing the like means deleting that record.

examples/like_post.py
from atproto import Client


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

    post = client.send_post(text='Hello World from Python SDK!')
    print('Post reference:', post)

    print('Like reference:', client.like(uri=post.uri, cid=post.cid))


if __name__ == '__main__':
    main()
examples/unlike_post.py
from atproto import Client


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

    post = client.send_post('Test like-unlike from Python SDK')
    print('Post reference:', post)

    like = client.like(uri=post.uri, cid=post.cid)
    print('Like reference:', like)

    # this method return True/False depends on the response. could throw exceptions too
    print(client.unlike(like.uri))


if __name__ == '__main__':
    main()

Repost

examples/repost_post.py
from atproto import Client


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

    post_ref = client.send_post(text='Hello World from Python SDK!')
    print('Post reference:', post_ref)

    print('Reposted post reference:', client.repost(uri=post_ref.uri, cid=post_ref.cid))


if __name__ == '__main__':
    main()

Process notifications

examples/process_notifications.py
from time import sleep

from atproto import Client

# how often we should check for new notifications
FETCH_NOTIFICATIONS_DELAY_SEC = 3


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

    # fetch new notifications
    while True:
        # save the time in UTC when we fetch notifications
        last_seen_at = client.get_current_time_iso()

        response = client.app.bsky.notification.list_notifications()
        for notification in response.notifications:
            if not notification.is_read:
                print(f'Got new notification! Type: {notification.reason}; from: {notification.author.did}')
                # example: "Got new notification! Type: like; from: did:plc:hlorqa2iqfooopmyzvb4byaz"

        # mark notifications as processed (isRead=True)
        client.app.bsky.notification.update_seen({'seen_at': last_seen_at})
        print('Successfully process notification. Last seen at:', last_seen_at)

        sleep(FETCH_NOTIFICATIONS_DELAY_SEC)


if __name__ == '__main__':
    main()