Skip to content

codybrom/denim

Repository files navigation

Denim

JSR JSR Score

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/denim

Publishing

Threads 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 }.

Retrieval

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.

Profiles

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.

Replies

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.

Insights

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 => 42

getUserInsights(userId, accessToken, metrics, options?) returns user-level metrics. Accepts an options object with since/until timestamps and breakdown for demographics.

Search & Locations

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).

Tokens

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.

Other

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 }.

Utilities

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".

Testing

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

License

MIT

About

Deno/Typescript wrapper for posting with the Threads API

Topics

Resources

License

Stars

Watchers

Forks