Posts

Publish text, image, video, threads, quotes, and link cards on Bluesky.

Publish to Bluesky through the shared posts surface. Text, images, video, threads, and quotes all go through the same endpoint.

Quick Start

Post to Bluesky in under 60 seconds:

import { Configuration, PostsApi } from "@onepostly/sdk";

const postsApi = new PostsApi(
  new Configuration({ apiKey: process.env.ONEPOSTLY_API_KEY })
);

const response = await postsApi.createPost({
  createPostBody: {
    "text": "Ship day",
    "mediaUrls": [
      "https://cdn.onepostly.com/media/product.jpg"
    ],
    "mediaKind": "image",
    "destinations": [
      {
        "accountId": "a1b2c3d4-e5f6-4a7b-8c9d-e0f1a2b3c4d5"
      }
    ]
  },
});
console.log(response);
{
  "post": {
    "id": "b3e1a7c2-8f4d-4e6a-9c1b-2d3e4f5a6b7c",
    "text": "Ship day",
    "mediaUrls": [
      "https://cdn.onepostly.com/media/product.jpg"
    ],
    "mediaKind": "image",
    "status": "queued",
    "scheduledFor": null,
    "timezone": null,
    "destinations": [
      {
        "id": "d4e5f6a7-8b9c-4d0e-8f1a-2b3c4d5e6f7a",
        "accountId": "a1b2c3d4-e5f6-4a7b-8c9d-e0f1a2b3c4d5",
        "platform": "bluesky",
        "status": "draft",
        "externalPostId": "17895695668004550",
        "externalUrl": "https://bsky.app/profile/you/post/17895695668004550",
        "errorCode": null,
        "errorMessage": null,
        "publishedAt": "2026-08-25T10:00:00.000Z",
        "metrics": null
      }
    ],
    "createdAt": "2026-08-25T10:00:00.000Z",
    "updatedAt": "2026-08-25T10:00:00.000Z"
  }
}

The response is 202 Accepted with status: "queued". When published, the destination carries the post URI. Poll GET /v1/posts/:id or receive webhooks.

Content Types

Text Post

A simple text-only post. Keep it under 300 characters. Mentions, links, and hashtags are detected automatically:

import { Configuration, PostsApi } from "@onepostly/sdk";

const postsApi = new PostsApi(
  new Configuration({ apiKey: process.env.ONEPOSTLY_API_KEY })
);

const response = await postsApi.createPost({
  createPostBody: {
    "text": "Just shipped our new API client!",
    "mediaUrls": [],
    "mediaKind": "text",
    "destinations": [
      {
        "accountId": "a1b2c3d4-e5f6-4a7b-8c9d-e0f1a2b3c4d5"
      }
    ]
  },
});
console.log(response);
{
  "post": {
    "id": "b3e1a7c2-8f4d-4e6a-9c1b-2d3e4f5a6b7c",
    "text": "Just shipped our new API client!",
    "mediaUrls": [],
    "mediaKind": "text",
    "status": "queued",
    "scheduledFor": null,
    "timezone": null,
    "destinations": [
      {
        "id": "d4e5f6a7-8b9c-4d0e-8f1a-2b3c4d5e6f7a",
        "accountId": "a1b2c3d4-e5f6-4a7b-8c9d-e0f1a2b3c4d5",
        "platform": "bluesky",
        "status": "draft",
        "externalPostId": "17895695668004550",
        "externalUrl": "https://bsky.app/profile/you/post/17895695668004550",
        "errorCode": null,
        "errorMessage": null,
        "publishedAt": "2026-08-25T10:00:00.000Z",
        "metrics": null
      }
    ],
    "createdAt": "2026-08-25T10:00:00.000Z",
    "updatedAt": "2026-08-25T10:00:00.000Z"
  }
}

Image Post

Attach up to 4 images per post. JPEG, PNG, WebP, and GIF formats are supported. Each image must be under 2 MB:

import { Configuration, PostsApi } from "@onepostly/sdk";

const postsApi = new PostsApi(
  new Configuration({ apiKey: process.env.ONEPOSTLY_API_KEY })
);

