Skip to content
Bluesky integration · v1

Bluesky API for AT Protocol posting

Bluesky is not OAuth — it is a session against an AT Protocol server. PostMCP refreshes that session, uploads image blobs and writes the feed record for you.

Endpoint
POST /api/tools/create_post
Platform value
"bluesky"
Auth
x-api-key or Bearer token
Upstream
AT Protocol XRPC
Overview

What the Bluesky API gives you

Posting to Bluesky means speaking AT Protocol XRPC directly: create a session with an identifier and app password, upload any image as a blob, then write a record into the app.bsky.feed.post collection with the blob embedded. There is no OAuth screen and no managed media pipeline.

PostMCP wraps all three calls. It refreshes the session before every broadcast so stale JWTs never block a post, uploads media as a typed blob, and builds the app.bsky.embed.images embed. Because the server URL is per-account, self-hosted PDS instances work exactly like bsky.social.

Text posts

A record is written into app.bsky.feed.post with your text and an ISO createdAt.

Image posts

Media is fetched, uploaded through uploadBlob with the right MIME type and embedded as app.bsky.embed.images.

Session refresh on every send

The engine calls createSession with the stored credential before posting, so expired access JWTs never cause a failure.

Custom PDS support

Each connected account carries its own serverUrl, so self-hosted AT Protocol servers publish the same way as bsky.social.

Alt text by default

The post text is reused as image alt text when none is supplied, keeping posts accessible.

Scheduled publication

Queue by date and time alongside every other connected network.

Common uses

  • Bridge an X account to Bluesky with a single cross-post call.
  • Publish to a self-hosted PDS from the same automation as your other networks.
  • Let an agent post release notes trimmed to 300 graphemes.
  • Schedule a Bluesky presence without keeping a session alive yourself.
Quickstart

Post to Bluesky in three steps

Connect the account once, grab an API key, then send a single request. The same body works from a shell, a server, or an AI agent.

  1. 1

    Connect your Bluesky account

    Open the dashboard, choose Bluesky and complete the hosted connect flow. Credentials are encrypted into the user vault and never returned to your client.

  2. 2

    Create an API key

    Generate a key from the dashboard. Send it as x-api-key, as an Authorization: Bearer header, or as an apikey query parameter on the MCP transport.

  3. 3

    Send your first post

    POST to /api/tools/create_post with platforms: ["bluesky"]. Set publishImmediately to broadcast now, or supply scheduleDate and scheduleTime to queue it.

POST /api/tools/create_post
curl -X POST "https://api.postmcpai.com/api/tools/create_post" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $POSTMCPAI_API_KEY" \
  -d '{"content":"Shipping something new today. Built with PostMCP AI.","platforms":["bluesky"],"publishImmediately":true}'
Response — 200 OK
{
  "success": true,
  "message": "Post created and queued successfully",
  "post": {
    "id": "66a50c89e4b019a2b72f",
    "content": "Shipping something new today. Built with PostMCP AI.",
    "platforms": [
      "bluesky"
    ],
    "status": "published"
  }
}
Reference

Bluesky API endpoints

Every endpoint is a POST against the base URL https://api.postmcpai.com. The same seven operations cover all connected networks — set the platform value to "bluesky" to target Bluesky.

OperationEndpointWhat it does
preflight_postPOST/api/tools/preflight_postDry-run copy against limits, targets and credits before publishing.
create_postPOST/api/tools/create_postSchedule a post or broadcast it immediately.
publish_post_nowPOST/api/tools/publish_post_nowForce a queued post out ahead of its slot, or retry the profiles that failed.
list_postsPOST/api/tools/list_postsRead the queue — scheduled, published, draft and failed — with counts.
get_postPOST/api/tools/get_postRead one post, with per-profile delivery state and live URLs.
get_post_analyticsPOST/api/tools/get_post_analyticsRead how a post did: views, likes, comments and shares per profile.
get_profile_analyticsPOST/api/tools/get_profile_analyticsRead a profile’s followers, post count and views from its network.
update_postPOST/api/tools/update_postEdit copy, targets, schedule or status before publication.
reschedule_postPOST/api/tools/reschedule_postMove a post to another slot, keeping its copy and targets.
reset_stuck_postPOST/api/tools/reset_stuck_postRelease a post left stuck mid-publish so it can be retried.
delete_postPOST/api/tools/delete_postRemove a scheduled or draft post from the queue.
get_connected_accountsPOST/api/tools/get_connected_accountsList connected profiles, handles and page IDs per platform.
get_account_healthPOST/api/tools/get_account_healthFind connections whose token expired or is about to.
generate_imagePOST/api/tools/generate_imageGenerate a post image and get back a hosted URL for mediaUrl.
list_workspacesPOST/api/tools/list_workspacesList the workspaces on this key, with the id to scope other calls to.
get_user_infoPOST/api/tools/get_user_infoRead plan tier and credit balance.

