Posts
Publish text, image, video, and multi-image on Bluesky.
Bluesky publishes through the shared posts surface. Uploading media works the same on every platform, so see Media for uploads.
Supported formats
mediaKind | Supported | With text | Notes |
|---|---|---|---|
text | Yes | Required | No mediaUrls |
image | Yes | Yes | One image URL |
multi-image | Yes | Yes | 2–4 image URLs |
video | Yes | Yes | One video/mp4 URL |
stories | No | Bluesky has no stories |
mediaKind is required. Text is limited to 300 characters per post. Media URLs must be publicly accessible HTTPS links.
Create
const { post } = await posts.createPost({
createPostBody: {
text: "Hello from Onepostly https://onepostly.com @handle.bsky.social #bluesky",
mediaKind: "text",
destinations: [{ connectionId: "YOUR_BLUESKY_CONNECTION_ID" }],
},
});
// 202 Accepted, post.status is "queued"result = await posts.create_post(
create_post_body={
"text": "Hello from Onepostly https://onepostly.com @handle.bsky.social #bluesky",
"mediaKind": "text",
"destinations": [{"connectionId": "YOUR_BLUESKY_CONNECTION_ID"}],
},
)curl -X POST https://api.onepostly.com/v1/posts \
-H "x-api-key: op_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "Hello from Onepostly https://onepostly.com @handle.bsky.social #bluesky",
"mediaKind": "text",
"destinations": [
{ "connectionId": "YOUR_BLUESKY_CONNECTION_ID" }
]
}'Mentions, links, and hashtags are detected automatically. The response is 202 Accepted with status: "queued". Poll GET /v1/posts/:id until published or failed, or use webhooks.
Multi-image
await posts.createPost({
createPostBody: {
text: "Carousel",
mediaKind: "multi-image",
mediaUrls: [
"https://cdn.example.com/1.jpg",
"https://cdn.example.com/2.jpg",
"https://cdn.example.com/3.jpg",
],
destinations: [{ connectionId: "YOUR_BLUESKY_CONNECTION_ID" }],
},
});await posts.create_post(
create_post_body={
"text": "Carousel",
"mediaKind": "multi-image",
"mediaUrls": [,
"https://cdn.example.com/1.jpg"
"https://cdn.example.com/2.jpg"
"https://cdn.example.com/3.jpg"
]
"destinations": [{"connectionId": "YOUR_BLUESKY_CONNECTION_ID"}],
},
)curl -X POST https://api.onepostly.com/v1/posts \
-H "x-api-key: op_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "Carousel",
"mediaKind": "multi-image",
"mediaUrls": [
"https://cdn.example.com/1.jpg",
"https://cdn.example.com/2.jpg",
"https://cdn.example.com/3.jpg"
],
"destinations": [{ "connectionId": "YOUR_BLUESKY_CONNECTION_ID" }]
}'Requires 2–4 URLs. Each image may be up to 2 MB.
Video
Single video up to 100 MB. Same call shape as multi-image with mediaKind: "video" and one URL.
Link card
await posts.createPost({
createPostBody: {
text: "Check this out",
mediaKind: "text",
destinations: [
{
connectionId: "YOUR_BLUESKY_CONNECTION_ID",
link: "https://example.com/article",
},
],
},
});await posts.create_post(
create_post_body={
"text": "Check this out",
"destinations": [,
{
"connectionId": "YOUR_BLUESKY_CONNECTION_ID"
"link": "https://example.com/article"
}
]
},
)curl -X POST https://api.onepostly.com/v1/posts \
-H "x-api-key: op_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "Check this out",
"mediaKind": "text",
"destinations": [
{
"connectionId": "YOUR_BLUESKY_CONNECTION_ID",
"link": "https://example.com/article"
}
]
}'Thread
await posts.createPost({
createPostBody: {
text: "1/3 intro",
mediaKind: "text",
destinations: [{ connectionId: "YOUR_BLUESKY_CONNECTION_ID" }],
thread: ["2/3 body", "3/3 CTA"],
},
});await posts.create_post(
create_post_body={
"text": "1/3 intro",
"thread": ["2/3 body", "3/3 CTA"],
"destinations": [{"connectionId": "YOUR_BLUESKY_CONNECTION_ID"}],
},
)curl -X POST https://api.onepostly.com/v1/posts \
-H "x-api-key: op_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "1/3 intro",
"mediaKind": "text",
"destinations": [{ "connectionId": "YOUR_BLUESKY_CONNECTION_ID" }],
"thread": ["2/3 body", "3/3 CTA"]
}'thread[0] is the first post, each entry becomes a reply (max 25, 300 characters each). Media is attached to the first post only.
Quote
await posts.createPost({
createPostBody: {
text: "My take",
mediaKind: "text",
destinations: [
{
connectionId: "YOUR_BLUESKY_CONNECTION_ID",
quoteTweetId: "at://did:plc:xyz/app.bsky.feed.post/abc123",
},
],
},
});await posts.create_post(
create_post_body={
"text": "My take",
"destinations": [,
{
"connectionId": "YOUR_BLUESKY_CONNECTION_ID"
"quoteTweetId": "at://did:plc:xyz/app.bsky.feed.post/abc123"
}
]
},
)curl -X POST https://api.onepostly.com/v1/posts \
-H "x-api-key: op_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "My take",
"mediaKind": "text",
"destinations": [
{
"connectionId": "YOUR_BLUESKY_CONNECTION_ID",
"quoteTweetId": "at://did:plc:xyz/app.bsky.feed.post/abc123"
}
]
}'quoteTweetId must be the ID of the post you want to quote. You can combine it with an image or link.
Schedule
Add scheduledFor and timezone, same as other platforms. See the X schedule example.
Remote delete
await posts.deletePostDestination({ id: postId, destinationId });;await posts.delete_post_destination(id=post_id, destination_id=destination_id)curl -X DELETE \
"https://api.onepostly.com/v1/posts/POST_ID/destinations/DEST_ID" \
-H "x-api-key: op_YOUR_KEY"Publish errors
| Code | Cause | Fix |
|---|---|---|
TEXT_REQUIRED | Empty text | Add text |
TEXT_TOO_LONG | Over 300 characters | Trim the text |
MEDIA_REQUIRED | Missing media URL | Add mediaUrls |
INVALID_MULTI_IMAGE | Wrong count | 2–4 for multi-image |
MEDIA_UNFETCHABLE | URL not reachable | Use public HTTPS URL |
TOKEN_INVALID | App Password revoked/expired | Create new App Password and reconnect |
UNSUPPORTED_MEDIA_KIND | stories | Use supported kind |