const response = await postsApi.createPost({
  createPostBody: {
    "text": "Ship day",
    "mediaUrls": [
      "https://cdn.onepostly.com/media/product.jpg"
    ],
    "mediaKind": "image",
    "destinations": [
      {
        "accountId": "a1b2c3d4-e5f6-4a7b-8c9d-e0f1a2b3c4d5"
      }
    ]
  },
});
console.log(response);
{
  "post": {
    "id": "b3e1a7c2-8f4d-4e6a-9c1b-2d3e4f5a6b7c",
    "text": "Ship day",
    "mediaUrls": [
      "https://cdn.onepostly.com/media/product.jpg"
    ],
    "mediaKind": "image",
    "status": "queued",
    "scheduledFor": null,
    "timezone": null,
    "destinations": [
      {
        "id": "d4e5f6a7-8b9c-4d0e-8f1a-2b3c4d5e6f7a",
        "accountId": "a1b2c3d4-e5f6-4a7b-8c9d-e0f1a2b3c4d5",
        "platform": "bluesky",
        "status": "draft",
        "externalPostId": "17895695668004550",
        "externalUrl": "https://bsky.app/profile/you/post/17895695668004550",
        "errorCode": null,
        "errorMessage": null,
        "publishedAt": "2026-08-25T10:00:00.000Z",
        "metrics": null
      }
    ],
    "createdAt": "2026-08-25T10:00:00.000Z",
    "updatedAt": "2026-08-25T10:00:00.000Z"
  }
}

Multi-image post

Attach 2–4 images:

import { Configuration, PostsApi } from "@onepostly/sdk";

const postsApi = new PostsApi(
  new Configuration({ apiKey: process.env.ONEPOSTLY_API_KEY })
);

const response = await postsApi.createPost({
  createPostBody: {
    "text": "Product launch gallery",
    "mediaUrls": [
      "https://cdn.example.com/photo1.jpg",
      "https://cdn.example.com/photo2.jpg",
      "https://cdn.example.com/photo3.jpg"
    ],
    "mediaKind": "multi-image",
    "destinations": [
      {
        "accountId": "YOUR_BLUESKY_ACCOUNT_ID"
      }
    ]
  },
});
console.log(response);
{
  "post": {
    "id": "b3e1a7c2-8f4d-4e6a-9c1b-2d3e4f5a6b7c",
    "text": "Product launch gallery",
    "mediaUrls": [
      "https://cdn.example.com/photo1.jpg",
      "https://cdn.example.com/photo2.jpg",
      "https://cdn.example.com/photo3.jpg"
    ],
    "mediaKind": "multi-image",
    "status": "queued",
    "scheduledFor": null,
    "timezone": null,
    "destinations": [
      {
        "id": "d4e5f6a7-8b9c-4d0e-8f1a-2b3c4d5e6f7a",
        "accountId": "YOUR_BLUESKY_ACCOUNT_ID",
        "platform": "bluesky",
        "status": "draft",
        "externalPostId": "17895695668004550",
        "externalUrl": "https://bsky.app/profile/you/post/17895695668004550",
        "errorCode": null,
        "errorMessage": null,
        "publishedAt": "2026-08-25T10:00:00.000Z",
        "metrics": null
      }
    ],
    "createdAt": "2026-08-25T10:00:00.000Z",
    "updatedAt": "2026-08-25T10:00:00.000Z"
  }
}

Video Post

Attach a single video per post. MP4 format only, up to 100 MB:

import { Configuration, PostsApi } from "@onepostly/sdk";

const postsApi = new PostsApi(
  new Configuration({ apiKey: process.env.ONEPOSTLY_API_KEY })
);

const response = await postsApi.createPost({
  createPostBody: {
    "text": "Watch how it works",
    "mediaUrls": [
      "https://cdn.onepostly.com/media/demo.mp4"
    ],
    "mediaKind": "video",
    "destinations": [
      {
        "accountId": "a1b2c3d4-e5f6-4a7b-8c9d-e0f1a2b3c4d5"
      }
    ]
  },
});
console.log(response);
{
  "post": {
    "id": "b3e1a7c2-8f4d-4e6a-9c1b-2d3e4f5a6b7c",
    "text": "Watch how it works",
    "mediaUrls": [
      "https://cdn.onepostly.com/media/demo.mp4"
    ],
    "mediaKind": "video",
    "status": "queued",
    "scheduledFor": null,
    "timezone": null,
    "destinations": [
      {
        "id": "d4e5f6a7-8b9c-4d0e-8f1a-2b3c4d5e6f7a",
        "accountId": "a1b2c3d4-e5f6-4a7b-8c9d-e0f1a2b3c4d5",
        "platform": "bluesky",
        "status": "draft",
        "externalPostId": "17895695668004550",
        "externalUrl": "https://bsky.app/profile/you/post/17895695668004550",
        "errorCode": null,
        "errorMessage": null,
        "publishedAt": "2026-08-25T10:00:00.000Z",
        "metrics": null
      }
    ],
    "createdAt": "2026-08-25T10:00:00.000Z",
    "updatedAt": "2026-08-25T10:00:00.000Z"
  }
}

