---
title: Threads
description: Publish Threads containers, carousels, and media while preserving processing outcomes for explicit reconciliation.
---

The direct Threads adapter uses an existing user access token and Threads user ID. OAuth and account selection are application responsibilities. Set `THREADS_GRAPH_VERSION` deliberately when a version other than the adapter default (`v1.0`) is required.

## Setup

1. **Complete Meta app setup**

    Create a Meta app with Threads API access and complete the app review your use case requires.
    Token, user ID, scopes, and account eligibility remain server-owned setup.

2. **Store credentials server-side**

    Set `THREADS_USER_ID` and `THREADS_ACCESS_TOKEN` in server configuration, and
    `THREADS_GRAPH_VERSION` only when you need a non-default version.

3. **Configure the adapter**

    Construct the backend and select the account through a connected-account reference.

```ts
import { createSocial, connectedAccountRef } from "@opencoredev/social-sdk";
import { threads } from "@opencoredev/social-sdk/threads";

const social = createSocial({
  backend: threads({
    graphVersion: process.env.THREADS_GRAPH_VERSION,
    auth: {
      userId: process.env.THREADS_USER_ID!,
      accessToken: process.env.THREADS_ACCESS_TOKEN!,
    },
  }),
});

const account = connectedAccountRef({
  backend: "default",
  platform: "threads",
  accountId: process.env.THREADS_USER_ID!,
});
```

## Examples

### Publish a text post

```ts
const result = await social.posts.publish({
  targets: [{ account }],
  content: { text: "Hello Threads" },
});
```

### Publish a single image or video

The adapter requires caller-provided HTTPS URLs; it does not upload local bytes.

```ts
const result = await social.posts.publish({
  targets: [{ account }],
  content: {
    text: "One image",
    media: [
      {
        kind: "image",
        source: { kind: "https-url", url: "https://cdn.example.com/photo.jpg" },
      },
    ],
  },
});
```

### Publish a carousel

Carousel children are created before the parent container. The adapter accepts at most ten media items.

```ts
const result = await social.posts.publish({
  targets: [{ account }],
  content: {
    text: "A short gallery",
    media: [
      { kind: "image", source: { kind: "https-url", url: "https://cdn.example.com/1.jpg" } },
      { kind: "image", source: { kind: "https-url", url: "https://cdn.example.com/2.jpg" } },
      {
        kind: "video",
        source: { kind: "https-url", url: "https://cdn.example.com/clip.mp4" },
      },
    ],
  },
});
```

### Reconcile a processing container

Threads publishing has two upstream phases: container creation and `threads_publish`. If the publish call does not return a native post ID, the result is `processing`; retain the delivery reference and reconcile explicitly. A returned ID produces `published`. Unknown upstream responses remain unknown. There is no automatic polling or cross-provider fallback.

```ts
const outcome = result.outcomes[0];
if (outcome?.state === "processing" && outcome.delivery) {
  // Later, from your own job runner:
  const delivery = await social.posts.getDelivery(outcome.delivery);
  console.log(delivery.state); // published | processing | failed | unknown
}
```

## Beyond publishing

Reads, lifetime metrics, replies, quote posts, reposts, deletion, keyword search, and mentions are exposed through the typed adapter and native module. Normalized search is `social.search.posts(account, { query, cursor, limit, startTime, endTime, scope })`; it calls `GET /keyword_search` with the documented post fields and returns a page with an optional next cursor. Threads supports the recent search horizon only, so `scope: "all"` is rejected with `invalid_input`. The native search method additionally accepts `searchType` (`TOP` or `RECENT`), `searchMode` (`KEYWORD` or `TAG`), `mediaType`, `since`, `until`, and `authorUsername`. Public keyword search requires Meta's `threads_keyword_search` approval; without it, results may be limited to the authorized account's own posts. The adapter sends `q` and preserves empty result pages as `{ items: [] }`.

Profile reads use `GET /{threads-user-id}?fields=...` for the authorized app-scoped user. A public username can use `GET /profile_lookup?username=...`, which requires `threads_profile_discovery` and is subject to Meta's discovery limits. Profile responses are mapped from `profile_picture_url` and `biography`; if discovery omits an ID, the adapter preserves that limitation in the native result instead of claiming the username is a provider ID. Threads does not expose follow, block, or mute APIs, so those graph operations are not advertised.

Two operations are declared `unsupported-by-platform` in the capability manifest. `profiles.search` is unsupported because profile discovery only matches an exact username and Threads has no query-based profile search. `graph.read` is unsupported because the Threads user node has no followers or following edge. The authorized user's total follower count is still available as the `followers_count` metric from `social.analytics.getAccountMetrics`, which requires `threads_manage_insights`.

Reply moderation is available through native methods: `hideReply` calls `POST /{reply-id}/manage_reply` with `hide=true|false`; `listConversation` calls `GET /{media-id}/conversation`; `listPendingReplies` calls `GET /{media-id}/pending_replies`; and `managePendingReply` calls `POST /{reply-id}/manage_pending_reply` with `approve=true|false`. These operations require `threads_manage_replies` (and `threads_read_replies` for reads). Mentions call `GET /{threads-user-id}/mentions` and require `threads_manage_mentions` approval.

`deleteComment({ account, commentId, context })` deletes a reply with `DELETE /{threads-media-id}`, where the ID is the reply media ID returned when the reply was published. Meta only allows deleting media the authenticated user created, requires `threads_basic` and `threads_delete`, and limits each profile to 100 deletions per 24 hours. Use `hideReply` for replies from other users. See Meta's [delete posts](https://developers.facebook.com/docs/threads/posts/delete-posts) guide.

## Limits and requirements

:::note
The direct Threads adapter does not advertise native scheduling. Schedule a publish through an application-owned durable job runner when your workflow needs it.
:::

- Text, one HTTPS image or video, and carousels are supported.
- The token, user ID, scopes, app review, and account eligibility remain server-owned setup.
- Existing repository evidence covers adapter contract behavior and mocked transports; no live Threads account has been verified.

Consult Meta's [Threads API collection](https://www.postman.com/meta/threads/documentation/dht3nzz/threads-api) for current fields, permissions, media processing, and version changes.