create_post parameters

FieldTypeRequiredDescription
contentstringYesText body of the post. Truncation rules are enforced by the destination network, not by PostMCP.
targetAccountsarray[object]YesThe profiles that receive the post. Each entry takes platform and profileId (from get_connected_accounts). Only the profiles listed here are posted to.
platformsarray[string]OptionalShorthand for whole networks — linkedin, twitter, facebook, instagram, threads, bluesky, youtube. Each one expands to every connected profile on it, so prefer targetAccounts unless you mean that fan-out. Optional when targetAccounts is given.
publishImmediatelybooleanOptionalWhen true the post is broadcast on receipt. Defaults to false, which queues it.
scheduleDatestringOptionalPublication date as YYYY-MM-DD. Required when publishImmediately is false.
scheduleTimestringOptionalPublication time as HH:MM on a 24-hour clock. Required when publishImmediately is false.
timezonestringOptionalIANA zone the schedule above is written in, e.g. Asia/Kolkata. Omit it and the wall-clock slot resolves as UTC, which is rarely what "10am" meant.
mediaUrlstringOptionalPublicly reachable image or video URL. PostMCP fetches it and re-uploads it in the format the network expects. Required, and must be a video, when the post targets YouTube.
workspaceIdstringOptionalWorkspace to post from, taken from list_workspaces. Omitted, the call resolves to the default workspace on the key.
Authentication

Bluesky OAuth and API keys

Two layers of auth sit under every request: the Bluesky credential you grant once during connect, and the PostMCP API key your code sends on each call.

Step 1

Generate an app password

In Bluesky, open Settings → App Passwords and create one. Never use your account password.

Step 2

Connect the handle

Supply your handle or DID plus the app password. Point serverUrl at your own PDS if you self-host.

Step 3

Session established

com.atproto.server.createSession returns an access JWT, and the credential is encrypted into the user vault.

Step 4

Refreshed per broadcast

Before each publish the engine re-runs createSession, so long-idle automations keep working.

Credentials and XRPC methods

Credential / methodWhy it is needed
App passwordBluesky has no OAuth scope model. You generate an app password in Bluesky settings and connect with your handle.
com.atproto.repo.createRecordThe XRPC method that writes the post record into your repository.
com.atproto.repo.uploadBlobThe XRPC method that stores image bytes before they are embedded.

Sending your API key

Header (recommended)x-api-key: pmcp_sec_…
Bearer tokenAuthorization: Bearer pmcp_sec_…
Query (MCP SSE)/mcp?apikey=pmcp_sec_…
Under the hood

How PostMCP publishes to Bluesky

One request from you becomes this sequence server-side. Knowing the shape helps when you are debugging a failed broadcast.

  1. 1

    Resolve the server

    The account serverUrl is used, defaulting to https://bsky.social when unset.

  2. 2

    Refresh the session

    createSession runs with the stored identifier and app password, and the returned accessJwt is used for the rest of the broadcast.

  3. 3

    Upload the blob

    Media is fetched, its MIME type derived from the extension, and pushed to uploadBlob. A failed upload downgrades to a text-only post rather than failing the send.

  4. 4

    Build the record

    Text and an ISO createdAt form the record, with an app.bsky.embed.images embed added when a blob exists.

  5. 5

    Create the record

    createRecord writes into app.bsky.feed.post and the returned AT URI is stored as the post ID.

Upstream Bluesky calls