Media Requirements

Images

PropertyRequirement
Max images4 per post
FormatsJPEG, PNG, WebP, GIF
Max file size2 MB per image
Max dimensions2000 x 2000 px
Recommended1200 x 675 px (16:9)

Aspect ratios

TypeRatioDimensions
Landscape16:91200 x 675 px
Square1:11000 x 1000 px
Portrait4:5800 x 1000 px

Videos

PropertyRequirement
Max videos1 per post
FormatMP4 only
Max file size100 MB
Max duration60 seconds
Max dimensions1920 x 1080 px
Frame rate30 fps recommended
CodecH.264
AudioAAC

Media URL Requirements

These do not work as media URLs:

  • Google Drive returns an HTML download page, not the file
  • Dropbox returns an HTML preview page
  • OneDrive / SharePoint returns HTML
  • iCloud returns HTML

Test your URL in an incognito browser window. If you see a webpage instead of the raw image or video, the URL will not work.

Media URLs must be publicly accessible, return actual media bytes with the correct Content-Type, and use HTTPS.

Thread

Top-level text with normal mediaKind and mediaUrls rules is always the root post. threadItems adds 1 to 25 replies under that root in order.

PropertyTypeDescription
threadItemsarrayReplies under the root post in order (1 to 25)
threadItems[].contentstringPost text content
threadItems[].mediaItemsarrayMedia attachments for this reply

When threadItems is present the root needs top-level text or mediaUrls or the request fails with 400. The response post echoes the root only and never echoes threadItems.

import { Configuration, PostsApi } from "@onepostly/sdk";

const postsApi = new PostsApi(
  new Configuration({ apiKey: process.env.ONEPOSTLY_API_KEY })
);

const response = await postsApi.createPost({
  createPostBody: {
    "text": "A thread about building APIs",
    "mediaUrls": [
      "https://cdn.example.com/api-design.jpg"
    ],
    "mediaKind": "image",
    "destinations": [
      {
        "accountId": "YOUR_BLUESKY_ACCOUNT_ID"
      }
    ],
    "threadItems": [
      {
        "content": "Second, always version your API from day one."
      },
      {
        "content": "Finally, document everything!",
        "mediaItems": [
          "https://cdn.example.com/docs.jpg"
        ]
      }
    ]
  },
});
console.log(response);
{
  "post": {
    "id": "b3e1a7c2-8f4d-4e6a-9c1b-2d3e4f5a6b7c",
    "text": "A thread about building APIs",
    "mediaUrls": [
      "https://cdn.example.com/api-design.jpg"
    ],
    "mediaKind": "image",
    "status": "queued",
    "scheduledFor": null,
    "timezone": null,
    "destinations": [
      {
        "id": "d4e5f6a7-8b9c-4d0e-8f1a-2b3c4d5e6f7a",
        "accountId": "YOUR_BLUESKY_ACCOUNT_ID",
        "platform": "bluesky",
        "status": "draft",
        "externalPostId": "17895695668004550",
        "externalUrl": "https://bsky.app/profile/you/post/17895695668004550",
        "errorCode": null,
        "errorMessage": null,
        "publishedAt": "2026-08-25T10:00:00.000Z",
        "metrics": null
      }
    ],
    "createdAt": "2026-08-25T10:00:00.000Z",
    "updatedAt": "2026-08-25T10:00:00.000Z"
  }
}
  • Each item's content may be empty only when that item has mediaItems
  • Up to 4 images per reply or a single video. Link cards and quotes stay on the root post
  • Each segment must stay within 300 characters
  • All destinations in the request must be X, Threads, or Bluesky when threadItems is present

