API Guides

Threads API Guide 2026: Publishing, Permissions, Limits, and Insights

How the Threads API works in 2026: OAuth and 60-day tokens, permissions, the container publish flow for text, images, video, and carousels, media specs, the 250-post daily cap, insights, and error messages.
September 07, 2026 by Jesse Eisenbart11 min read
Oil painting of a braided river splitting into many shallow channels across a wide gravel valley floor beneath mountains

TLDR: The Threads API is Meta’s most approachable publishing API. It lives at graph.threads.net, uses OAuth with one-hour tokens exchanged for 60-day tokens, and publishes through the same container-then-publish flow as Instagram, with two differences that matter: text-only posts are allowed, and the quotas are published (250 posts and 1,000 replies per rolling 24 hours). Text is capped at 500 characters, images at 8 MB, video at 1 GB and 5 minutes, carousels at 20 items. Since launch in June 2024 Meta has added polls, GIFs, spoiler tags, keyword search, webhooks, and reply approval. The API is free. Checked against Meta’s Threads documentation on September 7, 2026.

Does Threads have an API?

Yes, and it is younger than the others. Meta’s changelog records the date: “June 18, 2024 - Threads API is open to all developers.” Since then the surface has grown faster than any other Meta API:

Date Addition
August 13, 2024 Webhooks
December 9, 2024 Keyword search
April 14, 2025 Polls
July to August 2025 Mentions, delete, and publish webhooks
October 3, 2025 Spoiler tags and text attachments
October 17, 2025 GIF support
December 15, 2025 Ghost posts
February 13, 2026 Reply approval management
February 27, 2026 GIPHY integration (Tenor support ended March 31, 2026)

The overview states that “the Threads API can be accessed by either graph.threads.com or graph.threads.net”; Meta’s own auth examples use the .net host, so this guide does too.

How does Threads API authentication work?

Threads uses its own OAuth flow rather than Facebook Login. The user authorises at https://threads.net/oauth/authorize with your client_id, redirect_uri, scope, and response_type=code; you exchange the code for a short-lived token valid for one hour, then exchange that for a long-lived token valid for 60 days. Meta’s long-lived tokens page gives the two calls:

  • Exchange: GET https://graph.threads.net/access_token?grant_type=th_exchange_token&client_secret=...&access_token=<short-lived>
  • Refresh: GET https://graph.threads.net/refresh_access_token?grant_type=th_refresh_token&access_token=<long-lived>, allowed once the token is “at least 24 hours old but have not expired.” Refreshed tokens are “valid for 60 days from the date at which they are refreshed,” and “tokens that have not been refreshed in 60 days will expire and can no longer be refreshed.”
import requests

THREADS = 'https://graph.threads.net'

def to_long_lived_token(short_lived_token: str, app_secret: str) -> dict:
    response = requests.get(
        f'{THREADS}/access_token',
        params={'grant_type': 'th_exchange_token', 'client_secret': app_secret, 'access_token': short_lived_token},
        timeout=30,
    )
    response.raise_for_status()
    return response.json()  # access_token, token_type, expires_in (~60 days in seconds)

def refresh_long_lived_token(long_lived_token: str) -> dict:
    # Allowed once the token is at least 24 hours old; do it well before day 60.
    response = requests.get(
        f'{THREADS}/refresh_access_token',
        params={'grant_type': 'th_refresh_token', 'access_token': long_lived_token},
        timeout=30,
    )
    response.raise_for_status()
    return response.json()

There is a second clock that Instagram does not have. The get started page explains that “permission grants made by app users with public profiles are valid for 90 days,” and refreshing the token extends the grant by another 90 days. But “if the app user’s profile is private … the permission grant cannot be extended and the app user must grant the expired permission to your app again.” A product that connects private Threads profiles needs a re-authorisation prompt on a 90-day cycle regardless of token health.

For development, add accounts as Threads Testers in the App Dashboard; a tester “can grant your app these permissions at any time” without review. For everyone else, “each permission must first be approved through the App Review process, and your app must be published.” Meta does not state a review turnaround for Threads.

