Bluesky

Bluesky AT Protocol: Create a Post

What publishing a post on the AT Protocol (Bluesky’s underlying protocol) actually involves.

Quick answer

On the AT Protocol there is no central 'create post' endpoint: publishing means writing an app.bsky.feed.post record into your account's own repository on its Personal Data Server via com.atproto.repo.createRecord, after exchanging an app password for a session (com.atproto.server.createSession) and uploading any images first with com.atproto.repo.uploadBlob. PostGate collapses all of it into one POST /api/posts call.

Every other guide in this cluster treats posting to Bluesky as a REST call: send a body, get a permalink back. That's accurate, and it's all you need if you're publishing through PostGate. This page goes one level deeper, into what actually happens on the wire when that post is created — because the AT Protocol (the "AT" is for "Authenticated Transfer") is not shaped like Instagram's or LinkedIn's publishing APIs, and pretending it is would be dishonest.

None of this is something you need to implement yourself to use PostGate — it's handled for you. Read it if you want to understand the protocol underneath the connected account, not to build against it directly.

01

Why is there no “create a Bluesky post” endpoint?

A typical social API — Instagram's Graph API, for instance — has a single service you talk to, and "publishing" means telling that service's database to insert a row visible to followers. The AT Protocol inverts that: your account has its own repository (a signed, versioned data store) hosted on a Personal Data Server (PDS). Publishing a post means writing a new record directly into your own repo — not calling Bluesky the company's servers to insert something into a shared database they own.

Separate services — an "AppView" that indexes and serves the Bluesky app's feeds, and relays that pass data between PDS instances — pick that record up afterward and make it visible to other users. The write and the visibility are two different systems.

02

A post is a record in a collection, not a row in a table

The write itself goes through com.atproto.repo.createRecord, addressed by three things: the repo (the account's DID — a Decentralized Identifier, its permanent address on the network), the collection — app.bsky.feed.post for an ordinary post — and the record itself, a JSON object matching that collection's lexicon (AT Protocol's schema definition format).

A minimal post record carries $type: "app.bsky.feed.post", text, and a createdAt timestamp. Optional fields include langs, facets (structured ranges marking links, mentions, and hashtags inside the text so clients can render them as rich text instead of parsing it themselves), and embed for attached images, external link cards, video, or quoted posts. The lexicon caps text at 300 graphemes and 3,000 UTF-8 bytes — the byte ceiling exists separately from the grapheme count because some scripts use far more bytes per visible character.

03

Why are images uploaded before the post record even exists?

An image can't be embedded inline in the record the way you might attach a file to a form. It's uploaded first, on its own, through com.atproto.repo.uploadBlob, which returns a blob reference — essentially a content hash and size, not the image data itself. That reference is what goes inside the post record's embed.images array, alongside required alt text. Only after the blob exists does the post-creation call happen. It's closer to how S3 pre-signed uploads work than to a typical multipart form post.
04

Authentication is a session, not a bearer token you mint once

An app password (or an OAuth grant, for clients that support it) is exchanged for a session via com.atproto.server.createSession, which returns a short-lived access JWT and a longer-lived refresh JWT — closer to a typical web session pair than a single long-lived API key. A client talking to the protocol directly has to handle refreshing that access token as it expires; this is one of the several things PostGate's connection layer absorbs so a per-post API call doesn't need to think about session lifetime at all.
05

What does PostGate collapse back into one request?

DID resolution, session management and refresh, blob upload before record creation, lexicon validation, and facet generation for links/mentions/hashtags are all real steps in a direct AT Protocol integration. PostGate performs them behind a single POST /api/posts call, the same envelope used for every other platform it supports.

If you're integrating through PostGate, the practical guide is Bluesky API: Post — this page exists so that when something surfaces a protocol-level term (a DID, a blob ref, a lexicon validation error), you know what it refers to.

Frequently asked questions

What endpoint creates a post on the AT Protocol?

com.atproto.repo.createRecord, addressed by three things: the repo (the account’s DID, its permanent address on the network), the collection (app.bsky.feed.post for an ordinary post), and the record itself — a JSON object matching that collection’s lexicon, carrying at minimum $type, text, and a createdAt timestamp. The write goes into the account’s own repository on its Personal Data Server, not into a shared database Bluesky owns.

What is the Bluesky post length limit at the protocol level?

The lexicon caps text at 300 graphemes and 3,000 UTF-8 bytes. The byte ceiling exists separately from the grapheme count because some scripts use far more bytes per visible character — so a post can be within the grapheme limit and still exceed the byte limit.

How do image uploads work on the AT Protocol?

An image can’t be embedded inline in the record. It is uploaded first, on its own, through com.atproto.repo.uploadBlob, which returns a blob reference — essentially a content hash and size, not the image data. That reference goes inside the post record’s embed.images array, alongside required alt text, and only after the blob exists does the post-creation call happen. It is closer to S3 pre-signed uploads than to a multipart form post.

What is a DID on Bluesky?

A Decentralized Identifier — the account’s permanent address on the network. It identifies the repository (a signed, versioned data store hosted on a Personal Data Server) that createRecord writes into. Separate services — an AppView that indexes feeds, and relays passing data between PDS instances — pick the record up afterward and make it visible to other users.

Do I need to implement AT Protocol details to post to Bluesky through PostGate?

No. DID resolution, session management and refresh, blob upload before record creation, lexicon validation, and facet generation for links, mentions, and hashtags are all real steps in a direct integration — and PostGate performs them behind a single POST /api/posts call, the same envelope used for every other platform it supports.

Build it with PostGate

One API for every social platform your customers care about — sandboxed, documented, and ready today.

Start building Read the docs