Quote

quoteTweetId takes the at:// URI of the post to quote. It combines with images and link cards:

import { Configuration, PostsApi } from "@onepostly/sdk";

const postsApi = new PostsApi(
  new Configuration({ apiKey: process.env.ONEPOSTLY_API_KEY })
);

const response = await postsApi.createPost({
  createPostBody: {
    "text": "My take",
    "mediaUrls": [],
    "mediaKind": "text",
    "destinations": [
      {
        "accountId": "YOUR_BLUESKY_ACCOUNT_ID",
        "quoteTweetId": "at://did:plc:xyz/app.bsky.feed.post/abc123"
      }
    ]
  },
});
console.log(response);
{
  "post": {
    "id": "b3e1a7c2-8f4d-4e6a-9c1b-2d3e4f5a6b7c",
    "text": "My take",
    "mediaUrls": [],
    "mediaKind": "text",
    "status": "queued",
    "scheduledFor": null,
    "timezone": null,
    "destinations": [
      {
        "id": "d4e5f6a7-8b9c-4d0e-8f1a-2b3c4d5e6f7a",
        "accountId": "YOUR_BLUESKY_ACCOUNT_ID",
        "platform": "bluesky",
        "status": "draft",
        "externalPostId": "17895695668004550",
        "externalUrl": "https://bsky.app/profile/you/post/17895695668004550",
        "errorCode": null,
        "errorMessage": null,
        "publishedAt": "2026-08-25T10:00:00.000Z",
        "metrics": null
      }
    ],
    "createdAt": "2026-08-25T10:00:00.000Z",
    "updatedAt": "2026-08-25T10:00:00.000Z"
  }
}

Pass link to attach a URL preview card:

import { Configuration, PostsApi } from "@onepostly/sdk";

const postsApi = new PostsApi(
  new Configuration({ apiKey: process.env.ONEPOSTLY_API_KEY })
);

const response = await postsApi.createPost({
  createPostBody: {
    "text": "Check this out",
    "mediaUrls": [
      "https://cdn.onepostly.com/media/product.jpg"
    ],
    "mediaKind": "text",
    "destinations": [
      {
        "accountId": "YOUR_BLUESKY_ACCOUNT_ID",
        "link": "https://example.com/article"
      }
    ]
  },
});
console.log(response);
{
  "post": {
    "id": "b3e1a7c2-8f4d-4e6a-9c1b-2d3e4f5a6b7c",
    "text": "Check this out",
    "mediaUrls": [
      "https://cdn.onepostly.com/media/product.jpg"
    ],
    "mediaKind": "text",
    "status": "queued",
    "scheduledFor": null,
    "timezone": null,
    "destinations": [
      {
        "id": "d4e5f6a7-8b9c-4d0e-8f1a-2b3c4d5e6f7a",
        "accountId": "YOUR_BLUESKY_ACCOUNT_ID",
        "platform": "bluesky",
        "status": "draft",
        "externalPostId": "17895695668004550",
        "externalUrl": "https://bsky.app/profile/you/post/17895695668004550",
        "errorCode": null,
        "errorMessage": null,
        "publishedAt": "2026-08-25T10:00:00.000Z",
        "metrics": null
      }
    ],
    "createdAt": "2026-08-25T10:00:00.000Z",
    "updatedAt": "2026-08-25T10:00:00.000Z"
  }
}

Post Languages

Tag the language of a post with langs on the Bluesky destination: 1–3 BCP-47 codes (e.g. ["en", "pt"]). Bluesky feed generators can filter on langs, so posts without it may not appear in language-scoped feeds.

import { Configuration, PostsApi } from "@onepostly/sdk";

const postsApi = new PostsApi(
  new Configuration({ apiKey: process.env.ONEPOSTLY_API_KEY })
);

