A Deno/TypeScript wrapper for the Threads API. Covers posting, retrieval, replies, profiles, insights, search, locations, tokens, and oEmbed.
You'll need a Threads app with an access token from Meta's developer portal. See the Threads API docs for setup.
deno add @codybrom/denimThreads publishing is two steps: create a container, then publish it.
import {
createThreadsContainer,
publishThreadsContainer,
} from "@codybrom/denim";
const containerId = await createThreadsContainer({
userId: "YOUR_USER_ID",
accessToken: "YOUR_ACCESS_TOKEN",
mediaType: "TEXT",
text: "Hello from Denim!",
});
await publishThreadsContainer("YOUR_USER_ID", "YOUR_ACCESS_TOKEN", containerId);createThreadsContainer(request) takes a ThreadsPostRequest and returns the
container ID string. The request requires userId, accessToken, mediaType,
and usually text. Optional fields control the post type:
| Field | Type | Purpose |
|---|---|---|
imageUrl |
string |
Image URL (for IMAGE or CAROUSEL items) |
videoUrl |
string |
Video URL (for VIDEO or CAROUSEL items) |
altText |
string |
Alt text for images and videos |
linkAttachment |
string |
URL to attach to a TEXT post |
replyControl |
string |
"everyone", "accounts_you_follow", "mentioned_only", "parent_post_author_only", or "followers_only" |
allowlistedCountryCodes |
string[] |
ISO country codes to restrict post visibility |
replyToId |
string |
Post ID to reply to |
quotePostId |
string |
Post ID to quote |
pollAttachment |
object |
{ option_a, option_b, option_c?, option_d? } |
topicTag |
string |
Topic tag for the post |
isGhostPost |
boolean |
Make a ghost post (text only, expires in 24h) |
isSpoilerMedia |
boolean |
Hide media behind a spoiler overlay |
textEntities |
array |
Text spoiler ranges: [{ entity_type, offset, length }] |
textAttachment |
object |
Long-form text: { plaintext, link_attachment_url? } |
gifAttachment |
object |
GIF: { gif_id, provider } |
locationId |
string |
Location ID from searchLocations |
children |
string[] |
Carousel item IDs from createCarouselItem |
autoPublishText |
boolean |
Skip the publish step for text posts |
publishThreadsContainer(userId, accessToken, containerId, getPermalink?)
publishes a container. Pass true for getPermalink to get { id, permalink }
instead of just the ID string.
createCarouselItem(request) creates individual items for a carousel post.
Takes the same request shape but mediaType must be "IMAGE" or "VIDEO".
repost(mediaId, accessToken) reposts an existing thread. Returns { id }.
deleteThread(mediaId, accessToken) deletes a thread. Returns
{ success: boolean, deleted_id?: string }.
All retrieval functions accept an optional fields string array to request
specific fields, and an optional PaginationOptions object
({ since?, until?, limit?, before?, after? }).
getThreadsList(userId, accessToken, options?, fields?) returns a user's
threads as { data: ThreadsPost[], paging }.
getSingleThread(mediaId, accessToken, fields?) returns a single ThreadsPost.
getGhostPosts(userId, accessToken, options?, fields?) returns a user's ghost
posts.
getProfile(userId, accessToken, fields?) returns the authenticated user's
ThreadsProfile (username, name, bio, profile picture, verification status).
lookupProfile(accessToken, username, fields?) looks up any public profile by
username. Returns a PublicProfile with follower counts and engagement stats.
Requires threads_profile_discovery permission.
getProfilePosts(accessToken, username, options?, fields?) returns a public
profile's posts.
getReplies(mediaId, accessToken, options?, fields?, reverse?) returns direct
replies to a post. Pass reverse: false for chronological order (default is
reverse chronological).
getConversation(mediaId, accessToken, options?, fields?, reverse?) returns the
full conversation thread (replies and nested replies). Pass reverse: false for
chronological order.
getUserReplies(userId, accessToken, options?, fields?) returns all replies
made by a user.
manageReply(replyId, accessToken, hide) hides or unhides a reply. Pass true
to hide, false to unhide.
getMediaInsights(mediaId, accessToken, metrics) returns metrics for a post.
Pass metric names as a string array: "views", "likes", "replies",
"reposts", "quotes", "shares".
const insights = await getMediaInsights(postId, token, ["views", "likes"]);
// insights.data[0].values[0].value => 42getUserInsights(userId, accessToken, metrics, options?) returns user-level
metrics. Accepts an options object with since/until timestamps and
breakdown for demographics.
searchKeyword(accessToken, options, fields?) searches posts by keyword or
topic tag. Options:
{ q, search_type?, search_mode?, media_type?, author_username?, ...pagination }.
Requires threads_keyword_search permission for searching beyond your own
posts.
searchLocations(accessToken, options, fields?) searches for locations by name
or coordinates. Options: { query?, latitude?, longitude? }. Returns location
objects with IDs you can pass to createThreadsContainer as locationId.
getLocation(locationId, accessToken, fields?) returns details for a location
(name, address, city, country, coordinates).
exchangeCodeForToken(clientId, clientSecret, code, redirectUri) exchanges an
OAuth authorization code for a short-lived access token. Returns
{ access_token, user_id }.
getAppAccessToken(clientId, clientSecret) gets an app-level access token via
client credentials. Returns { access_token, token_type }.
exchangeToken(clientSecret, accessToken) exchanges a short-lived token for a
long-lived one (60 days). Returns { access_token, token_type, expires_in }.
refreshToken(accessToken) refreshes a long-lived token before it expires. Same
return shape.
debugToken(accessToken, inputToken) returns metadata about a token: app ID,
scopes, expiry, validity.
getPublishingLimit(userId, accessToken, fields?) returns rate limit info: post
quota, reply quota, and remaining usage.
getMentions(userId, accessToken, options?, fields?) returns posts that mention
the authenticated user.
getOEmbed(accessToken, url, maxWidth?) returns embeddable HTML for a Threads
post URL. Returns { html, provider_name, type, version, width }.
validateRequest(request) checks a ThreadsPostRequest for invalid
combinations (wrong media type for polls, too many text entities, etc.) and
throws descriptive errors. Called automatically by createThreadsContainer.
checkContainerStatus(containerId, accessToken) polls a container's publishing
status. Returns { status, error_message? } where status is "FINISHED",
"IN_PROGRESS", "EXPIRED", "ERROR", or "PUBLISHED".
Denim ships a MockThreadsAPI interface for testing without network requests.
Set an implementation on globalThis.threadsAPI and all functions route through
it instead of calling the Threads API:
import { MockThreadsAPIImpl } from "@codybrom/denim";
const mock = new MockThreadsAPIImpl();
(globalThis as any).threadsAPI = mock;
// Now all denim functions use the mock
const container = await createThreadsContainer({ ... });
// Enable error mode to test failure paths
mock.setErrorMode(true);See mod_test.ts for more examples.
deno task test