MethodEndpointPurpose
POST{serverUrl}/xrpc/com.atproto.server.createSessionExchanges the identifier and app password for a fresh access JWT.
POST{serverUrl}/xrpc/com.atproto.repo.uploadBlobUploads image bytes with an explicit Content-Type and Content-Length.
POST{serverUrl}/xrpc/com.atproto.repo.createRecordWrites the record into the `app.bsky.feed.post` collection.

Direct Bluesky API vs PostMCP

AspectCalling Bluesky directlyWith PostMCP
AuthManage app passwords and session JWTsCredential encrypted, session refreshed per send
ImagesuploadBlob then build the embed by handOne `mediaUrl` field
Image galleriesUpload each blob, list up to four in `app.bsky.embed.images`One `mediaUrls` array of up to four
Session expiryStale JWTs fail the writeRefreshed before every broadcast
Self-hostingDifferent base URL per serverPer-account `serverUrl`, same request
SchedulingNot part of AT ProtocolSame queue as every other network
Constraints

Bluesky limits and supported media

These ceilings are set by Bluesky, not by PostMCP. Your content field is forwarded unchanged, so the network enforces them rather than silently truncating.

Text length300 graphemes per post
Image formatsJPEG, PNG, GIF, WebP
Images per postUp to 4 via mediaUrls, or 1 video
Blob sizeAround 1 MB per image on bsky.social
Auth modelHandle plus app password, no OAuth
Serversbsky.social or any self-hosted PDS
Troubleshooting

Common Bluesky API errors

Failures are recorded per platform on the post record, so a multi-network broadcast that partially succeeds tells you exactly which leg failed and why.

CodeMessageLikely causeFix
AuthenticationRequiredInvalid identifier or passwordThe app password was revoked, or the handle changed.Generate a new app password and reconnect the account.
BlobTooLargeuploadBlob failedThe image exceeded the server blob ceiling, roughly 1 MB on bsky.social.Compress the image; the post still ships as text-only if the upload fails.
InvalidRequestRecord validation failedText exceeded 300 graphemes or the record shape was rejected.Shorten the copy to fit the Bluesky limit.
RateLimitExceededToo many requestsAT Protocol servers apply per-account write limits.Space writes out using the scheduler.
Model Context Protocol

Post to Bluesky from an AI agent

The same seven operations are exposed as MCP tools. Point Claude Desktop, Cursor, or any MCP client at the server and your agent can publish to Bluesky directly.

claude_desktop_config.json
{
  "mcpServers": {
    "postmcpai": {
      "command": "npx",
      "args": ["-y", "@postmcpai/server"],
      "env": {
        "POSTMCPAI_API_KEY": "pmcp_sec_YOUR_SECRET_KEY",
        "POSTMCPAI_API_URL": "https://api.postmcpai.com"
      }
    }
  }
}

Prompt the agent directly

With the server connected, natural language is enough — the agent picks the tool and fills the arguments:

“Draft a Bluesky post about today’s release and schedule it for 9:30am tomorrow.”
FAQ

Bluesky API questions

How do I post to Bluesky with the API?

POST to https://api.postmcpai.com/api/tools/create_post with platforms: ["bluesky"] and your content. PostMCP refreshes the AT Protocol session and writes a record into app.bsky.feed.post.

Does Bluesky use OAuth?

Not in this flow. You generate an app password in Bluesky settings and connect it with your handle. PostMCP encrypts that credential and exchanges it for a fresh session JWT before each broadcast.

How are images posted to Bluesky?

The engine fetches your mediaUrl, derives the MIME type from the extension, uploads the bytes through com.atproto.repo.uploadBlob and embeds the returned blob as app.bsky.embed.images on the record. Pass up to four image URLs as mediaUrls and they go out as one gallery embed; a video always goes on its own.

Can I post to a self-hosted AT Protocol server?

Yes. Every connected account carries its own serverUrl, which defaults to https://bsky.social. Point it at your PDS and the same request body works unchanged.

What is the Bluesky character limit?

300 graphemes — counted by grapheme cluster, so emoji and combined characters count as one each rather than by byte length.

What happens if the image upload fails?

The blob failure is logged and the record is written as a text-only post. You get a published post rather than a dropped one.

Other social media APIs

Start posting to Bluesky today

Connect the account, take an API key and send your first request in under five minutes — from a shell, your backend, or an AI agent.