const response = await postsApi.createPost({
  createPostBody: {
    "text": "Olá! Hello!",
    "mediaUrls": [],
    "mediaKind": "text",
    "destinations": [
      {
        "accountId": "YOUR_BLUESKY_ACCOUNT_ID",
        "langs": [
          "pt",
          "en"
        ]
      }
    ]
  },
});
console.log(response);
{
  "post": {
    "id": "b3e1a7c2-8f4d-4e6a-9c1b-2d3e4f5a6b7c",
    "text": "Olá! Hello!",
    "mediaUrls": [],
    "mediaKind": "text",
    "status": "queued",
    "scheduledFor": null,
    "timezone": null,
    "destinations": [
      {
        "id": "d4e5f6a7-8b9c-4d0e-8f1a-2b3c4d5e6f7a",
        "accountId": "YOUR_BLUESKY_ACCOUNT_ID",
        "platform": "bluesky",
        "status": "draft",
        "externalPostId": "17895695668004550",
        "externalUrl": "https://bsky.app/profile/you/post/17895695668004550",
        "errorCode": null,
        "errorMessage": null,
        "publishedAt": "2026-08-25T10:00:00.000Z",
        "metrics": null
      }
    ],
    "createdAt": "2026-08-25T10:00:00.000Z",
    "updatedAt": "2026-08-25T10:00:00.000Z"
  }
}

When langs is omitted, the account default applies (see Bluesky Default Languages). Per-post langs always overrides the default, and the same tags are written to the root post and every thread segment. Bluesky has no post editing, so langs can only be set at creation time.

Schedule

Add scheduledFor (local wall time) and timezone to the same call:

  • timezone defaults to UTC. Use an IANA name like Europe/Istanbul
  • Do not append Z or an offset to scheduledFor: it is local wall time
  • Max 1 year ahead
  • Cancel with DELETE /v1/posts/:id any time before it publishes

Remote Delete

Deletes the post record on Bluesky:

import { Configuration, PostsApi } from "@onepostly/sdk";

const postsApi = new PostsApi(
  new Configuration({ apiKey: process.env.ONEPOSTLY_API_KEY })
);

const response = await postsApi.deletePostDestination({
  destinationId: "d4e5f6a7-8b9c-4d0e-8f1a-2b3c4d5e6f7a",
});
console.log(response);
{
  "post": {
    "id": "b3e1a7c2-8f4d-4e6a-9c1b-2d3e4f5a6b7c",
    "text": "Ship day",
    "mediaUrls": [
      "https://cdn.onepostly.com/media/product.jpg"
    ],
    "mediaKind": "image",
    "status": "queued",
    "scheduledFor": null,
    "timezone": null,
    "destinations": [
      {
        "id": "d4e5f6a7-8b9c-4d0e-8f1a-2b3c4d5e6f7a",
        "accountId": "a1b2c3d4-e5f6-4a7b-8c9d-e0f1a2b3c4d5",
        "platform": "bluesky",
        "status": "draft",
        "externalPostId": "17895695668004550",
        "externalUrl": "https://bsky.app/profile/you/post/17895695668004550",
        "errorCode": null,
        "errorMessage": null,
        "publishedAt": "2026-08-25T10:00:00.000Z",
        "metrics": null
      }
    ],
    "createdAt": "2026-08-25T10:00:00.000Z",
    "updatedAt": "2026-08-25T10:00:00.000Z"
  }
}

Publish Errors

CodeCauseFix
TEXT_REQUIREDEmpty text on a text postAdd text
TEXT_TOO_LONGOver 300 characters (graphemes)Trim the text
MEDIA_REQUIREDImage / video post without a URLAdd at least one entry to mediaUrls
INVALID_MULTI_IMAGEFewer than 2 or more than 4 imagesAdjust the count
INVALID_MEDIAImage over 2 MB, or more than one videoCheck the Media Requirements
MEDIA_UNFETCHABLEMedia URL not reachableUse a public HTTPS URL
INVALID_LINKlink is not a valid URLCheck the URL
INVALID_QUOTEquoteTweetId is not an at:// URI, or the post cannot be resolvedPass a valid at:// post URI
UNSUPPORTED_MEDIA_KINDmediaKind: "stories"Use text, image, multi-image, or video
TOKEN_INVALIDApp Password revoked or session expiredCreate a new App Password and reconnect
ACCOUNT_SUSPENDEDBluesky suspended or deactivated the accountResolve it in the Bluesky app before posting again
RATE_LIMITEDBluesky throttled the requestWait out the window, then create a new post
PLATFORM_UNAVAILABLEBluesky reported a transient server-side failureTry again later
VALIDATION_ERRORBluesky rejected the record shapeCheck the fields in the error message

See Also