The atproto API reference

5 min read

Every method, identity path, token and refusal of the atproto server a site with the social network serves — the reference; the guide is under Building a site.

The guide is The social network. This page says exactly what is there. Module: network; with it off the function answers every request with MethodNotImplemented and the host carries none of its rewrites.

Where it answers

An atproto client asks for /xrpc/<method> at the root of the host it was given and resolves a handle at that host's /.well-known, so the site's own address answers them:

path rewritten to (deploy/_redirects)
/xrpc/* $SUPABASE_URL/functions/v1/atproto/xrpc/:splat
/.well-known/atproto-did the function, which reads the host it was asked on
/.well-known/did.json the same: a member's did:web document on their handle's host, the site's own on the site's

The dev server proxies the same three paths to the function (the kit's Vite plugin). The function's own address is not one a client can use: a client discards any path on the server it is given.

Other apps are the site owner's choice

Other apps signing in is off unless the site's config says network: { apps: true }. Off, every method an app would use — com.atproto.server.createSession, refreshSession, createAppPassword, every app.bsky.* read and write, a space's feed generator — answers 403 Forbidden, "This site does not let other apps sign in. Use the site itself." (clientAllowed in _shared/atproto/ids.js, pinned by build/network.test.mjs). What still answers: com.atproto.server.describeServer, which the site's own account page reads, and, where the site federates, com.atproto.sync.*, com.atproto.identity.resolveHandle and the public repository reads (describeRepo, getRecord, listRecords), which relays and other servers need.

Identity

handle <name>.<NETWORK_HANDLE_DOMAIN>, where the name is the member's username — or, until they take one, m and the last twelve hex of their id. A username always wins. Atproto reserves .localhost, so a local stack uses a domain such as network.test
DID did:web:<handle> (%3A for a port); a member who has joined the wider network has a did:plc instead
the site's own DID did:web:<the site's host> — the server a space's feed generator names
a record's URI at://<did>/<collection>/<rkey>, never stored: made from the author's identity as it is now
a space's post at://<author did>/app.bsky.feed.post/s~<message id, hex> — no record of anybody's; the key says so

network_user_of_name (service role only) answers whose a handle name is; network_people gives a signed-in reader the handle name of everybody they can see.

Signing in

A member signs another app in with an app password, never their own: made on their account page (Other apps), shown once, stored as a hash (network_app_passwords), revoked there — which ends every session made with it. com.atproto.server.createSession takes a handle, a DID or the member's address with an app password; it mints a session from the site's own auth server for that member (a sign-in link minted and redeemed at once, sent nowhere), so a ban, a password change or a sign-out everywhere ends it.

The two tokens are JWTs in shape (scope, sub, aud, exp; access two hours, refresh ninety days) and are never trusted for what they claim: a token is its hash in network_sessions. An expired access token answers ExpiredToken and the client refreshes. The site's own session is accepted too, which is how the account page makes app passwords. Ten wrong passwords from one address in an hour are refused with RateLimitExceeded.

Methods

method what it does here
com.atproto.server.describeServer the site's DID and its handle domain; invite code required
createSession · refreshSession · getSession · deleteSession as above
createAppPassword · listAppPasswords · revokeAppPassword the member's own
createAccount refused: accounts are made on the site, and the refusal says where
checkAccountStatus active
com.atproto.identity.resolveHandle a handle on this site, to its DID
updateHandle refused: a handle is the username, changed on the account page
com.atproto.repo.createRecord · putRecord · deleteRecord · applyWrites the writer's own repository only; validated against Bluesky's lexicons
getRecord · listRecords · describeRepo under the same guards as every read
uploadBlob a picture (png, jpeg, gif, webp, up to 10 MB) or a video (mp4, webm, mov, up to 100 MB), named by its hash, kept in the network bucket under the member's id; a post's record carries up to four pictures (app.bsky.embed.images) or one video (app.bsky.embed.video), and a post holding more, or a mix, goes as its first four pictures with a link to the whole post in its text
app.bsky.actor.getProfile(s) · searchActors(Typeahead) · getPreferences · putPreferences profiles with counts and where the reader stands; search among the people the reader can see (network_find); an app's own settings (network_prefs)
app.bsky.feed.getTimeline · getAuthorFeed · getPostThread · getPosts · getLikes · getRepostedBy over the tables, as the reader
getFeedGenerator(s) · getSuggestedFeeds · describeFeedGenerator · getFeed · getFeedSkeleton each space with a feed channel the reader belongs to, as a feed: its posts to a member, nothing to anybody else
app.bsky.graph.getFollows · getFollowers · getBlocks · getMutes · muteActor · unmuteActor · getRelationships
app.bsky.notification.listNotifications · getUnreadCount · updateSeen likes, reposts, follows, replies and quotes
app.bsky.labeler.getServices, app.bsky.unspecced.*, lists and starter packs empty: the site has none
chat.bsky.* not served: direct messages are the spaces'

What a record becomes:

collection written as
app.bsky.feed.post a network post; its audience is everyone for a member who has joined the wider network and the people here for everybody else, since a client cannot ask. An answer to a space's post is a reply in that room
app.bsky.feed.like a like; on a space's post, the room's own ❤️ reaction
app.bsky.feed.repost a repost; a space's post cannot be reposted
app.bsky.graph.follow · app.bsky.graph.block a follow, a block
app.bsky.actor.profile the display name and the line on the site's own profile

A picture in a view is an address the reader's own session signed for an hour. A drawn face has no address, so an author without an uploaded picture shows the app's default.

Where the site federates

POST /network/join · POST /network/leave the member's own act, with the site's session (the account page's card); refused on a closed site
com.atproto.sync.subscribeRepos the event stream: every commit, identity and account event of a joined member, numbered (network_seq), from the relay's cursor, then live; a cursor ahead of the stream is FutureCursor
getRepo · getLatestCommit · getRecord · getRepoStatus · listRepos · getBlob a joined member's public repository, to anybody; a record not in it is not found; a member who left or was suspended answers RepoDeactivated or RepoTakendown
a read about somebody elsewhere (a profile, an author's feed, a thread, posts, follows, likes, reposts) asked of the app view (network.appview, Bluesky's by default) as the member, with a service token signed by their key; a member who has not joined is refused, saying why
getTimeline, listNotifications the first page merges the app view's with the site's own
POST /internal/project · POST /internal/crawl the service role only: the database asking for repositories to be made agree, and the daily crawl request

A commit event carries the new blocks and the blocks its proofs pass through, and prevData (the tree's root before), so a relay verifies and inverts it from the event alone. The PLC directory is NETWORK_PLC_URL (https://plc.directory unless set); the relay NETWORK_RELAY_URL or the config's network.relay.

Refusals

error
a closed site asked for com.atproto.sync.* or service auth MethodNotImplemented — the network is closed
a closed site asked about somebody elsewhere the same
a write to somebody else's repository Forbidden
a reply or quote wider than its post, across a block, by a suspended member refused by the database, said as InvalidRecord or Forbidden
a record that is not its lexicon InvalidRecord
a method this site does not serve MethodNotImplemented

Testing

build/atproto.test.mjs drives the served function with Bluesky's own client: app passwords, signing in by handle and address and refusing the site password, three posts and three readers by every method, a space's post by record, in a list and in a thread, a space's feed to a member and an outsider, the timeline, likes, reposts, replies, notifications, a picture uploaded and posted, a record read back, the closed site's refusals, and revoking an app password. It skips itself, saying why, when the function is not served.