Threads APIThreads automationauto postingNode.jsMindThread

Threads API Auto Posting Tutorial 2026: From App Setup to Scheduled Posts (Node.js Code Included)

· 60 min read
Table of Contents
  1. Three facts about the Threads API before you start
  2. Step 1: Create the Threads app
  3. Which scopes you need
  4. Step 2: Accept the Threads Tester invitation
  5. Step 3: OAuth and tokens
  6. 3a. Send the user to the authorization page
  7. 3b. Exchange the code for a short-lived token (1 hour)
  8. 3c. Immediately exchange for a 60-day long-lived token
  9. 3d. Refresh before expiry
  10. Step 4: Publish (two-step)
  11. Image posts
  12. Video posts: do not sleep, poll
  13. Step 5: Scheduling. The API has none, so you build it
  14. Common errors, quick lookup
  15. Don't want to build it?

Short answer first: auto posting with the Threads API takes five steps. ① Create an app with the Threads use case in the Meta developer dashboard. ② Have your Threads account accept the Threads Tester invite. ③ Run OAuth, get a 1-hour token, exchange it right away for a 60-day token. ④ Publishing is two-step: POST /{user-id}/threads creates a media container, POST /{user-id}/threads_publish publishes it. ⑤ The API has no scheduler, so you call it from cron or a scheduling tool. Each account gets 250 API-published posts per rolling 24 hours and text posts are capped at 500 characters.

Every code block below is lifted from the production code of MindThread, which uses this API to auto post for 100+ Threads accounts. Where we hit a wall, I say so.

If you do not want to write code and just want "schedule it and forget it", skip to the last section. This article is for people wiring up the API themselves.

Three facts about the Threads API before you start

1. It is Meta's official API at graph.threads.net. It is separate from the Instagram Graph API, with its own App ID and App Secret. When you create the app you will see two sets of IDs; use the Threads pair.

2. Publishing is two-step. You cannot publish in one request. First create a media container and get a creation_id, then publish that id. For text posts a short wait in between is enough; for video you must poll the container status until processing finishes.

3. The quota is a rolling 24-hour window. Official numbers: 250 API-published posts and 1,000 replies per Threads profile per rolling 24 hours. Text is limited to 500 characters. The quota is per account, not per app.

Step 1: Create the Threads app

  1. Go to developers.facebook.com and create a new app with the Threads use case.
  2. In the app dashboard, open the Threads settings and note the Threads App ID and Threads App Secret. Again: the Threads pair, not the Facebook pair.
  3. Set the Redirect URI for OAuth. For local development something like https://localhost:3000/callback is fine; production must be a real HTTPS URL.

Which scopes you need

For auto posting, two:

  • threads_basic reads your own profile and posts
  • threads_content_publish publishes

Both are granted automatically, no App Review. You only need review for replies, reading comments, and insights (threads_manage_replies, threads_read_replies, threads_manage_insights, and so on) if you want them to work for arbitrary users.

MindThread keeps every scope it uses in one constant, annotated as the single source of truth for App Review. We once added a permission in code without adding it to the authorization URL; users authorized, and the new feature silently returned 401.

// auto-granted (no review): threads_basic, threads_content_publish
// everything else needs App Review
const SCOPES = 'threads_basic,threads_content_publish'

Step 2: Accept the Threads Tester invitation

This is where most people get stuck. While the app is in development mode, only accounts added as Threads Testers can use the API.

  1. App dashboard → App rolesRolesAdd People → pick Threads Tester, enter the Threads account.
  2. Log in to Threads with that account (web or mobile), go to Account SettingsWebsite permissions, and accept the pending invite.

Skip step 2 and the whole OAuth flow looks fine until the first publish call, which fails with a permission error.

Step 3: OAuth and tokens

3a. Send the user to the authorization page

https://www.threads.net/oauth/authorize
  ?client_id={THREADS_APP_ID}
  &redirect_uri={REDIRECT_URI}
  &scope=threads_basic,threads_content_publish
  &response_type=code
  &state={random string, CSRF protection}

