RelayAPI

Threads API

Schedule and automate Threads posts with RelayAPI — text posts, images, videos, carousels, and thread sequences.

Quick Reference

PropertyValue
Platform keythreads
Auth methodOAuth 2.0 (via Instagram/Facebook)
Character limit500
Images per post20 (carousel)
Videos per post1
Image formatsJPEG, PNG
Image max size8 MB (auto-compressed)
Video formatsMP4, MOV
Video max size100 MB
Video max duration5 minutes
Post typesText, Image, Video, Carousel, Thread sequence
SchedulingYes
AnalyticsLimited (impressions, likes, comments, shares, views)

SDK sourceTypeScript · Python · Go · Java · REST API

Before You Start

Threads has a 500-character limit — the #1 failure cause when cross-posting from LinkedIn (3,000) or Facebook (63,000). Always use target_options.threads.content to provide a shorter version. Threads is connected via Instagram — losing Instagram access means losing Threads access. Requires an Instagram Business or Creator account with Threads enabled. Threads enforces a 250 API-published posts per 24-hour window limit. WebP images may fail — use JPEG or PNG for reliability.

Quick Start

Post to Threads:

import Relay from '@relayapi/sdk';

const client = new Relay();
const post = await client.posts.create({
  content: 'Hot take: the best API is the one with the best docs.',
  targets: ['threads'],
  scheduled_at: 'now',
});

console.log(post.id); // post_abc123
from relay import Relay

client = Relay()
post = client.posts.create(
    content='Hot take: the best API is the one with the best docs.',
    targets=['threads'],
    scheduled_at='now',
)

print(post.id)  # post_abc123
client := relaygo.NewClient()

post, err := client.Posts.New(context.TODO(), relaygo.PostNewParams{
    Content:     relaygo.F("Hot take: the best API is the one with the best docs."),
    Targets:     relaygo.F([]string{"threads"}),
    ScheduledAt: relaygo.F("now"),
})

fmt.Println(post.ID) // post_abc123
RelayClient client = RelayOkHttpClient.fromEnv();

PostCreateResponse post = client.posts().create(PostCreateParams.builder()
    .content("Hot take: the best API is the one with the best docs.")
    .addTarget("threads")
    .scheduledAt("now")
    .build());

System.out.println(post.id()); // post_abc123
curl -X POST https://api.relayapi.dev/v1/posts \
  -H "Authorization: Bearer $RELAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Hot take: the best API is the one with the best docs.",
    "targets": ["threads"],
    "scheduled_at": "now"
  }'

Content Types

Text Post

One of the few platforms that supports text-only posts. Up to 500 characters.

const post = await client.posts.create({
  content: 'Hot take: the best API is the one with the best docs.',
  targets: ['threads'],
  scheduled_at: 'now',
});
post = client.posts.create(
    content='Hot take: the best API is the one with the best docs.',
    targets=['threads'],
    scheduled_at='now',
)
post, err := client.Posts.New(context.TODO(), relaygo.PostNewParams{
    Content:     relaygo.F("Hot take: the best API is the one with the best docs."),
    Targets:     relaygo.F([]string{"threads"}),
    ScheduledAt: relaygo.F("now"),
})
PostCreateResponse post = client.posts().create(PostCreateParams.builder()
    .content("Hot take: the best API is the one with the best docs.")
    .addTarget("threads")
    .scheduledAt("now")
    .build());
curl -X POST https://api.relayapi.dev/v1/posts \
  -H "Authorization: Bearer $RELAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Hot take: the best API is the one with the best docs.",
    "targets": ["threads"],
    "scheduled_at": "now"
  }'

Image Post

const post = await client.posts.create({
  content: 'New office setup!',
  targets: ['threads'],
  media: [
    { url: 'https://cdn.example.com/office.jpg', type: 'image' },
  ],
  scheduled_at: 'now',
});
post = client.posts.create(
    content='New office setup!',
    targets=['threads'],
    media=[
        {'url': 'https://cdn.example.com/office.jpg', 'type': 'image'},
    ],
    scheduled_at='now',
)
post, err := client.Posts.New(context.TODO(), relaygo.PostNewParams{
    Content: relaygo.F("New office setup!"),
    Targets: relaygo.F([]string{"threads"}),
    Media: relaygo.F([]relaygo.PostNewParamsMedia{
        {URL: relaygo.F("https://cdn.example.com/office.jpg"), Type: relaygo.F(relaygo.PostNewParamsMediaTypeImage)},
    }),
    ScheduledAt: relaygo.F("now"),
})
PostCreateResponse post = client.posts().create(PostCreateParams.builder()
    .content("New office setup!")
    .addTarget("threads")
    .addMedia(PostCreateParams.Media.builder()
        .url("https://cdn.example.com/office.jpg")
        .type(PostCreateParams.Media.Type.IMAGE)
        .build())
    .scheduledAt("now")
    .build());
curl -X POST https://api.relayapi.dev/v1/posts \
  -H "Authorization: Bearer $RELAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "New office setup!",
    "targets": ["threads"],
    "media": [
      {"url": "https://cdn.example.com/office.jpg", "type": "image"}
    ],
    "scheduled_at": "now"
  }'

