Bluesky API and scheduling: how to post programmatically
Bluesky has no scheduling. The API writes a post the moment you call com.atproto.repo.createRecord, and the record schema defines no field that holds it back. To schedule, you run your own queue or use a tool that holds the post for you. Every endpoint, limit and price below was checked on official sources on September 17, 2026.
Can you schedule posts on Bluesky?
Not through the protocol. The post record schema requires exactly two fields, text and createdAt, and the current optional ones are langs, facets, embed, reply, labels and tags. There is no publishAt and no scheduledFor. The separate app.bsky.draft API only stores drafts privately, with no publish time. createdAt is a timestamp you write yourself, and writing a future one does not queue anything: the record is in your repository and on the network as soon as the write succeeds.
Bluesky's timestamps guide says its own App View sorts a post whose createdAt is in the future by the time the server indexed it, in author feeds, reply threads and other chronological feeds. So scheduling on Bluesky is always somebody holding the post until a clock says go. Either your cron, or a vendor's.
The Bluesky API in five minutes
Two calls. Create an app password in Settings, never use your account password, then exchange it for a session.
curl -X POST https://bsky.social/xrpc/com.atproto.server.createSession \
-H "Content-Type: application/json" \
-d '{"identifier": "you.bsky.social", "password": "xxxx-xxxx-xxxx-xxxx"}'
The response carries accessJwt (short-lived) and refreshJwt. Then write the record:
curl -X POST https://bsky.social/xrpc/com.atproto.repo.createRecord \
-H "Authorization: Bearer $ACCESS_JWT" \
-H "Content-Type: application/json" \
-d '{
"repo": "you.bsky.social",
"collection": "app.bsky.feed.post",
"record": {
"$type": "app.bsky.feed.post",
"text": "Posted from a cron job.",
"createdAt": "2026-09-17T08:00:00.000Z",
"langs": ["en"]
}
}'
You get back the record location and its hash:
{
"uri": "at://did:plc:u5cwb2mwiv2bfq53cjufe6yn/app.bsky.feed.post/3k4duaz5vfs2b",
"cid": "bafyreibjifzpqj6o6wcq3hejh7y4z4z2vmiklkvykc57tw3pcbx3kxifpm"
}
That is the whole publishing surface. No app review, no business account, no partner programme. Compared with what Instagram and TikTok ask for, documented in the social media API guide, Bluesky is the easiest API in this space by a wide margin.
What a Bluesky post record accepts
From the lexicon, checked September 17, 2026.
| Field | Limit |
|---|---|
text |
300 graphemes, 3,000 bytes |
langs |
3 entries |
tags |
8 entries, 64 graphemes each |
| Images per post | 4 with app.bsky.embed.images; the newer app.bsky.embed.gallery allows 20 in the schema, but tells apps to cap it at 10 for now |
| Image size | 2,000,000 bytes each (raised from 1,000,000) |
| Blob upload (server limit, on the rate limits page) | 52,428,800 bytes (50 MB) |
Two traps. Links and mentions are not parsed from your text: you attach byte ranges yourself in facets, or your URL renders as plain text. And 300 is graphemes, not code points: Python's len() over-counts flags, skin tones and joined emoji (a family emoji counts as 7) and can reject a post Bluesky would accept. Count graphemes, and check the 3,000-byte ceiling separately.
Bluesky API rate limits
The rate limits page uses a points budget for writes, on top of per-IP request caps.
| Limit | Value |
|---|---|
| Write budget | 5,000 points per hour, 35,000 points per day |
| Cost of a create | 3 points, so 1,666 creates per hour |
| Cost of an update / delete | 2 points / 1 point |
| Overall API requests | 3,000 per 5 minutes, per IP |
createSession |
30 per 5 minutes, 300 per day, per account |
For a scheduler this is generous to the point of irrelevance: a publishing queue that posts ten times a day uses 30 of 35,000 points. The one to watch is createSession. Do not authenticate on every job. Cache the session and refresh it, or a busy worker loop will hit 30 in five minutes while publishing nothing.
Scheduling it yourself
The cheapest scheduler is a table and a cron. Store the text, the media and a publish_at, then run every minute:
import os, requests, datetime as dt
BASE = "https://bsky.social/xrpc"
def session():
r = requests.post(f"{BASE}/com.atproto.server.createSession", json={
"identifier": os.environ["BSKY_HANDLE"],
"password": os.environ["BSKY_APP_PASSWORD"],
})
r.raise_for_status()
return r.json()
def publish(s, text):
now = dt.datetime.now(dt.timezone.utc).isoformat().replace("+00:00", "Z")
r = requests.post(f"{BASE}/com.atproto.repo.createRecord",
headers={"Authorization": f"Bearer {s['accessJwt']}"},
json={"repo": s["did"], "collection": "app.bsky.feed.post",
"record": {"$type": "app.bsky.feed.post", "text": text, "createdAt": now}})
r.raise_for_status()
return r.json()["uri"]
Three rules I learned the expensive way. Set createdAt at publish time, not at queue time: Bluesky's timestamps guide says its App View sorts a post by createdAt when that time is in the past, so a post stamped an hour before it goes out sorts an hour down. Make the job idempotent, one row, one lock, or a retry double-posts. And log the returned uri, because that is your proof the post exists.
Tools that schedule Bluesky for you
Prices taken from each vendor's own pricing page on September 17, 2026.
| Tool | Price | Bluesky | Hosted | Best for |
|---|---|---|---|---|
| Buffer | Free plan: 3 channels, 10 scheduled posts per channel. Essentials $5 per channel per month, billed annually at $60 a year | Yes | Yes | One person, a handful of channels |
| Postiz | $29 a month for 5 channels, $49 for 30, or self-host the open source build | Yes | Both | Teams who want the option to self-host |
| Mixpost | One-off licence: Lite free, Pro $299, Enterprise $1,199. Bluesky is in Pro | Yes | Self-hosted, plus Mixpost Cloud (no public price) | Buy once, run it on your own server |
| Ayrshare | $149 a month for 1 profile, $299 for 10, from $599 for 30 | Yes, platform value bluesky |
Yes | API-first products reselling publishing |
| Blotato | $29 a month for 20 accounts, $97 for 40 | Yes, listed on its homepage | Yes | AI content workflows across nine networks |
| PlugKit | $29 a month for 5 connected accounts, $97 for 30 | No | Yes | Agents on Instagram, TikTok, LinkedIn, X, YouTube, Facebook, WhatsApp, Telegram |
| Your own cron | Server cost only | Yes | Self-hosted | One account, full control, zero abstraction |
If Bluesky is the only network you publish to, none of these is worth paying for. The API is a POST with a bearer token and the limits are 1,666 creates an hour. Write the cron.
Where PlugKit fits, and where it does not
PlugKit does not support Bluesky, today. The eight platforms are Instagram, Facebook, LinkedIn, TikTok, YouTube, X, WhatsApp and Telegram, listed on plugkit.co this morning. If your account list is Bluesky plus Mastodon, use Buffer or a script and stop reading.
Where it earns its place is the opposite case: the APIs that are painful. Meta app review, TikTok's Direct Post scope, X billed per call. One key, one hosted MCP server at https://api.plugkit.co/mcp, added with claude mcp add -t http plugkit https://api.plugkit.co/mcp, and your agent publishes to Instagram, Facebook, LinkedIn, TikTok, YouTube, X and Telegram, reads and answers messages on Instagram, Facebook, WhatsApp and Telegram, and replies to comments and runs comment-to-DM automations on Instagram and Facebook. That is $29 a month for 5 connected accounts, cancel anytime. The per-platform limits are in the rate limits reference, the agent setup in the Hermes Agent guide, and the hosted vs self-hosted trade-off in Postiz alternatives.
For another young API, this time from Meta, see the Threads API.
FAQ
Can you schedule posts on Bluesky?
Not natively. The AT Protocol post record has no scheduling field, and a createdAt set in the future does not delay publication. Scheduling comes from something holding the post outside Bluesky: your own cron job, or a tool like Buffer, Postiz or Mixpost.
Is the Bluesky API free?
Yes, for normal use. Bluesky's developer docs list rate limits but no plan, price or per-call fee, and you authenticate with a free app password and stay inside 5,000 write points an hour, which is about 1,666 posts. That is the opposite of X, which bills every read and every publish.
How do I authenticate with the Bluesky API?
Create an app password in your Bluesky settings, then POST your handle and that password to com.atproto.server.createSession. You get an accessJwt to send as a bearer token and a refreshJwt to renew it. Never use your real password, and cache the session: createSession is capped at 30 calls per 5 minutes.
What is the character limit for a Bluesky post?
300 graphemes, with a hard ceiling of 3,000 bytes. An emoji counts as one grapheme but takes 4 to 25 bytes, so the byte ceiling only bites on text packed with joined emoji, like families or couples. With the images embed you can attach up to four images of 2,000,000 bytes each.
Does PlugKit support Bluesky?
No. PlugKit covers Instagram, Facebook, LinkedIn, TikTok, YouTube, X, WhatsApp and Telegram. If you need Bluesky today, script it directly against the AT Protocol or use a tool that lists it, and keep PlugKit for the platforms where OAuth, app review and per-call billing make a hosted API worth paying for.
One plug for every platform your agent touches
$29 a month for 5 connected accounts, cancel anytime. Every feature in every plan.
Get your API key →