Which Threads API permissions do you need?

Permission Grants
threads_basic “Required for making any calls to all Threads API endpoints.”
threads_content_publish Creating containers, publishing, and reading the publishing limit
threads_read_replies GET calls to reply endpoints
threads_manage_replies POST calls to reply endpoints: hide, unhide, approve
threads_manage_insights GET calls to insights endpoints
threads_delete Deleting posts
threads_location_tagging Location search and tagging
threads_keyword_search Keyword search; Meta calls this “a sensitive scope that requires App Review completion with a demo of your use case”
threads_profile_discovery Looking up other public profiles with at least 100 followers; 1,000 requests per user per rolling 24 hours

A publishing tool needs threads_basic and threads_content_publish; add threads_manage_insights for analytics and the reply scopes for moderation. Request only what your review demo shows.

How do you publish a post with the Threads API?

Two calls, the same shape as Instagram: create a container, then publish it. The posts documentation covers the container types.

  1. Create a container with POST /{threads-user-id}/threads. media_type is TEXT, IMAGE, or VIDEO; media comes from a public image_url or video_url that Meta fetches. For carousels, create each item with is_carousel_item=true, then a parent with media_type=CAROUSEL and children listing the item IDs.
  2. Check status with GET /{container-id}?fields=status,error_message. Meta’s troubleshooting page recommends “querying a container’s status once per minute, for no more than 5 minutes,” and lists the states: IN_PROGRESS, FINISHED, PUBLISHED, ERROR, and EXPIRED (“the container was not published within 24 hours”).
  3. Publish with POST /{threads-user-id}/threads_publish?creation_id={container-id}.

The container parameters that matter for publishing:

Parameter Meaning
text Up to 500 characters; emoji count as their UTF-8 bytes
media_type TEXT, IMAGE, VIDEO; CAROUSEL on the parent
image_url / video_url Publicly reachable URLs
is_carousel_item true on each carousel child
children Comma-separated child container IDs, 2 to 20
link_attachment “The URL that should be attached to a Threads post and displayed as a link preview” (text posts)
reply_control everyone, accounts_you_follow, mentioned_only, followers_only, or parent_post_author_only
quote_post_id “ID of another post that you want to quote”
topic_tag One tag, 1 to 50 characters
gif_attachment A GIF from the GIPHY integration

A text post with a link preview in Node:

const THREADS = 'https://graph.threads.net/v1.0'

async function post(path, params, token) {
	const response = await fetch(`${THREADS}${path}`, {
		method: 'POST',
		body: new URLSearchParams({ ...params, access_token: token }),
	})
	const data = await response.json()
	if (!response.ok || data.error) throw new Error(JSON.stringify(data.error ?? data))
	return data
}

export async function publishTextPost({ userId, token, text, link, replyControl = 'everyone' }) {
	const { id: containerId } = await post(
		`/${userId}/threads`,
		{ media_type: 'TEXT', text, link_attachment: link, reply_control: replyControl },
		token,
	)
	const { id: postId } = await post(`/${userId}/threads_publish`, { creation_id: containerId }, token)
	return postId
}

Text containers are usually ready immediately. Media containers need the status loop; a carousel in Python:

import time
import requests

THREADS = 'https://graph.threads.net/v1.0'

def _post(path: str, params: dict, token: str) -> dict:
    response = requests.post(f'{THREADS}{path}', data={**params, 'access_token': token}, timeout=30)
    data = response.json()
    if not response.ok or 'error' in data:
        raise RuntimeError(data.get('error', data))
    return data

def _wait_until_finished(container_id: str, token: str) -> None:
    for _ in range(5):                                   # Meta: once per minute, at most 5 minutes
        data = requests.get(
            f'{THREADS}/{container_id}',
            params={'fields': 'status,error_message', 'access_token': token},
            timeout=30,
        ).json()
        status = data.get('status')
        if status == 'FINISHED':
            return
        if status in ('ERROR', 'EXPIRED'):
            raise RuntimeError(f"container {status}: {data.get('error_message')}")
        time.sleep(60)
    raise RuntimeError('container not ready after 5 minutes')