Video Post

Max 1 GB, up to 5 minutes.

const post = await client.posts.create({
  content: 'Behind the scenes',
  targets: ['threads'],
  media: [
    { url: 'https://cdn.example.com/video.mp4', type: 'video' },
  ],
  scheduled_at: 'now',
});
post = client.posts.create(
    content='Behind the scenes',
    targets=['threads'],
    media=[
        {'url': 'https://cdn.example.com/video.mp4', 'type': 'video'},
    ],
    scheduled_at='now',
)
post, err := client.Posts.New(context.TODO(), relaygo.PostNewParams{
    Content: relaygo.F("Behind the scenes"),
    Targets: relaygo.F([]string{"threads"}),
    Media: relaygo.F([]relaygo.PostNewParamsMedia{
        {URL: relaygo.F("https://cdn.example.com/video.mp4"), Type: relaygo.F(relaygo.PostNewParamsMediaTypeVideo)},
    }),
    ScheduledAt: relaygo.F("now"),
})
PostCreateResponse post = client.posts().create(PostCreateParams.builder()
    .content("Behind the scenes")
    .addTarget("threads")
    .addMedia(PostCreateParams.Media.builder()
        .url("https://cdn.example.com/video.mp4")
        .type(PostCreateParams.Media.Type.VIDEO)
        .build())
    .scheduledAt("now")
    .build());
curl -X POST https://api.relayapi.dev/v1/posts \
  -H "Authorization: Bearer $RELAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Behind the scenes",
    "targets": ["threads"],
    "media": [
      {"url": "https://cdn.example.com/video.mp4", "type": "video"}
    ],
    "scheduled_at": "now"
  }'

Swipeable image carousel. Mix of images supported.

const post = await client.posts.create({
  content: 'Product launch day!',
  targets: ['threads'],
  media: [
    { url: 'https://cdn.example.com/feature1.jpg', type: 'image' },
    { url: 'https://cdn.example.com/feature2.jpg', type: 'image' },
    { url: 'https://cdn.example.com/feature3.jpg', type: 'image' },
  ],
  scheduled_at: 'now',
});
post = client.posts.create(
    content='Product launch day!',
    targets=['threads'],
    media=[
        {'url': 'https://cdn.example.com/feature1.jpg', 'type': 'image'},
        {'url': 'https://cdn.example.com/feature2.jpg', 'type': 'image'},
        {'url': 'https://cdn.example.com/feature3.jpg', 'type': 'image'},
    ],
    scheduled_at='now',
)
post, err := client.Posts.New(context.TODO(), relaygo.PostNewParams{
    Content: relaygo.F("Product launch day!"),
    Targets: relaygo.F([]string{"threads"}),
    Media: relaygo.F([]relaygo.PostNewParamsMedia{
        {URL: relaygo.F("https://cdn.example.com/feature1.jpg"), Type: relaygo.F(relaygo.PostNewParamsMediaTypeImage)},
        {URL: relaygo.F("https://cdn.example.com/feature2.jpg"), Type: relaygo.F(relaygo.PostNewParamsMediaTypeImage)},
        {URL: relaygo.F("https://cdn.example.com/feature3.jpg"), Type: relaygo.F(relaygo.PostNewParamsMediaTypeImage)},
    }),
    ScheduledAt: relaygo.F("now"),
})
PostCreateResponse post = client.posts().create(PostCreateParams.builder()
    .content("Product launch day!")
    .addTarget("threads")
    .addMedia(PostCreateParams.Media.builder()
        .url("https://cdn.example.com/feature1.jpg")
        .type(PostCreateParams.Media.Type.IMAGE)
        .build())
    .addMedia(PostCreateParams.Media.builder()
        .url("https://cdn.example.com/feature2.jpg")
        .type(PostCreateParams.Media.Type.IMAGE)
        .build())
    .addMedia(PostCreateParams.Media.builder()
        .url("https://cdn.example.com/feature3.jpg")
        .type(PostCreateParams.Media.Type.IMAGE)
        .build())
    .scheduledAt("now")
    .build());
curl -X POST https://api.relayapi.dev/v1/posts \
  -H "Authorization: Bearer $RELAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Product launch day!",
    "targets": ["threads"],
    "media": [
      {"url": "https://cdn.example.com/feature1.jpg", "type": "image"},
      {"url": "https://cdn.example.com/feature2.jpg", "type": "image"},
      {"url": "https://cdn.example.com/feature3.jpg", "type": "image"}
    ],
    "scheduled_at": "now"
  }'

Thread Sequence

Create a connected sequence of posts. The first item is the root post, and each subsequent item is a reply. Each item can have its own content and media.

