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
| Property | Requirement |
|---|---|
| Max images | 4 per post |
| Formats | JPEG, PNG, WebP, GIF |
| Max file size | 2 MB per image |
| Max dimensions | 2000 x 2000 px |
| Recommended | 1200 x 675 px (16:9) |
Aspect ratios
| Type | Ratio | Dimensions |
|---|---|---|
| Landscape | 16:9 | 1200 x 675 px |
| Square | 1:1 | 1000 x 1000 px |
| Portrait | 4:5 | 800 x 1000 px |
Videos
| Property | Requirement |
|---|---|
| Max videos | 1 per post |
| Format | MP4 only |
| Max file size | 100 MB |
| Max duration | 60 seconds |
| Max dimensions | 1920 x 1080 px |
| Frame rate | 30 fps recommended |
| Codec | H.264 |
| Audio | AAC |
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.
| Property | Type | Description |
|---|---|---|
threadItems | array | Replies under the root post in order (1 to 25) |
threadItems[].content | string | Post text content |
threadItems[].mediaItems | array | Media 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
contentmay be empty only when that item hasmediaItems - 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
threadItemsis 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"
}
}Link Card
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:
timezonedefaults toUTC. Use an IANA name likeEurope/Istanbul- Do not append
Zor an offset toscheduledFor: it is local wall time - Max 1 year ahead
- Cancel with
DELETE /v1/posts/:idany 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
| Code | Cause | Fix |
|---|---|---|
TEXT_REQUIRED | Empty text on a text post | Add text |
TEXT_TOO_LONG | Over 300 characters (graphemes) | Trim the text |
MEDIA_REQUIRED | Image / video post without a URL | Add at least one entry to mediaUrls |
INVALID_MULTI_IMAGE | Fewer than 2 or more than 4 images | Adjust the count |
INVALID_MEDIA | Image over 2 MB, or more than one video | Check the Media Requirements |
MEDIA_UNFETCHABLE | Media URL not reachable | Use a public HTTPS URL |
INVALID_LINK | link is not a valid URL | Check the URL |
INVALID_QUOTE | quoteTweetId is not an at:// URI, or the post cannot be resolved | Pass a valid at:// post URI |
UNSUPPORTED_MEDIA_KIND | mediaKind: "stories" | Use text, image, multi-image, or video |
TOKEN_INVALID | App Password revoked or session expired | Create a new App Password and reconnect |
ACCOUNT_SUSPENDED | Bluesky suspended or deactivated the account | Resolve it in the Bluesky app before posting again |
RATE_LIMITED | Bluesky throttled the request | Wait out the window, then create a new post |
PLATFORM_UNAVAILABLE | Bluesky reported a transient server-side failure | Try again later |
VALIDATION_ERROR | Bluesky rejected the record shape | Check the fields in the error message |