{"id":14984949,"url":"https://github.com/codybrom/denim","last_synced_at":"2026-02-09T06:21:33.880Z","repository":{"id":251307716,"uuid":"837023466","full_name":"codybrom/denim","owner":"codybrom","description":"Deno/Typescript wrapper for posting with the Threads API","archived":false,"fork":false,"pushed_at":"2024-09-24T16:16:02.000Z","size":104,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-10T23:14:34.015Z","etag":null,"topics":["deno","threads","threads-api"],"latest_commit_sha":null,"homepage":"https://jsr.io/@codybrom/denim","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/codybrom.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-08-02T03:57:06.000Z","updated_at":"2024-10-07T21:49:45.000Z","dependencies_parsed_at":"2024-09-29T18:40:56.121Z","dependency_job_id":"87efb7d4-c61b-4944-9e48-142d5469cd5d","html_url":"https://github.com/codybrom/denim","commit_stats":{"total_commits":27,"total_committers":1,"mean_commits":27.0,"dds":0.0,"last_synced_commit":"4d0561fabe45013dad6bb8a1266dce01b0f73708"},"previous_names":["codybrom/denim"],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codybrom%2Fdenim","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codybrom%2Fdenim/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codybrom%2Fdenim/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codybrom%2Fdenim/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/codybrom","download_url":"https://codeload.github.com/codybrom/denim/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248312134,"owners_count":21082638,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["deno","threads","threads-api"],"created_at":"2024-09-24T14:09:54.585Z","updated_at":"2026-02-09T06:21:33.874Z","avatar_url":"https://github.com/codybrom.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Denim\n\n[![JSR](https://jsr.io/badges/@codybrom/denim)](https://jsr.io/@codybrom/denim)\n[![JSR Score](https://jsr.io/badges/@codybrom/denim/score)](https://jsr.io/@codybrom/denim)\n\nA Deno/TypeScript wrapper for the\n[Threads API](https://developers.facebook.com/docs/threads). Covers posting,\nretrieval, replies, profiles, insights, search, locations, tokens, and oEmbed.\n\nYou'll need a Threads app with an access token from\n[Meta's developer portal](https://developers.facebook.com/apps/). See the\n[Threads API docs](https://developers.facebook.com/docs/threads/get-started) for\nsetup.\n\n```bash\ndeno add @codybrom/denim\n```\n\n## Publishing\n\nThreads publishing is two steps: create a container, then publish it.\n\n```typescript\nimport {\n\tcreateThreadsContainer,\n\tpublishThreadsContainer,\n} from \"@codybrom/denim\";\n\nconst containerId = await createThreadsContainer({\n\tuserId: \"YOUR_USER_ID\",\n\taccessToken: \"YOUR_ACCESS_TOKEN\",\n\tmediaType: \"TEXT\",\n\ttext: \"Hello from Denim!\",\n});\n\nawait publishThreadsContainer(\"YOUR_USER_ID\", \"YOUR_ACCESS_TOKEN\", containerId);\n```\n\n`createThreadsContainer(request)` takes a `ThreadsPostRequest` and returns the\ncontainer ID string. The request requires `userId`, `accessToken`, `mediaType`,\nand usually `text`. Optional fields control the post type:\n\n| Field                     | Type       | Purpose                                                                                                       |\n| ------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------- |\n| `imageUrl`                | `string`   | Image URL (for `IMAGE` or `CAROUSEL` items)                                                                   |\n| `videoUrl`                | `string`   | Video URL (for `VIDEO` or `CAROUSEL` items)                                                                   |\n| `altText`                 | `string`   | Alt text for images and videos                                                                                |\n| `linkAttachment`          | `string`   | URL to attach to a `TEXT` post                                                                                |\n| `replyControl`            | `string`   | `\"everyone\"`, `\"accounts_you_follow\"`, `\"mentioned_only\"`, `\"parent_post_author_only\"`, or `\"followers_only\"` |\n| `allowlistedCountryCodes` | `string[]` | ISO country codes to restrict post visibility                                                                 |\n| `replyToId`               | `string`   | Post ID to reply to                                                                                           |\n| `quotePostId`             | `string`   | Post ID to quote                                                                                              |\n| `pollAttachment`          | `object`   | `{ option_a, option_b, option_c?, option_d? }`                                                                |\n| `topicTag`                | `string`   | Topic tag for the post                                                                                        |\n| `isGhostPost`             | `boolean`  | Make a ghost post (text only, expires in 24h)                                                                 |\n| `isSpoilerMedia`          | `boolean`  | Hide media behind a spoiler overlay                                                                           |\n| `textEntities`            | `array`    | Text spoiler ranges: `[{ entity_type, offset, length }]`                                                      |\n| `textAttachment`          | `object`   | Long-form text: `{ plaintext, link_attachment_url? }`                                                         |\n| `gifAttachment`           | `object`   | GIF: `{ gif_id, provider }`                                                                                   |\n| `locationId`              | `string`   | Location ID from `searchLocations`                                                                            |\n| `children`                | `string[]` | Carousel item IDs from `createCarouselItem`                                                                   |\n| `autoPublishText`         | `boolean`  | Skip the publish step for text posts                                                                          |\n\n`publishThreadsContainer(userId, accessToken, containerId, getPermalink?)`\npublishes a container. Pass `true` for `getPermalink` to get `{ id, permalink }`\ninstead of just the ID string.\n\n`createCarouselItem(request)` creates individual items for a carousel post.\nTakes the same request shape but `mediaType` must be `\"IMAGE\"` or `\"VIDEO\"`.\n\n`repost(mediaId, accessToken)` reposts an existing thread. Returns `{ id }`.\n\n`deleteThread(mediaId, accessToken)` deletes a thread. Returns\n`{ success: boolean, deleted_id?: string }`.\n\n## Retrieval\n\nAll retrieval functions accept an optional `fields` string array to request\nspecific fields, and an optional `PaginationOptions` object\n(`{ since?, until?, limit?, before?, after? }`).\n\n`getThreadsList(userId, accessToken, options?, fields?)` returns a user's\nthreads as `{ data: ThreadsPost[], paging }`.\n\n`getSingleThread(mediaId, accessToken, fields?)` returns a single `ThreadsPost`.\n\n`getGhostPosts(userId, accessToken, options?, fields?)` returns a user's ghost\nposts.\n\n## Profiles\n\n`getProfile(userId, accessToken, fields?)` returns the authenticated user's\n`ThreadsProfile` (username, name, bio, profile picture, verification status).\n\n`lookupProfile(accessToken, username, fields?)` looks up any public profile by\nusername. Returns a `PublicProfile` with follower counts and engagement stats.\nRequires `threads_profile_discovery` permission.\n\n`getProfilePosts(accessToken, username, options?, fields?)` returns a public\nprofile's posts.\n\n## Replies\n\n`getReplies(mediaId, accessToken, options?, fields?, reverse?)` returns direct\nreplies to a post. Pass `reverse: false` for chronological order (default is\nreverse chronological).\n\n`getConversation(mediaId, accessToken, options?, fields?, reverse?)` returns the\nfull conversation thread (replies and nested replies). Pass `reverse: false` for\nchronological order.\n\n`getUserReplies(userId, accessToken, options?, fields?)` returns all replies\nmade by a user.\n\n`manageReply(replyId, accessToken, hide)` hides or unhides a reply. Pass `true`\nto hide, `false` to unhide.\n\n## Insights\n\n`getMediaInsights(mediaId, accessToken, metrics)` returns metrics for a post.\nPass metric names as a string array: `\"views\"`, `\"likes\"`, `\"replies\"`,\n`\"reposts\"`, `\"quotes\"`, `\"shares\"`.\n\n```typescript\nconst insights = await getMediaInsights(postId, token, [\"views\", \"likes\"]);\n// insights.data[0].values[0].value =\u003e 42\n```\n\n`getUserInsights(userId, accessToken, metrics, options?)` returns user-level\nmetrics. Accepts an options object with `since`/`until` timestamps and\n`breakdown` for demographics.\n\n## Search \u0026 Locations\n\n`searchKeyword(accessToken, options, fields?)` searches posts by keyword or\ntopic tag. Options:\n`{ q, search_type?, search_mode?, media_type?, author_username?,\n...pagination }`.\nRequires `threads_keyword_search` permission for searching beyond your own\nposts.\n\n`searchLocations(accessToken, options, fields?)` searches for locations by name\nor coordinates. Options: `{ query?, latitude?, longitude? }`. Returns location\nobjects with IDs you can pass to `createThreadsContainer` as `locationId`.\n\n`getLocation(locationId, accessToken, fields?)` returns details for a location\n(name, address, city, country, coordinates).\n\n## Tokens\n\n`exchangeCodeForToken(clientId, clientSecret, code, redirectUri)` exchanges an\nOAuth authorization code for a short-lived access token. Returns\n`{ access_token, user_id }`.\n\n`getAppAccessToken(clientId, clientSecret)` gets an app-level access token via\nclient credentials. Returns `{ access_token, token_type }`.\n\n`exchangeToken(clientSecret, accessToken)` exchanges a short-lived token for a\nlong-lived one (60 days). Returns `{ access_token, token_type, expires_in }`.\n\n`refreshToken(accessToken)` refreshes a long-lived token before it expires. Same\nreturn shape.\n\n`debugToken(accessToken, inputToken)` returns metadata about a token: app ID,\nscopes, expiry, validity.\n\n## Other\n\n`getPublishingLimit(userId, accessToken, fields?)` returns rate limit info: post\nquota, reply quota, and remaining usage.\n\n`getMentions(userId, accessToken, options?, fields?)` returns posts that mention\nthe authenticated user.\n\n`getOEmbed(accessToken, url, maxWidth?)` returns embeddable HTML for a Threads\npost URL. Returns `{ html, provider_name, type, version, width }`.\n\n## Utilities\n\n`validateRequest(request)` checks a `ThreadsPostRequest` for invalid\ncombinations (wrong media type for polls, too many text entities, etc.) and\nthrows descriptive errors. Called automatically by `createThreadsContainer`.\n\n`checkContainerStatus(containerId, accessToken)` polls a container's publishing\nstatus. Returns `{ status, error_message? }` where `status` is `\"FINISHED\"`,\n`\"IN_PROGRESS\"`, `\"EXPIRED\"`, `\"ERROR\"`, or `\"PUBLISHED\"`.\n\n## Testing\n\nDenim ships a `MockThreadsAPI` interface for testing without network requests.\nSet an implementation on `globalThis.threadsAPI` and all functions route through\nit instead of calling the Threads API:\n\n```typescript\nimport { MockThreadsAPIImpl } from \"@codybrom/denim\";\n\nconst mock = new MockThreadsAPIImpl();\n(globalThis as any).threadsAPI = mock;\n\n// Now all denim functions use the mock\nconst container = await createThreadsContainer({ ... });\n\n// Enable error mode to test failure paths\nmock.setErrorMode(true);\n```\n\nSee `mod_test.ts` for more examples.\n\n```bash\ndeno task test\n```\n\n## License\n\n[MIT](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcodybrom%2Fdenim","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcodybrom%2Fdenim","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcodybrom%2Fdenim/lists"}