Posts

Publish text, image, video, and multi-image on Threads with threads and quotes.

Publish to Threads through the shared posts surface. Text, images, video, carousels, reply chains, and quotes all go through the same endpoint.

Quick Start

Post to Threads 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": "threads",
        "status": "draft",
        "externalPostId": "17895695668004550",
        "externalUrl": "https://www.threads.com/@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". Poll GET /v1/posts/:id until the destination is published or failed, or receive webhooks instead of polling.

Content Types

mediaKindContentMedia countNotes
textText-only postNo mediaMax 500 characters
imageSingle imageExactly 1 URLOptional text
multi-imageCarousel2 to 20 URLsImages and videos can be mixed
videoSingle videoExactly 1 URLOptional text
storiesNoNoNot supported on Threads

Text is capped at 500 characters everywhere: posts, captions, and thread segments.

Image Post

A single image with an optional caption:

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": "threads",
        "status": "draft",
        "externalPostId": "17895695668004550",
        "externalUrl": "https://www.threads.com/@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

A single video with an optional caption. Max 1 GB, up to 5 minutes:

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": "threads",
        "status": "draft",
        "externalPostId": "17895695668004550",
        "externalUrl": "https://www.threads.com/@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"
  }
}

Up to 20 items in a single swipeable post. Images and videos can be mixed:

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 day! Here are all the new features:",
    "mediaUrls": [
      "https://cdn.example.com/feature1.jpg",
      "https://cdn.example.com/feature2.jpg",
      "https://cdn.example.com/feature3.jpg"
    ],
    "mediaKind": "multi-image",
    "destinations": [
      {
        "accountId": "YOUR_THREADS_ACCOUNT_ID"
      }
    ]
  },
});
console.log(response);
{
  "post": {
    "id": "b3e1a7c2-8f4d-4e6a-9c1b-2d3e4f5a6b7c",
    "text": "Product launch day! Here are all the new features:",
    "mediaUrls": [
      "https://cdn.example.com/feature1.jpg",
      "https://cdn.example.com/feature2.jpg",
      "https://cdn.example.com/feature3.jpg"
    ],
    "mediaKind": "multi-image",
    "status": "queued",
    "scheduledFor": null,
    "timezone": null,
    "destinations": [
      {
        "id": "d4e5f6a7-8b9c-4d0e-8f1a-2b3c4d5e6f7a",
        "accountId": "YOUR_THREADS_ACCOUNT_ID",
        "platform": "threads",
        "status": "draft",
        "externalPostId": "17895695668004550",
        "externalUrl": "https://www.threads.com/@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"
  }
}

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": "Here is everything you need to know about our API...",
    "mediaUrls": [
      "https://cdn.onepostly.com/media/cover.jpg"
    ],
    "mediaKind": "image",
    "destinations": [
      {
        "accountId": "YOUR_THREADS_ACCOUNT_ID"
      }
    ],
    "threadItems": [
      {
        "content": "1/ First, authentication is simple..."
      },
      {
        "content": "2/ Next, create your first post...",
        "mediaItems": [
          "https://cdn.onepostly.com/media/step2a.jpg",
          "https://cdn.onepostly.com/media/step2b.jpg"
        ]
      }
    ]
  },
});
console.log(response);
{
  "post": {
    "id": "b3e1a7c2-8f4d-4e6a-9c1b-2d3e4f5a6b7c",
    "text": "Here is everything you need to know about our API...",
    "mediaUrls": [
      "https://cdn.onepostly.com/media/cover.jpg"
    ],
    "mediaKind": "image",
    "status": "queued",
    "scheduledFor": null,
    "timezone": null,
    "destinations": [
      {
        "id": "d4e5f6a7-8b9c-4d0e-8f1a-2b3c4d5e6f7a",
        "accountId": "YOUR_THREADS_ACCOUNT_ID",
        "platform": "threads",
        "status": "draft",
        "externalPostId": "17895695668004550",
        "externalUrl": "https://www.threads.com/@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
  • Every reply is a full Threads post with one image or video per item or several images as a carousel with a max of 20
  • Each segment must stay within 500 characters
  • All destinations in the request must be X, Threads, or Bluesky when threadItems is present

Quote

Pass quoteTweetId with the id of the Threads post being quoted:

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_THREADS_ACCOUNT_ID",
        "quoteTweetId": "THREADS_POST_ID"
      }
    ]
  },
});
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_THREADS_ACCOUNT_ID",
        "platform": "threads",
        "status": "draft",
        "externalPostId": "17895695668004550",
        "externalUrl": "https://www.threads.com/@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"
  }
}

Destination Parameters

Prop

Type

Media Requirements

Images

PropertyRequirement
Max images20 per carousel (1 for a single image post)
FormatsJPEG, PNG
Max file size8 MB per image
Recommended1080 × 1350 px (4:5 portrait)

Aspect ratios

RatioDimensionsNotes
4:51080 × 1350 pxPortrait, recommended
1:11080 × 1080 pxSquare
16:91080 × 608 pxLandscape

Videos

PropertyRequirement
Max videos1 per post (single video or carousel slide)
FormatMP4 (H.264 / AAC)
Max file size1 GB
Max duration5 minutes
Aspect ratio9:16 vertical, 16:9 landscape, or 1:1 square
Resolution1080p recommended

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.

Schedule

Add scheduledFor (local wall time) and timezone to the same call. The request shape is otherwise identical to an immediate post.

  • 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
  • Scheduled posts return 201 Created and publish automatically at the scheduled moment
  • Max 1 year ahead
  • Cancel with posts.cancelPost({ id }) / posts.cancel_post(id=…), or DELETE /v1/posts/:id, any time before then

Remote Delete

Removes the post and its replies, and emits the post.platform.deleted webhook:

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": "threads",
        "status": "draft",
        "externalPostId": "17895695668004550",
        "externalUrl": "https://www.threads.com/@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_REQUIREDText-only post with empty messageAdd text
TEXT_TOO_LONGOver 500 characters (post, segment, or firstComment)Trim the text
INVALID_TOPIC_TAGEmpty, over 50 characters, or contains . / &Fix the tag
MEDIA_REQUIREDImage / video without a URLAdd at least one entry to mediaUrls
UNSUPPORTED_MEDIA_KINDmediaKind: "stories" on a Threads destinationUse text, image, multi-image, or video
INVALID_MULTI_IMAGEFewer than 2 or more than 20 URLsAdjust the count
MEDIA_UNFETCHABLEThreads could not download the URLUse a public HTTPS URL
CONTAINER_FAILEDMedia processing failed on Threads' sideRetry. Check the file opens correctly
TOKEN_INVALIDConnection token expired or revokedReconnect the account
PERMISSION_DENIEDConnection lacks publish permissionReconnect the account
RATE_LIMITEDThreads throttled the requestRetry later

See Also