def publish_carousel(user_id: str, token: str, items: list[dict], text: str) -> str:
    """items: [{'media_type': 'IMAGE', 'image_url': ...} or {'media_type': 'VIDEO', 'video_url': ...}], 2 to 20."""
    child_ids = []
    for item in items:
        child = _post(f'/{user_id}/threads', {**item, 'is_carousel_item': 'true'}, token)
        _wait_until_finished(child['id'], token)
        child_ids.append(child['id'])

    parent = _post(
        f'/{user_id}/threads',
        {'media_type': 'CAROUSEL', 'children': ','.join(child_ids), 'text': text},
        token,
    )
    _wait_until_finished(parent['id'], token)
    return _post(f'/{user_id}/threads_publish', {'creation_id': parent['id']}, token)['id']

Replies are posts too: create a container with the parent’s ID as the reply target and publish it the same way. Quote posts use quote_post_id on an ordinary container.

What are the Threads API media requirements?

From the posts documentation, as of September 2026:

Image Video Carousel
Formats JPEG, PNG MOV or MP4 Per item
Codecs HEVC or H.264
Max size 8 MB 1 GB Per item
Aspect ratio Up to 10:1
Width 320 to 1440 px
Duration Up to 300 seconds (5 minutes)
Frame rate 23 to 60 fps
Count 1 1 2 to 20, images and video mixed

Compared with Instagram, this is generous: PNG is accepted, the ratio limit is 10:1 rather than a 4:5 to 1.91:1 window, and a carousel takes twice as many items. The strictness is on the video side; a 6-minute clip that Instagram would accept as a reel fails here on duration.

What are the Threads API rate limits?

Threads is unusual among Meta’s APIs in publishing its numbers. The overview lists, per account and per rolling 24 hours:

  • 250 API-published posts, with a carousel counting as one
  • 1,000 replies
  • 100 deletions
  • 500 location searches

Check where an account stands with GET /{threads-user-id}/threads_publishing_limit, which returns usage against these quotas. Above them sits the general Graph-style call limit: “Calls within 24 hours = 4800 * Number of Impressions,” with a floor of 10 impressions, so a brand-new account with no audience gets 48,000 calls a day, which is plenty for publishing and tight for aggressive polling. Poll containers once a minute as Meta recommends, not once a second.

Replies, quotes, polls, and GIFs

The reply management endpoints cover the moderation loop: GET /{media-id}/replies for top-level replies, GET /{media-id}/conversation for the flattened thread including nested replies, POST /{reply-id}/manage_reply with hide=true or false, and, since February 2026, GET /{media-id}/pending_replies with POST /{reply-id}/manage_pending_reply and an approve flag for accounts that hold replies for approval. Hiding a reply cascades to its nested replies.

Publishing-side extras: reply_control decides who can reply at creation time; quote_post_id makes a quote post; topic_tag attaches one tag of up to 50 characters; gif_attachment attaches a GIPHY GIF. Polls have been supported since April 2025 and report a total_votes field; spoiler tags since October 2025. The parameter shapes for polls and spoilers are on the posts page and change as features land, so read the current reference before building on them.

What analytics does the Threads API expose?

With threads_manage_insights, the insights endpoints return:

  • Per post, GET /{threads-media-id}/insights: views, likes, replies, reposts, quotes, shares. Meta marks views and shares as still in development, so expect gaps.
  • Per account, GET /{threads-user-id}/threads_insights: views as a time series, likes, replies, reposts, quotes, clicks, followers_count, and follower_demographics.

History starts at the API’s launch: “the user insights since and until parameters do not work for dates before April 13, 2024.” Reposts of other people’s posts report no metrics of their own.

Threads API error messages you will see

Threads does not publish numeric error subcodes for publishing. Instead, a container that reaches ERROR carries an error_message string, listed on the troubleshooting page:

