@bernierllc/social-media-threads
v1.2.0
Published
Threads API (Meta) integration service with OAuth 2.0, two-step container-based publishing, and token lifecycle management
Readme
@bernierllc/social-media-threads
Threads API (Meta) integration service with OAuth 2.0, two-step container-based publishing, and access token lifecycle management.
Features
- OAuth 2.0 authorization code flow (
threads_basic,threads_content_publishscopes) - Short-lived → long-lived token exchange and long-lived token refresh
- Single-account-per-instance credential storage
- Two-step container-based publishing (text, image, video posts)
- Typed error classes with token-expiry handling
- Optional NeverHub service-discovery registration (graceful degradation)
Installation
npm install @bernierllc/social-media-threadsUsage
Basic Setup
import { ThreadsService } from '@bernierllc/social-media-threads';
const threads = new ThreadsService({
clientId: 'your-threads-app-id',
clientSecret: 'your-threads-app-secret',
callbackUrl: 'https://your-app.com/callback',
apiVersion: 'v1.0', // Optional, defaults to v1.0
});Authentication (OAuth flow)
// 1. Redirect the user to the authorization URL
const authUrl = threads.getAuthorizationUrl('optional-csrf-state');
// 2. On callback, exchange the returned `code` for a short-lived token
const shortLived = await threads.exchangeCodeForToken(code);
// 3. Exchange the short-lived token for a long-lived token (~60 days)
const longLived = await threads.exchangeForLongLivedToken();
// 4. Before it expires, refresh the long-lived token
const refreshed = await threads.refreshLongLivedToken();Authenticate with existing credentials
const authResult = await threads.authenticate({
accessToken: 'existing-long-lived-token',
tokenType: 'long_lived',
userId: '1234567',
expiresAt: Date.now() + 3600000,
});
if (authResult.success) {
console.log('Authenticated as:', authResult.user?.username);
}Posting Content
Publishing is a two-step process (create a media container, then publish it). postThread (and its alias publishThread) wraps both steps:
const result = await threads.postThread({
mediaType: 'TEXT',
text: 'Hello Threads!',
});
if (result.success) {
console.log('Post ID:', result.postId);
} else {
console.error('Error:', result.error);
}Image and video posts use a media URL instead of text-only content:
await threads.postThread({
mediaType: 'IMAGE',
imageUrl: 'https://example.com/image.jpg',
text: 'Optional caption',
});
await threads.postThread({
mediaType: 'VIDEO',
videoUrl: 'https://example.com/video.mp4',
replyControl: 'accounts_you_follow', // optional
});The two steps are also available individually:
const containerId = await threads.createMediaContainer({ mediaType: 'TEXT', text: 'Hello!' });
const publishResult = await threads.publishMediaContainer(containerId);NeverHub Service Discovery
initialize() registers the service with NeverHub. It is entirely optional —
every method below works without calling it — and it is safe to call when
NeverHub is not running: registration silently no-ops in degraded mode.
await threads.initialize();API Reference
ThreadsService
Constructor
new ThreadsService(config: ThreadsServiceConfig)ThreadsServiceConfig:
clientId(string, required): Threads App IDclientSecret(string, required): Threads App SecretcallbackUrl(string, required): OAuth callback URLscopes(string[], optional): OAuth scopes (default:['threads_basic', 'threads_content_publish'])apiVersion(string, optional): API version (default:'v1.0')
Methods
getAuthorizationUrl(state?: string, callbackUrl?: string): string
Build the URL to redirect the user to for authorization.
authenticate(credentials: ThreadsCredentials): Promise
Store credentials on the instance and verify them by fetching the profile.
exchangeCodeForToken(code: string, callbackUrl?: string): Promise
Exchange an OAuth authorization code for a short-lived access token.
exchangeForLongLivedToken(shortLivedToken?: string): Promise
Exchange a short-lived token for a long-lived token (defaults to the stored token).
refreshLongLivedToken(accessToken?: string): Promise
Refresh a long-lived token before it expires (defaults to the stored token).
createMediaContainer(content: ThreadsContent): Promise
Create a media container (step 1 of publishing). Throws SocialMediaThreadsError on failure.
publishMediaContainer(containerId: string): Promise
Publish a previously created container (step 2 of publishing).
postThread(content: ThreadsContent): Promise / publishThread(content: ThreadsContent): Promise
Create and publish a container in one call. publishThread is an alias of postThread.
isAuthenticated(): boolean
Check if the service holds valid, non-expired credentials.
getUser(): ThreadsUser | null
Get the authenticated user's profile, if fetched.
getCredentials(): ThreadsCredentials | null
Get the currently stored credentials.
Requirements
- Node.js >= 18.0.0
- A Threads App configured via the Meta Developer Portal with
threads_basicandthreads_content_publishpermissions
Error Handling
OAuth and token methods return a result object with success/error fields and never throw. Publishing methods differ by step:
createMediaContainerthrows aSocialMediaThreadsError(withcode: 'INVALID_INPUT' | 'OPERATION_FAILED' | 'TOKEN_EXPIRED') on failure.publishMediaContainer,postThread, andpublishThreadreturn a{ success: false, error }result instead of throwing.
try {
const containerId = await threads.createMediaContainer({ mediaType: 'TEXT', text: 'Hi' });
} catch (error) {
if (error instanceof SocialMediaThreadsError && error.code === 'TOKEN_EXPIRED') {
// refresh the token and retry
}
}Testing
npm test # Run tests in watch mode
npm run test:run # Run tests once
npm run test:coverage # Run tests with coverageLicense
Copyright (c) 2025 Bernier LLC
This file is licensed to the client under a limited-use license. The client may use and modify this code only within the scope of the project it was delivered for. Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
Support
For issues and questions, please open an issue on the GitHub repository.
