Start here

What this guide is really about

If you build automation, run a scheduling tool, or publish for clients, you eventually hit the same wall: the Threads app is built for one person posting in the moment, not for a pipeline that needs to push approved posts at chosen times. The Threads API exists for exactly that job, and Meta documents every endpoint. What the docs do not give you is the operator's view: how the pieces connect, which limits bite first, and where scheduled posts quietly break.

This guide walks the publishing path end to end, at the level a working automation actually needs. You will see the two-step container publish, the media specifications that reject posts before they ever render, the quota math that shapes any scheduler, the retry rules that prevent embarrassing double posts, and the token lifecycle that quietly kills most homemade schedulers.

If you are not building anything and just want a calendar that posts for you, that is a different problem with a simpler answer: use native and API-based scheduling instead of this operator path. This article is for the person who writes or supervises the code.

Quick answer

Threads API publishing is two calls: create a media container (POST /{threads-user-id}/threads), wait until status is FINISHED (about 30 seconds), then publish it (POST /{threads-user-id}/threads_publish). Limits per 24 hours: 250 posts, 1,000 replies, 100 deletions. Media needs a public URL (images up to 8 MB, video up to 5 min and 1 GB). Long-lived tokens last 60 days; refresh extends grants for public profiles.

What you will leave with

The exact two-call publishing flow with parameters and the processing wait

Every rate limit and media specification that shapes a publishing pipeline

Retry and status rules that prevent double posts and expired containers

An honest build-versus-buy test before you commit to hand-rolling

Key takeaways

Publishing is two steps: create a media container, then publish it after processing finishes; never fire the second call blind.

The binding constraint is 250 API posts per rolling 24 hours, enforced at the publish call, checkable any time via the publishing limit endpoint.

Media lives on a public server and must meet exact specs; most silent failures are video encoding or private hosting problems.

Long-lived tokens last 60 days and refreshing them extends the 90-day permission grant only for public profiles.

Retries must be container-first: query container status before re-attempting a publish, or you will double-post.

What you need before the first publish

Everything starts in the Meta App Dashboard. Create an app with the Threads use case, and note a detail that trips up first-time integrators: the dashboard shows two app IDs with two secrets, and Threads work belongs to the Threads app ID, not the other one. Until your app goes through App Review, only Threads testers you invite can grant it permissions, which is fine for building against your own account. The Threads API Get Started guide walks through every screen; the operator-relevant details are below.

Permissions are granular. Every endpoint needs threads_basic. Publishing needs threads_content_publish. Replying needs threads_manage_replies, reading replies needs threads_read_replies, and insights need threads_manage_insights. Ask for the minimum set your pipeline uses; each extra permission widens your App Review story later.

Authentication follows OAuth 2.0. The authorization window hands you a code, the code exchanges for a short-lived token valid one hour, and that exchanges for a long-lived token valid 60 days. Permission grants from users with public profiles last 90 days. There is one structural exception to remember: if the account authorizing your app is private, the permission grant cannot be extended by token refresh, and the user must re-authorize when it lapses.

If your posts include images or video, one more requirement shapes your architecture: Meta downloads media from a URL you provide, so the file must sit on a publicly accessible server at publish time. Signed URLs that expire, intranet paths, and localhost are all silent killers. If you still need the non-code path, start with native and API-based scheduling, then come back here when you are ready to own the publish calls.

The two-step publish, end to end

A single post is two calls, and the separation is not cosmetic. Step one creates a media container: POST to /{threads-user-id}/threads with media_type set to TEXT, IMAGE, or VIDEO, the text, which obeys the 500-character limit and emoji byte counting we break down separately, and for image or video posts the image_url or video_url of the file on your public server. The response returns a container ID. The official Threads Posts documentation lists every parameter; what follows is the workflow view. Threads is now downloading and processing your media.

Step two publishes. Meta recommends waiting on average 30 seconds after container creation before calling POST /{threads-user-id}/threads_publish with creation_id set to the container ID. That wait is not a formality: publishing before processing finishes is a classic source of failed or flaky posts, especially with video. For a robust implementation, poll the container status endpoint rather than sleeping blindly, and only call threads_publish when the status reads FINISHED.