Always send state and verify it on the way back. MindThread uses an HMAC-signed string and checks signature plus age. We originally set the age limit to 10 minutes; users who hesitated on the consent screen came back and got rejected as CSRF. It is 45 minutes now.

3b. Exchange the code for a short-lived token (1 hour)

const res = await fetch('https://graph.threads.net/oauth/access_token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    client_id: THREADS_APP_ID,
    client_secret: THREADS_APP_SECRET,
    grant_type: 'authorization_code',
    redirect_uri: REDIRECT_URI,
    code,
  }),
})
const { access_token: shortToken, user_id } = await res.json()

3c. Immediately exchange for a 60-day long-lived token

The short-lived token lives one hour. Exchange it on the spot; never store the short one.

const params = new URLSearchParams({
  grant_type: 'th_exchange_token',
  client_secret: THREADS_APP_SECRET,
  access_token: shortToken,
})
const res = await fetch(`https://graph.threads.net/access_token?${params}`)
const { access_token: longToken, expires_in } = await res.json()
// expires_in is in seconds (about 60 days); store the expiry, you will need it

3d. Refresh before expiry

const params = new URLSearchParams({
  grant_type: 'th_refresh_token',
  access_token: currentLongToken,
})
const res = await fetch(`https://graph.threads.net/refresh_access_token?${params}`)
const { access_token: newToken, expires_in } = await res.json()

The number one reason auto posting stops is a token that was never refreshed. MindThread runs a daily job that refreshes every token expiring within 7 days, and writes a flag back to the database when a refresh fails. Otherwise you think the account is alive while it has quietly stopped posting.

Step 4: Publish (two-step)

This is MindThread's production text-post function, nearly verbatim:

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

async function publishTextPost(threadsUserId: string, accessToken: string, text: string) {
  // Step A: create the media container (a draft)
  const createRes = await fetch(`${THREADS_API}/${threadsUserId}/threads`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      media_type: 'TEXT',
      text,
      access_token: accessToken,
    }),
  })
  if (!createRes.ok) {
    const err = await createRes.json().catch(() => ({}))
    throw new Error(`container create failed ${createRes.status}: ${err?.error?.message}`)
  }
  const { id: creationId } = await createRes.json()

  // give the server a moment to process the container (3s is enough for text)
  await new Promise(r => setTimeout(r, 3000))

  // Step B: publish
  const publishRes = await fetch(`${THREADS_API}/${threadsUserId}/threads_publish`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      creation_id: creationId,
      access_token: accessToken,
    }),
  })
  if (!publishRes.ok) {
    const err = await publishRes.json().catch(() => ({}))
    throw new Error(`publish failed ${publishRes.status}: ${err?.error?.message}`)
  }
  return publishRes.json() // { id: post id }
}

Details that matter:

  • threadsUserId is the user_id from step 3b; me also works.
  • Both requests are application/x-www-form-urlencoded, not JSON.
  • The useful error text is in error.message. Always log it; Meta's error codes alone tell you little.

Image posts

Set media_type to IMAGE and add image_url (a publicly reachable HTTPS URL). Everything else is the same; a 3-second wait before publish is fine.

body: new URLSearchParams({
  media_type: 'IMAGE',
  image_url: imageUrl,
  text,
  access_token: accessToken,
})

Video posts: do not sleep, poll

Video needs server-side transcoding and the time varies. A fixed 3-second sleep fails intermittently; we learned that the hard way. Poll the container until it reports FINISHED:

async function waitForContainerReady(creationId: string, accessToken: string) {
  const deadline = Date.now() + 120_000
  await new Promise(r => setTimeout(r, 5000))
  while (Date.now() < deadline) {
    const res = await fetch(
      `${THREADS_API}/${creationId}?fields=status,error_message&access_token=${encodeURIComponent(accessToken)}`
    )
    const { status, error_message } = await res.json()
    if (status === 'FINISHED') return
    if (status === 'ERROR' || status === 'EXPIRED') {
      throw new Error(`video container ${status}: ${error_message || 'no detail'}`)
    }
    await new Promise(r => setTimeout(r, 3000)) // IN_PROGRESS, keep waiting
  }
  throw new Error('video container not FINISHED within 120s')
}

