npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@codybrom/denim

v2.0.0

Published

A Deno/TypeScript library for the Threads API

Readme

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