Carousels extend the same pattern. You create one container per image or video child with is_carousel_item true, then a parent container with media_type CAROUSEL whose children list those IDs, then publish the carousel container. Between 2 and 20 children are allowed, and the whole carousel counts as a single post against your rate limit. Two related behaviors are worth knowing early: the first URL in the text field becomes the link preview card, and posts with no media use media_type TEXT with the text field required.

If URLs in text are your traffic path, note the preview mechanics before you design link posts: the first URL wins the preview, and everything else reads as plain text. That single rule decides where your link sits in a marketing post.

Two-step diagram: create media container, wait for processing, then publish the container
One post, two calls, one processing wait.

Media specifications that silently reject posts

The API does not transcode ambition. It downloads your file and checks it against the image and video specifications, and mismatches surface later as container errors rather than polite warnings. Images must be JPEG or PNG, 8 MB maximum, at least 320 pixels wide and at most 1440, with an aspect ratio within 10:1 and sRGB color. Anything wider gets scaled down, and other color spaces get converted, but size and format are hard gates.

Video is where most pipelines bleed. Requirements: MP4 or MOV container, H.264 or HEVC video at 23 to 60 fps, maximum 1920 columns wide, bitrate up to 100 Mbps VBR; audio AAC at up to 48 kHz sample rate and 128 kbps, mono or stereo; duration over zero and at most 300 seconds; file size up to 1 GB; and the moov atom at the front of the file with no edit lists. If your encoder's defaults predate the spec, validate before shipping the pipeline, not after your first ERROR container.

Common mistakes

Firing threads_publish without confirming container status, then retrying the publish blindly and posting twice.

Treating the two app IDs in the App Dashboard as interchangeable; only the Threads app ID and its secret work for this API.

Checking the publishing limit only after errors appear, instead of before draining a scheduled queue.

Letting a 60-day long-lived token expire because nothing calendared the refresh, killing every scheduled post silently.

Hosting media on expiring signed URLs or private paths, which surfaces as FAILED_DOWNLOADING_VIDEO containers.

Rate limits and the quota math that shapes your scheduler

The headline number is 250 API-published posts per Threads profile per rolling 24-hour window, enforced at the publish call, not at container creation. Replies have their own limit of 1,000 per 24 hours, deletions 100, and location searches 500. Carousels count as one post, which matters when you are planning how much of the quota a mixed calendar eats. These numbers come straight from the rate limiting rules in the API overview.

The second layer is app-level: calls are counted per app and user pair in a rolling 24-hour window. The budget is 4800 multiplied by the account's impressions in the last 24 hours, and impressions themselves floor at 10, so the practical minimum call budget is 48,000. That is usually plenty for publishing, but chatty read loops still waste headroom, so batch reads and cache what you can.

Both layers are visible in advance. GET /{threads-user-id}/threads_publishing_limit can return quota_usage with config, reply_quota_usage with reply_config, delete_quota_usage with delete_config, and location_search_quota_usage with location_search_config, each config carrying its own quota_total. A scheduler worth the name checks the relevant fields before draining a queue, not after the 429s start. A practical pattern: check the limit before each publish burst, record quota_usage in your logs next to each post, and alert when usage crosses your own comfort threshold, because Meta's number moves on a rolling window and yours should not wait for it.

Design tip that survives contact with production: space your retries. If a burst fails and you re-fire it later, spreading posts across minutes instead of dumping the backlog into one minute keeps you inside the rolling window and inside your audience's patience.

A clean limits board showing post, reply, and deletion quotas on a rolling 24-hour window
Quotas every scheduler must respect.

Failure modes and the retry rules that prevent double posts

When publishing does not go as planned, the container status endpoint is your source of truth. GET on the container ID with fields=status,error_message returns one of five states: IN_PROGRESS while media is still processing, FINISHED when it is ready to publish, PUBLISHED when it is live, ERROR when processing failed, and EXPIRED when the container was never published within 24 hours. Meta recommends polling about once per minute for no more than five minutes; the full set of container status codes and video error messages lives in the troubleshooting reference.