const post = await client.posts.create({
  targets: ['threads'],
  scheduled_at: 'now',
  target_options: {
    threads: {
      thread: [
        {
          content: "Here's a thread about API design",
          media: [{ url: 'https://cdn.example.com/cover.jpg', type: 'image' }],
        },
        { content: '1/ First, REST principles...' },
        { content: '2/ Authentication is crucial...' },
        { content: '3/ Always version your API! /end' },
      ],
    },
  },
});
post = client.posts.create(
    targets=['threads'],
    scheduled_at='now',
    target_options={
        'threads': {
            'thread': [
                {
                    'content': "Here's a thread about API design",
                    'media': [{'url': 'https://cdn.example.com/cover.jpg', 'type': 'image'}],
                },
                {'content': '1/ First, REST principles...'},
                {'content': '2/ Authentication is crucial...'},
                {'content': '3/ Always version your API! /end'},
            ],
        },
    },
)
post, err := client.Posts.New(context.TODO(), relaygo.PostNewParams{
    Targets:     relaygo.F([]string{"threads"}),
    ScheduledAt: relaygo.F("now"),
    TargetOptions: relaygo.F(map[string]interface{}{
        "threads": map[string]interface{}{
            "thread": []map[string]interface{}{
                {"content": "Here's a thread about API design",
                    "media": []map[string]interface{}{
                        {"url": "https://cdn.example.com/cover.jpg", "type": "image"},
                    }},
                {"content": "1/ First, REST principles..."},
                {"content": "2/ Authentication is crucial..."},
                {"content": "3/ Always version your API! /end"},
            },
        },
    }),
})
PostCreateResponse post = client.posts().create(PostCreateParams.builder()
    .addTarget("threads")
    .scheduledAt("now")
    .targetOptions(PostCreateParams.TargetOptions.builder()
        .putAdditionalProperty("threads", JsonValue.from(Map.of(
            "thread", List.of(
                Map.of("content", "Here's a thread about API design",
                    "media", List.of(Map.of("url", "https://cdn.example.com/cover.jpg", "type", "image"))),
                Map.of("content", "1/ First, REST principles..."),
                Map.of("content", "2/ Authentication is crucial..."),
                Map.of("content", "3/ Always version your API! /end")
            )
        )))
        .build())
    .build());
curl -X POST https://api.relayapi.dev/v1/posts \
  -H "Authorization: Bearer $RELAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "targets": ["threads"],
    "scheduled_at": "now",
    "target_options": {
      "threads": {
        "thread": [
          {"content": "Here'\''s a thread about API design",
           "media": [{"url": "https://cdn.example.com/cover.jpg", "type": "image"}]},
          {"content": "1/ First, REST principles..."},
          {"content": "2/ Authentication is crucial..."},
          {"content": "3/ Always version your API! /end"}
        ]
      }
    }
  }'

Each item in a thread sequence has its own 500-character limit. The root post is published first, and each subsequent item becomes a reply to the previous one.

Media Requirements

Images

PropertyRequirement
Max per post10 (carousel)
FormatsJPEG, PNG, WebP, GIF
Max file size8 MB (auto-compressed)
Recommended1080 x 1350 px (4:5 portrait)
Aspect ratios4:5, 1:1, 16:9

Videos

PropertyRequirement
Max per post1
FormatsMP4, MOV
Max file size1 GB
Max duration5 minutes
Recommended1080p, H.264, AAC, 30fps

target_options Fields

All fields go inside target_options.threads on your post request.

FieldTypeDescription
contentstringOverride content for Threads specifically. Use this to provide a version under 500 characters when cross-posting.
mediaobject[]Override media for Threads specifically
threadobject[]Array of {content, media?} for thread sequences. Each item has its own 500-char limit.

Common Errors

ErrorCauseFix
Text must be at most 500 charactersContent exceeds the 500-character limitUse target_options.threads.content to provide a shorter version.
Media download failed (2207052)URL returns HTML instead of media bytesUse a direct CDN URL. WebP images may fail — use JPEG or PNG instead.
Instagram account restricted (2207050)Linked Instagram account has policy violationsResolve any policy violations on the Instagram account first.
Publishing failedMax retries exhaustedUsually temporary. Wait and retry manually.

Known Quirks

  • 500-character limit — the #1 failure cause when cross-posting from platforms with higher limits like LinkedIn (3,000) or Facebook (63,000).
  • Connected via Instagram — losing Instagram access means losing Threads access. Requires Instagram Business or Creator account.
  • WebP images may fail — use JPEG or PNG for best reliability.
  • Cannot post new top-level comments — only replies are supported.
  • Cannot like or unlike comments via the API.
  • Cannot edit posts after publishing.
  • No DMs available via the Threads API.
  • 250 API-published posts per 24-hour window — this limit applies to all content types.
  • Posts cannot be edited after publishing.

Automations

Threads automation uses its own Graph base (graph.threads.net/v1.0). Replies use the 2-step container → publish flow like public Threads posts.

Triggers

TypeFires on
threads_replyReply to your thread
threads_mention@-mention
threads_publishYour thread publishes (useful for chained automations)

Send nodes

NodeEndpointRequired fields
threads_reply_to_post2-step: POST /{user}/threads then POST /{user}/threads_publishtext, reply_to_id
threads_hide_replyPOST /{reply-id}/manage_reply body { hide: true }reply_id

Found something wrong? Help us improve this page.

On this page

Submit an Issue
Requires a GitHub account.View repo