Threads API: Two-Step Publishing, Threading Gotchas, and Content Moderation

Social API Engineering web

TL;DR

Threads publishing is a two-step process: create a container, then publish it. Replies must ALL point to reply_to_id=root_id — replying to a reply creates a separate conversation. Meta App ID and Threads App ID are different things. Content moderation happens at container creation, not publish time.

Two-Step Publish Flow

Every post to Threads goes through two API calls:

POST /{user-id}/threads
  media_type=TEXT
  text="Your post content"
  → returns { id: "container_id" }

POST /{user-id}/threads_publish
  creation_id={container_id}
  → returns { id: "media_id" }

The container is where validation happens — character limits (500, not 450), content moderation, and parameter parsing. The publish call just makes it live. If the container creation succeeds, the publish almost always will too.

This means you can pre-validate content by creating containers without publishing them. Useful for pipelines that need to sanitize text before posting.

The Threading Bug That Wastes Everyone’s Time

When creating threaded replies (splitting long content across multiple posts), the intuitive approach is to chain them:

Post A (root)
  └── Post B (reply_to_id = A)
        └── Post C (reply_to_id = B)  ← WRONG

This creates three separate conversations. Threads does not nest replies the way you’d expect. The correct approach:

Post A (root)
  ├── Post B (reply_to_id = A)
  └── Post C (reply_to_id = A)  ← ALL replies point to root

Every reply in a thread must target the original post’s ID. Not the previous reply. Not the parent. The root. This is undocumented behavior that contradicts how every other threaded platform works.

Meta App ID vs. Threads App ID

When setting up OAuth, you encounter two different IDs:

  • Meta App ID (parent): The app registered in Meta Developer Console (e.g., 1767888910690641)
  • Threads App ID (product): The Threads-specific product ID (e.g., 1557745284864598)

OAuth client_id uses the Threads App ID, not the parent Meta App ID. The developer console shows the Meta App ID prominently. The Threads App ID is buried in the product settings. Using the wrong one gives unhelpful error messages.

For tester accounts, the User Token Generator in Meta Developer Console bypasses the full OAuth flow entirely. No redirect URI, no authorization code exchange. Just click and get a token. This is the fastest path for personal-use bots.

Content Moderation at Container Creation

Threads enforces content moderation during container creation, not at publish time. Certain terms trigger rejection regardless of context — including some that appear in literature and academic texts.

For an automated pipeline that posts audiobook transcriptions, this means sanitization must happen before the API call. The approach: maintain a redaction list that replaces sensitive terms with bracketed placeholders for the Threads post, while keeping the original verbatim transcript in the local database.

Never sanitize source data. Only sanitize the output channel.

The topic_tag Parameter

Container creation accepts a topic_tag parameter for categorizing posts. Constraints:

  • No periods allowed
  • No ampersands allowed
  • Applied at container creation time
  • Useful for organizing bot output (e.g., bytheweiaudiomark)

Practical Takeaways

  1. Validate before publishing. Create the container first. If it succeeds, the content passed moderation. If not, you can retry with sanitized text without wasting a publish call.

  2. Thread replies to root, always. This is the single most common mistake in Threads API implementations. Test with three or more posts to catch it — two-post threads work either way.

  3. Separate your app IDs. If you’re building both read and write integrations, use separate Meta apps. Keep analysis (read-only) separate from publishing (write). Different token scopes, different rate limit pools.

  4. 500 characters, not 450. The actual limit is 500. Various unofficial docs and libraries hardcode 450. Split at sentence boundaries for clean threading.

Developer Perspective

The Threads API is functional but feels like it was designed for the publishing flow of a human-operated app, not automated pipelines. Two-step publish makes sense for draft-and-preview UX. It makes less sense for fire-and-forget automation where you just want to post text.

The threading behavior is the real trap. It violates the principle of least surprise — every developer’s mental model of “reply to the last message” is wrong here. The fact that it silently creates separate conversations instead of erroring makes it worse. You ship broken threads without knowing until someone checks the app.

That said, once you know the rules, the API is reliable. The moderation is aggressive but consistent. And having topic_tag for bot output is genuinely useful for keeping automated posts discoverable without polluting your main feed’s aesthetics.