ERROR containers come with a code, and the codes read like a checklist of encoding sins: FAILED_DOWNLOADING_VIDEO when your URL was not reachable in time, FAILED_PROCESSING_VIDEO and FAILED_PROCESSING_AUDIO for transcoding failures, INVALID_ASPEC_RATIO, INVALID_BIT_RATE, INVALID_DURATION, INVALID_FRAME_RATE, INVALID_AUDIO_CHANNELS, and INVALID_AUDIO_CHANNEL_LAYOUT. Treat each as a validation bug to fix upstream, because retrying the same file reproduces the same error.

The rule that saves your reputation: never blind-retry threads_publish. The call can succeed server-side while your client times out, and a naive retry posts the content twice to a real audience. The safe sequence is container-first: on any doubt, query the container status. If it reads PUBLISHED, you are done, log and move on. If it reads FINISHED, one careful publish attempt follows. Only if the container is EXPIRED or ERROR do you create a fresh container from the original asset.

This is also where homemade schedulers diverge from production ones. A weekend script retries what fails. A pipeline people trust asks the platform what happened before acting, keeps the container ID as the idempotency key, and writes one row per post attempt so a human can audit what went out.

Token lifecycle and connection hygiene

Homegrown schedulers often fail for an operational reason, not a clever code bug: a long-lived token quietly passes its 60-day expiry and every later publish starts failing. The fix is routine maintenance. Use the long-lived token exchange and refresh flow, call GET /refresh_access_token before expiry, and put the refresh date on a calendar the same way you would a domain renewal.

Refreshing a long-lived token also extends the underlying permission grant by another 90 days, but only for app users with public profiles. Private profiles cannot have their grant extended by refresh, so if your pipeline serves private accounts, plan for periodic re-authorization instead of assuming the connection is permanent. When a token behaves oddly, inspect expiry, scopes, and validity in Meta's Access Token Debugger before rotating anything else.

Store tokens per app and user pair. Threads user access tokens are app-scoped, which means a token issued to one app cannot be reused by another, and your logs should treat the pair as the unit of identity. For multi-account operators, this is the difference between one refresh job and a pile of mysterious dead connections.

Build it or buy it: an honest operator's test

Hand-rolling wins when the job is narrow: one or two accounts you own, publishing logic you fully control, an integration that scratches a learning itch or slots into an existing internal system. The API is free to use within its limits, and the docs plus the official sample app on GitHub are enough to stand up a basic publisher without buying a scheduler. For many solo operators that is genuinely enough.

Buying wins when the cost curve of operational detail bends against you: many accounts each needing token refresh tracking, approval steps before publish, retry logic that is safe by default, and quota visibility across a team. That stack of operational chores is precisely what scheduling tools exist to absorb, and it is why the Threads ecosystem has approved third-party apps in the first place.

If you weigh tools, our Threads scheduler comparison walks through what to compare before choosing one, and the free Threads Post Creator on this site handles the writing end without any API setup at all. The honest summary: publish through the API when the pipeline itself is the product; use a scheduler when the posts are.

Action checklist

Use this as the practical next pass after reading the guide.

  1. +
    Create a Meta app with the Threads use case and save the Threads app ID and secret
  2. +
    Add yourself as a Threads tester and grant threads_basic plus threads_content_publish
  3. +
    Exchange for a long-lived token and calendar a refresh before day 60
  4. +
    Validate image and video files against spec before container creation
  5. +
    Publish only from containers whose status reads FINISHED, and store container IDs as idempotency keys
  6. +
    Log quota_usage beside every publish and alert before the rolling window pinches
A published Threads post on a phone beside a pipeline log listing container status entries
Status-first retries keep the feed clean.
Wrap-up

Conclusion

The Threads API is unusually legible for a publishing platform: two calls to make a post, published limits you can query on demand, explicit error codes, and a token model with no hidden mechanics. The operators who ship reliable pipelines are not the ones with clever code. They are the ones who respect the 30-second processing wait, check container status before retrying anything, and treat the 60-day token refresh as a date with consequences.

Start with one text post through the two-step flow, add media once containers feel predictable, and only then wire in scheduling. Every failure mode in this guide is cheaper to meet in that order.