ATProto PHP "Library"

GithubATPROTO Library to communicate with Bluesky @ Github


A light weight implementation of the atproto protocol to have fun with Bluesky


After looking into the already available libraries to communicate with atproto I decided to go my own way. I somehow abstain from using libraries which include a dozen other libraries in their composer.json while the real communication functionality can be covered by way fewer 3rd party components.

Currently (I would call it pre-alpha) I include only one 3rd party library, namely

  • "tcdent/php-restclient"

Maybe it will become more in the future.

V0.0.0.5 remark

To make it all less of a mess, I renamed almost all functions..... You can find a list of the functions I implemented here:

Implemented functions sorted by

Implemented functions sorted by the name of the


Composer installation

You can install the library with composer by executing

composer require schnoog/php_atproto

This will install the library in the usual vendor directory. Now copy the file /vendor/schnoog/php_atproto/src/config.dist.php to your install directory and rename it to config.php

or use the following command from within your install directory to copy the file automatically (if there's alreay a file name config.php in the install directory it will not be overwritten.

php -r 'if(!file_exists(__DIR__ . "/config.php")) copy (__DIR__ . "/vendor/schnoog/php_atproto/src/config.dist.php", __DIR__ . "/config.php");'

Change the content of the config.php and enter your credentials

Package installation

This is the way to go if you don't have installed composer at all.

I created a second repository which includes all files required to run this thing.

Download the zipped version from here: Packed php_atproto version

Just unzip it, set your credentials in the config.php and you're ready to go. Yes, it's really that easy

Manual installation

This means you have a copy of the whole repository either by

After you have the files in your desired directory, copy the file config.dist.php into your install directory and rename it to config.php

Don't forget to install the dependency by executing composer install

Change the content of the config.php and enter your credentials

My requirements

I have (currently) only a few:

  • Login (create auth token)
  • Locally store the token
  • If I call a function to f.e. post on Bluesky, I should only be required to call the functions with its' parameteres, the rest should happen behind the scenes

How does it work

I'm not the biggest fan of OOP. Not everything in my digital world has to be a class. So I'll keep the interface purely function (evenso that especially creating a post would be easier to encapsulate in a class)

What's alread there

OK, let's check what's available


This small example just authentificates against the server and stores the token locally. If a local stored token is available and the last validity check was performed more than specified in $config['atproto']['storedsession_validity'] seconds ago, the validity will be checked against the backend (in my case BlueSky)

And here a remindes for myself: Paste if (!atp_session_get())return false; at the beginning of the interface (post, get timeline...) functions.

require_once(__DIR__ . "/src/includer.php");

if (!atp_session_get()){
    echo "Session estalished and checked"  . PHP_EOL;

Posting (into the own timeline)

Yes, this thing actually can post into the own timeline. All you need to do (after including the includer.php) is calling the function.

But this comes with limitations (as everything on earth):

  • Only up to 4 images alles (jpg or png), each up to 1 000 000 bytes
  • Either Webcard or additional images, both together doesn't work

Here's a little bit about the parameters for the main function atp_create_post

  • Thats the text you're sending

$text = "This is the text to post, \n containing mentioning and a link";

  • A simple array of the language keys, but you can also use null instead

$lang = ['de','en'];

  • This will make entered URLs clickable, otherwise they will remain simple text

  • $add_link_facets = true;

  • This will place mentions if you mention someobody in the text, if true the person you mentioned will be informed about it

$add_mentions_facets = true;

  • Here I'm defining 2 images in the root path of this script which will be uploaded. You can also use URLs to images. But even if you only use one image, you have to provide the filename/url within an array

$images = [ __DIR__ . "/pic01.jpg" , __DIR__ . "/pic02.jpg"];

  • Here we add an alternative text to the second image. As you can see this requires the alt text for the first image to be empty

$images_alts = [ '', 'Alt text for second image'];

  • That's the URL for a webcard

As mentioned before, you can either have images or a webcard. If you defined both (and provide the function with them ;) ), only the webcard will be displayed

$website_uri = "";

  • The title displayed on the webcard. If I leave this empty, the script will try to read the title defined by the website

$website_title = "My user defined title";

  • And the description of the website. Just like $website_title this is scrapped by the script if left empty

$website_description = "My user defined description";

  • The filename (or url) to the image to use for the webcard. If none is supplied, the scrapper script tries to find one in the meta tags of the website

$website_image = __DIR__ . "/website_image.png";

OK, let's look how some calls you be done

require_once(__DIR__ . "/src/includer.php");

Let's define some variables
$text = "This is the text to post, \n containing mentioning and a link";
$lang = ['de','en'];
$add_link_facets = true;
$add_mentions_facets = true;
$images = [ __DIR__ . "/pic01.jpg" , __DIR__ . "/pic02.jpg"];  
$images_alts = [ '', 'Alt text for second image']; 
$website_uri  = ""; 
$website_title = "My user defined title"; 
$website_description = "My user defined description";
$website_image = __DIR__ . "/website_image.png"; 

//The most simple text post - parsing of mentions and links is ENABLED by default
$answer = atp_create_post($text);

//Now a post, just like above, but this time with the 2 defined images attached, and the $lang keys
$answer = atp_create_post($text,$lang,true,true,$images,$images_alts);

//And now a post which includes a webcard, for which we only provide the URL
$answer = atp_create_post($text,$lang,true,true,[],[],$website_uri);

//Why not a post with a full user defined webcard? Own title, description and image
$answer = atp_create_post($text,null,true,true,[],[],$website_uri,$website_title,$website_description,$website_image);

Reading the own timeline

Yes, also reading the own timeline (or feed or however it's called) is also quite easy. Tbh, the most easiest thing available

require_once(__DIR__ . "/src/includer.php");

//Get 27 entries of the own timeline and print it by DebugOut
$timeline = atp_get_own_timeline(27);


Search posts

This function is somehow limited. I'm currently unable to receive any valid result from app.bsky.feed.searchPosts. So instead I'm reading from

Not nice, but it works however

require_once(__DIR__ . "/src/includer.php");

//Search for posts containing "Arduino" and print the result
$answer = atp_search_posts_by_term("Arduino");

More info

I'll create a list with the functions and endpoints implemented and will try to keep it up to date.