Docs
Bluesky

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

mediaKindSupportedWith textNotes
textYesRequiredNo mediaUrls
imageYesYesOne image URL
multi-imageYesYes2–4 image URLs
videoYesYesOne video/mp4 URL
storiesNoBluesky 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.

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

CodeCauseFix
TEXT_REQUIREDEmpty textAdd text
TEXT_TOO_LONGOver 300 charactersTrim the text
MEDIA_REQUIREDMissing media URLAdd mediaUrls
INVALID_MULTI_IMAGEWrong count2–4 for multi-image
MEDIA_UNFETCHABLEURL not reachableUse public HTTPS URL
TOKEN_INVALIDApp Password revoked/expiredCreate new App Password and reconnect
UNSUPPORTED_MEDIA_KINDstoriesUse supported kind

See also