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.
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.
- Create a container with
POST /{threads-user-id}/threads.media_typeisTEXT,IMAGE, orVIDEO; media comes from a publicimage_urlorvideo_urlthat Meta fetches. For carousels, create each item withis_carousel_item=true, then a parent withmedia_type=CAROUSELandchildrenlisting the item IDs. - 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, andEXPIRED(“the container was not published within 24 hours”). - 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 marksviewsandsharesas still in development, so expect gaps. - Per account,
GET /{threads-user-id}/threads_insights:viewsas a time series,likes,replies,reposts,quotes,clicks,followers_count, andfollower_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.