error_message Cause Fix
FAILED_DOWNLOADING_VIDEO Meta could not fetch video_url Public, fast, long-lived URL
FAILED_PROCESSING_VIDEO / FAILED_PROCESSING_AUDIO Transcode failed Re-encode as H.264 MP4 with AAC audio
INVALID_ASPEC_RATIO (sic) Ratio outside limits Crop
INVALID_BIT_RATE Bitrate too high Re-encode at a lower bitrate
INVALID_DURATION Over 300 seconds Trim
INVALID_FRAME_RATE Outside 23 to 60 fps Re-encode
INVALID_AUDIO_CHANNELS / INVALID_AUDIO_CHANNEL_LAYOUT Unsupported audio layout Re-encode to stereo AAC
UNKNOWN Unspecified Retry once; recreate the container

Alongside these, the usual Graph API errors apply: code 190 for an expired token, 10 for a missing permission, and 4 or 17 for throttling. Log error_message on every ERROR status; the strings are the only diagnostic you get.

Is the Threads API free?

Yes. Meta documents no fee, and there is no paid tier. The constraints are the quotas above and App Review for anyone who is not a Threads Tester. Among the ten networks in our comparison, Threads is the one where the limits are both generous and written down, which makes it the easiest Meta platform to build against.

The shorter path: one request, same 500 characters

The container flow, the status loop, the 60-day token refresh, the 90-day grant renewal for private profiles, and a media transcoder that respects a different spec from Instagram’s add up to a real integration for one network. PostZen’s Threads integration handles that behind one request, and the same request shape publishes to nine other networks, including Instagram, which shares the container model but not the rules.

curl -X POST https://api.postzen.dev/v1/posts \
  -H "Authorization: Bearer pzn_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "content": "We just shipped queue-based scheduling. Fill your slots once, publish forever.",
    "publishNow": true,
    "mediaItems": [{ "url": "https://cdn.example.com/queues.png" }],
    "platforms": [
      {
        "platform": "threads",
        "accountId": "jh72r5nqk9wx3v8m1t4cz6bs0fy5dg3e",
        "settings": { "replyControl": "everyone" }
      }
    ]
  }'

PostZen enforces Threads’ 500-character limit up front, takes images up to 8 MB and videos up to 1 GB, and exposes replyControl with everyone, accountsYouFollow, and mentionedOnly. Send two or more mediaItems and the post becomes a carousel, images and videos mixed, with the child-container polling handled for you; a single request accepts up to 10 items against Threads’ ceiling of 20. Analytics for Threads is live, including post metrics and follower counts, on the analytics page. Details are in the Threads integration docs, the Threads page, and the social media API overview. Threads’ quotas and media rules are Meta’s and apply on the other side of any tool, including ours.

Frequently asked questions

Does Threads have an API?

Yes. Meta opened the Threads API to all developers on June 18, 2024. It publishes text, image, video, and carousel posts, manages replies, and returns insights, through graph.threads.net with OAuth tokens that last 60 days.

Is the Threads API free?

Meta documents no fee. Usage is limited by quotas: 250 API-published posts and 1,000 replies per rolling 24 hours per account, plus a call-volume limit tied to the account’s impressions.

What is the character limit for Threads API posts?

Text is limited to 500 characters. Emoji count as their UTF-8 byte length, so a post heavy on emoji hits the limit sooner than the visible character count suggests.

Can I post text-only to Threads through the API?

Yes. Unlike Instagram, Threads accepts a TEXT container with no media, optionally with a link_attachment that renders as a link preview.

How many images can a Threads carousel have?

Between 2 and 20 items, images and videos mixed. Each item is its own container marked is_carousel_item, then a CAROUSEL container lists them in children. The whole carousel counts as one post against the daily quota.

Why does my Threads API token stop working after 90 days even though I refresh it?

Tokens and permission grants are separate. Refreshing a long-lived token extends a public profile’s grant for another 90 days, but a private profile’s grant cannot be extended; that user must authorise your app again.