API Hosts and Auth
Lexicon API definitions do not always indicate which network services implement the endpoint, and whether auth is required when making HTTP requests. This guide describes the most common API request patterns, and lists the specific hostnames for Bluesky-operated services.
As a reminder, the Bluesky application is built on atproto, a decentralized social web protocol. Unlike some social media platforms, there is not one centralized API. More like the classic web, there can be multiple independent service providers and account hosts.
Common Request Types
Most client API requests fall in one of a few categories.
Data record writes, and account management: all public data in the network exists as records in user repositories on their PDS instance, which means all data creation, update, and deletion, for all applications, involves repository API calls to the PDS. This includes things like creating posts, updating profiles, following and unfollowing, etc. These actions require authentication, are made to the user's PDS instance (which needs to be resolved or discovered as part of creating an auth session), and usually involve the com.atproto.* Lexicons. Account management requests, such as updating the account handle, also go directly to the PDS. See PDS Entryway for the distinction between PDS instances and the "entryway" service.
Authenticated Bluesky app requests: API requests relevant to the Bluesky Social app (app.bsky.* Lexicon endpoints) are routed to a Bluesky endpoint. This includes reads, as well as private data writes which don't involve repository records, such as "mutes". In the current atproto architecture, these requests all go through the user PDS instance, and get proxied to the correct service. For most services, the proxying is controlled by the atproto-proxy header.
Public Bluesky app requests: many Bluesky Lexicon endpoints are public, and do not require authentication. These endpoints can be made directly against the Bluesky API, preferably via the https://public.api.bsky.app hostname, which includes additional caching.
Note that it is perfectly fine for authenticated clients to use authenticated requests to hit public Bluesky API endpoints. It is often simpler for authenticated clients to make all requests via the PDS and proxying, instead of juggling multiple API client connections.
Firehose: data updates from the entire network can be streamed over a WebSocket using the com.atproto.sync.subscribeRepos Lexicon endpoint. This endpoint does not require auth, and can be made to individual PDS instances (for data just from that PDS), or to a Relay to receive updates from the entire network.
Other Proxied App Requests: for example, the chat.bsky.* centralized chat/DM APIs, or the tools.ozone.* moderation APIs. These are generally authenticated, routed via the PDS, and use service proxying to route to the relevant service instance.
Other Request Types
There are a few other patterns of API requests.
Fetching Content from Original PDS: sometimes a service or tool needs to request blobs, account status, or repository directly from the original PDS. These are usually un-authenticated requests, and use com.atproto.* Lexicons. The specific PDS hostname needs to be resolved from the relevant account's identity (DID document).
Inter-Service Requests: most clients can rely on PDS instances to handle service request proxying, but PDS implementations themselves need to handle those requests. Service DIDs need to be resolved to specific HTTPS hostnames, and service auth tokens generated and signed. Receiving services need to decode and verify the auth token.
Admin Auth: used for a few specific operational tasks, like administering PDS instances, or bulk operations against Ozone moderation services. Requests are made directly to the relevant service, using fixed/static Bearer tokens.
Bulk Data Requests: for example, when loading existing data from the network in to a new service instance, like a standalone app. It is possible to distribute load across all the PDS instances, or centralize requests to a Relay instance, which may have a faster network connection. For most apps, Jetstream's network replay is the simpler path — it serves filtered history over HTTP without a per-PDS crawl.
Bluesky Services
This table summarizes the hostnames for Bluesky-operated atproto network services.
| Type | Host URL | Service DID |
|---|---|---|
| Relay | https://bsky.network | n/a |
| Entryway | https://bsky.social | n/a |
| PDS Instances | https://<NAME>.<REGION>.host.bsky.network | n/a |
| Bluesky App | https://api.bsky.app | did:web:api.bsky.app#bsky_appview |
| Chat / DMs | https://api.bsky.chat | did:web:api.bsky.chat#bsky_chat |
| Ozone / Moderation | https://mod.bsky.app | did:plc:ar7c4by46qjdydhdevvrndac#atproto_labeler |
Service Auth
There are two types of auth in the atproto network: client–server auth (where a user authenticates to their PDS, typically via OAuth) and service-to-service auth.
The PDS is the root of a user's authority. Service auth is used when making a request from the PDS to another service on behalf of the user — for example, the video upload flow mints a short-lived service token scoped to the video service.
These service auth tokens are a simple asymmetrically-signed JWT. The payload looks like:
type Payload = {
iss: // user's DID
aud: // DID of the service that the request is being made to
exp: // expiration date of the token, normally set to a short timeframe (<60s)
}
The JWT is signed by the signing key in the user's DID document (the same key that signs repository updates). For more detail, see the atproto specs.
To create and verify service auth tokens, you can use the @atproto/xrpc-server library:
import { createServiceJwt, verifyServiceJwt } from '@atproto/xrpc-server'
import { IdResolver } from '@atproto/identity'
// Creating a service JWT
const keypair = // users keypair
const jwt = await createServiceJwt({
iss: // usersDid
aud: 'did:example:server',
keypair,
})
// Verifying a service JWT
// helper method to resolve a user's DID to their atproto signing key
const idResolver = new IdResolver()
const getSigningKey = async (
did: string,
forceRefresh: boolean,
): Promise<string> => {
return this.idResolver.did.resolveAtprotoKey(did, forceRefresh)
}
// it is important to always check the audience of the provided service JWT
const payload = await verifyServiceJwt(jwt, 'did:example:server', getSigningKey)