Container status is one of IN_PROGRESS, FINISHED, ERROR, EXPIRED, PUBLISHED. Create the container with media_type: 'VIDEO' and video_url.

Step 5: Scheduling. The API has none, so you build it

The Threads API has no "publish at this time" feature. The official docs only remind you to enforce the publishing quota yourself "if your app allows users to schedule posts". So scheduling is your job:

Approach Good for Watch out for
System cron / systemd timer One or two accounts of your own The machine must stay on; schedule token refresh too
Vercel Cron / Cloudflare Cron Triggers People already on serverless Function time limits; video polling can exceed them
n8n / Make Non-coders who can build flows You still own dedup and token refresh
A product like MindThread Many accounts, needs to be reliable Scheduling, dedup, refresh, and retries are built in

Whichever you pick, three things are non-negotiable:

  1. Record what you already published. A scheduler that reruns or retries without dedup will double post, and Threads does not treat that kindly.
  2. Count your own daily posts instead of waiting for the API to reject you.
  3. Split failures into two classes. Timeouts and 5xx are transient and can be retried with backoff. Permission and token errors are permanent: stop and alert a human, retrying only repeats the error.

Common errors, quick lookup

Symptom Usually
OAuth works, publish says no permission The Threads account never accepted the tester invite
Everything fails after one hour You stored the short-lived token instead of exchanging it
Failures around day 60 Long-lived token was never refreshed
Video posts fail intermittently, text is fine Fixed sleep instead of polling container status
Image posts fail image_url is not public HTTPS, or the file is too large / wrong format
Everything fails mid-day You hit the 250-post rolling quota

Don't want to build it?

All of the above is already a product: MindThread connects your Threads accounts, lets you set time slots, and handles scheduling, dedup, token renewal, retries, and multi-account management, with a free tier to start. For the no-code path see Best Threads Auto Posting Tools in 2026.

If what you actually want is automated engagement (commenting and liking on other people's posts), that is not something the Threads API does: replies need reviewed permissions and the API does not expose the feed. See Threads automation guide for what is and is not possible.


Code in this article comes from MindThread's production codebase and is current as of August 2026. We update this page when Meta changes the API; for quotas and permission names, the official Threads API documentation is authoritative.

FAQ

Is the Threads API free to use?

Yes. The API itself has no fee. Create an app with the Threads use case in the Meta developer dashboard and you can start. Your own account works through the Threads Tester flow without App Review. The limits are 250 API-published posts and 1,000 replies per account per rolling 24 hours.

Does the Threads API support scheduled posts?

No. The API only publishes now. Scheduling is on you: run a cron job, n8n, or Vercel Cron that calls the publish endpoints at the right time, and keep a record of what you already posted so retries do not duplicate. Tools like MindThread already bundle scheduling, dedup, and token renewal.

How long does a Threads access token last?

The short-lived token from OAuth lasts 1 hour. Exchange it immediately with th_exchange_token for a 60-day long-lived token, then refresh with th_refresh_token before it expires. Forgetting the refresh is the most common reason auto posting silently stops.

Why does the Threads API say I do not have permission?

Most often the Threads account has not accepted the tester invitation. After you add the account as a Threads Tester in the app dashboard, log in to Threads with that account and accept the invite under Account Settings, Website permissions.

Weekly AI Automation Playbook

No fluff — just templates, SOPs, and technical breakdowns you can use right away.

Join the Solo Lab Community

Free resource packs, daily build logs, and AI agents you can talk to. A community for solo devs who build with AI.

Need Technical Help?

Free consultation — reply within 24 hours.