TL;DR
Hootsuite’s public API was deprecated in 2024. To build social media management workflows today, integrate directly with native platform APIs—such as Facebook Graph API, X/Twitter API v2, LinkedIn Marketing API, Instagram Graph API, and YouTube Data API. This guide shows how to implement OAuth 2.0, publish and schedule multi-platform posts, aggregate analytics, manage team permissions, and handle rate limits.
Note: Hootsuite previously exposed a public API with OAuth 2.0 authentication and endpoints for profiles, posts, analytics, and teams. Since its deprecation, use Hootsuite partner integrations, webhooks, or native social platform APIs for similar functionality.
Introduction
Hootsuite manages over 30 million social media accounts for 200,000+ businesses across 175+ countries. If you are building a social media dashboard, marketing platform, or reporting tool, direct API integrations let you automate publishing, engagement monitoring, and analytics collection.
For teams managing 20+ accounts, the core implementation goal is usually the same:
- Connect each social account with OAuth.
- Store and refresh tokens securely.
- Normalize post data into a shared internal format.
- Publish through platform-specific adapters.
- Queue scheduled posts and retries.
- Aggregate platform metrics into a reporting model.
Hootsuite API Status and Alternatives
Current API Situation
As of 2024, Hootsuite has deprecated its public API. Consider one of these approaches instead:
| Approach | Description | Best for |
|---|---|---|
| Native platform APIs | Direct integration with Facebook, X/Twitter, LinkedIn, and other platforms | Full control and custom workflows |
| Audiense | Hootsuite-owned audience intelligence | Audience analysis |
| HeyOrca | Social media scheduling API | Content calendars |
| Buffer API | Social media management | Small teams |
| Sprout Social API | Enterprise social management | Large organizations |
| Agorapulse API | Social media CRM | Community management |
Recommended Approach: Native Platform APIs
For most custom applications, native APIs provide the most control over publishing, account management, and analytics.
| Platform | API | Typical capabilities |
|---|---|---|
| Facebook and Instagram | Graph API | Posts, insights, comments |
| X/Twitter | API v2 | Tweets, analytics, streams |
| Marketing API | Posts, company pages, ads | |
| API v5 | Pins, boards, analytics | |
| TikTok | Display API | Videos and user info |
| YouTube | Data API | Videos, playlists, analytics |
Getting Started: Multi-Platform Authentication
Step 1: Register Developer Apps
Create a developer application for every platform you support. Store app credentials in environment variables rather than source control.
const SOCIAL_CREDENTIALS = {
facebook: {
appId: process.env.FB_APP_ID,
appSecret: process.env.FB_APP_SECRET,
redirectUri: process.env.FB_REDIRECT_URI
},
twitter: {
apiKey: process.env.TWITTER_API_KEY,
apiSecret: process.env.TWITTER_API_SECRET,
redirectUri: process.env.TWITTER_REDIRECT_URI
},
linkedin: {
clientId: process.env.LINKEDIN_CLIENT_ID,
clientSecret: process.env.LINKEDIN_CLIENT_SECRET,
redirectUri: process.env.LINKEDIN_REDIRECT_URI
},
instagram: {
appId: process.env.FB_APP_ID, // Uses Facebook Login
appSecret: process.env.FB_APP_SECRET
}
};
Use separate redirect URIs where needed, for example:
https://your-app.example.com/oauth/facebook/callback
https://your-app.example.com/oauth/twitter/callback
https://your-app.example.com/oauth/linkedin/callback
Step 2: Generate OAuth 2.0 Authorization URLs
Create a shared authorization URL builder, but keep scopes and endpoints platform-specific.
const getAuthUrl = (platform, state) => {
const configs = {
facebook: {
url: 'https://www.facebook.com/v18.0/dialog/oauth',
params: {
client_id: SOCIAL_CREDENTIALS.facebook.appId,
redirect_uri: SOCIAL_CREDENTIALS.facebook.redirectUri,
scope: [
'pages_manage_posts',
'pages_read_engagement',
'instagram_basic',
'instagram_content_publish'
].join(','),
state
}
},
twitter: {
url: 'https://twitter.com/i/oauth2/authorize',
params: {
client_id: SOCIAL_CREDENTIALS.twitter.apiKey,
redirect_uri: SOCIAL_CREDENTIALS.twitter.redirectUri,
scope: 'tweet.read tweet.write users.read offline.access',
state,
response_type: 'code'
}
},
linkedin: {
url: 'https://www.linkedin.com/oauth/v2/authorization',
params: {
client_id: SOCIAL_CREDENTIALS.linkedin.clientId,
redirect_uri: SOCIAL_CREDENTIALS.linkedin.redirectUri,
scope: 'w_member_social r_basicprofile',
state,
response_type: 'code'
}
}
};
const config = configs[platform];
if (!config) {
throw new Error(`Unsupported platform: ${platform}`);
}
return `${config.url}?${new URLSearchParams(config.params)}`;
};
Generate and persist a state value before redirecting the user. Validate it in the callback to prevent authorization-request forgery.
Step 3: Exchange the Authorization Code for Tokens
After the platform redirects to your callback route, exchange the authorization code for an access token.
const handleOAuthCallback = async (platform, code) => {
const tokenEndpoints = {
facebook: 'https://graph.facebook.com/v18.0/oauth/access_token',
twitter: 'https://api.twitter.com/2/oauth2/token',
linkedin: 'https://www.linkedin.com/oauth/v2/accessToken'
};
const credentials = SOCIAL_CREDENTIALS[platform];
const response = await fetch(tokenEndpoints[platform], {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
client_id:
credentials.apiKey ||
credentials.appId ||
credentials.clientId,
client_secret:
credentials.appSecret ||
credentials.apiSecret ||
credentials.clientSecret,
redirect_uri: credentials.redirectUri,
code,
grant_type: 'authorization_code'
})
});
if (!response.ok) {
throw new Error(`Token exchange failed for ${platform}`);
}
return response.json();
};
Step 4: Store Tokens Securely
Store tokens per user and per connected social account. Encrypt access and refresh tokens at rest.
const SocialToken = {
userId: 'user_123',
platform: 'facebook',
accessToken: 'encrypted_token_here',
refreshToken: 'encrypted_refresh_token',
tokenExpiry: Date.now() + 5183999, // 60 days
scopes: ['pages_manage_posts', 'pages_read_engagement'],
pageId: 'page_456', // Facebook or Instagram account mapping
pageName: 'My Business Page'
};
At minimum, persist:
- The application user ID
- Platform name
- Encrypted access token
- Encrypted refresh token, when provided
- Token expiration time
- Granted scopes
- Connected page, organization, or account IDs
Post Scheduling and Publishing
Define a Shared Post Payload
Use one internal payload for your UI and scheduler, then translate it to each platform’s API format.
const postData = {
message: 'New article: Building a multi-platform social publisher',
link: 'https://example.com/articles/social-publisher',
photo: 'https://example.com/images/social-publisher.png',
platforms: ['facebook', 'twitter', 'linkedin', 'instagram'],
facebookPageId: 'page_456',
linkedinAuthorUrn: 'urn:li:organization:123',
igAccountId: '17841400000000000'
};
Create a Multi-Platform Publisher
Call each platform adapter only when the selected platform is present.
const createSocialPost = async (postData) => {
const results = {};
if (postData.platforms.includes('facebook')) {
results.facebook = await postToFacebook({
pageId: postData.facebookPageId,
message: postData.message,
link: postData.link,
photo: postData.photo
});
}
if (postData.platforms.includes('twitter')) {
results.twitter = await postToTwitter({
text: postData.message,
media: postData.photo
});
}
if (postData.platforms.includes('linkedin')) {
results.linkedin = await postToLinkedIn({
authorUrn: postData.linkedinAuthorUrn,
text: postData.message,
contentUrl: postData.link
});
}
if (postData.platforms.includes('instagram')) {
results.instagram = await postToInstagram({
igAccountId: postData.igAccountId,
imageUrl: postData.photo,
caption: postData.message
});
}
return results;
};
In production, record the result separately for every platform. A post may succeed on Facebook and fail on LinkedIn, so avoid treating multi-platform publishing as all-or-nothing.
Publish to a Facebook Page
const postToFacebook = async (postData) => {
const token = await getFacebookPageToken(postData.pageId);
const params = new URLSearchParams({
message: postData.message,
access_token: token
});
if (postData.link) {
params.append('link', postData.link);
}
if (postData.photo) {
params.append('photo', postData.photo);
}
const response = await fetch(
`https://graph.facebook.com/v18.0/${postData.pageId}/feed?${params}`,
{ method: 'POST' }
);
return response.json();
};
Publish to X/Twitter
Upload media first when the post includes an image, then include the returned media ID in the tweet request.
const postToTwitter = async (postData) => {
const token = await getTwitterToken();
let mediaIds = [];
if (postData.media) {
const mediaUpload = await uploadTwitterMedia(postData.media, token);
mediaIds = [mediaUpload.media_id_string];
}
const response = await fetch('https://api.twitter.com/2/tweets', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
text: postData.text,
media: mediaIds.length > 0
? { media_ids: mediaIds }
: undefined
})
});
return response.json();
};
Publish to LinkedIn
const postToLinkedIn = async (postData) => {
const token = await getLinkedInToken();
const post = {
author: postData.authorUrn,
lifecycleState: 'PUBLISHED',
specificContent: {
'com.linkedin.ugc.ShareContent': {
shareCommentary: {
text: postData.text
},
shareMediaCategory: postData.contentUrl ? 'ARTICLE' : 'NONE',
media: postData.contentUrl
? [{
status: 'READY',
media: postData.contentUrn,
description: { text: postData.text }
}]
: []
}
},
visibility: {
'com.linkedin.ugc.MemberNetworkVisibility': 'PUBLIC'
}
};
const response = await fetch('https://api.linkedin.com/v2/ugcPosts', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
'X-Restli-Protocol-Version': '2.0.0'
},
body: JSON.stringify(post)
});
return response.json();
};
Publish to Instagram
Instagram publishing uses a two-step flow: create a media container, then publish it.
const postToInstagram = async (postData) => {
const token = await getInstagramToken();
const containerResponse = await fetch(
`https://graph.facebook.com/v18.0/${postData.igAccountId}/media`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
image_url: postData.imageUrl,
caption: postData.caption
})
}
);
const container = await containerResponse.json();
const publishResponse = await fetch(
`https://graph.facebook.com/v18.0/${postData.igAccountId}/media_publish`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
creation_id: container.id
})
}
);
return publishResponse.json();
};
Schedule Posts with a Job Queue
Do not rely on in-process timers for scheduled publishing. Save the post in your database and enqueue a delayed job.
const schedulePost = async (postData, scheduledTime) => {
const scheduledPost = await db.scheduledPosts.create({
message: postData.message,
platforms: postData.platforms,
scheduledTime,
status: 'pending',
media: postData.media,
link: postData.link
});
await jobQueue.add(
'publish-social-post',
{ postId: scheduledPost.id },
{ delay: scheduledTime - Date.now() }
);
return scheduledPost;
};
Process the queued job and persist the per-post result.
jobQueue.process('publish-social-post', async (job) => {
const post = await db.scheduledPosts.findById(job.data.postId);
try {
const result = await createSocialPost(post);
await db.scheduledPosts.update(post.id, {
status: 'published',
publishedAt: new Date(),
results: result
});
return result;
} catch (error) {
await db.scheduledPosts.update(post.id, {
status: 'failed',
error: error.message
});
throw error;
}
});
Track at least these states:
pending -> publishing -> published
-> failed
Analytics and Reporting
Fetch Cross-Platform Analytics
Each platform returns metrics in a different shape. Fetch the raw responses first, then normalize them into a shared reporting format.
const getSocialAnalytics = async (accountId, dateRange) => {
const analytics = {
facebook: await getFacebookAnalytics(accountId.facebook, dateRange),
twitter: await getTwitterAnalytics(accountId.twitter, dateRange),
linkedin: await getLinkedInAnalytics(accountId.linkedin, dateRange),
instagram: await getInstagramAnalytics(accountId.instagram, dateRange)
};
const totals = {
impressions: sum(analytics, 'impressions'),
engagement: sum(analytics, 'engagement'),
clicks: sum(analytics, 'clicks'),
shares: sum(analytics, 'shares'),
comments: sum(analytics, 'comments'),
newFollowers: sum(analytics, 'newFollowers')
};
return { analytics, totals };
};
Fetch Facebook Insights
const getFacebookAnalytics = async (pageId, dateRange) => {
const token = await getFacebookPageToken(pageId);
const metrics = [
'page_impressions_unique',
'page_engaged_users',
'page_post_engagements',
'page_clicks',
'page_fan_adds'
];
const params = new URLSearchParams({
metric: metrics.join(','),
since: dateRange.from,
until: dateRange.until,
access_token: token
});
const response = await fetch(
`https://graph.facebook.com/v18.0/${pageId}/insights?${params}`
);
return response.json();
};
Fetch X/Twitter Analytics
const getTwitterAnalytics = async (userId, dateRange) => {
const token = await getTwitterToken();
const response = await fetch(
`https://api.twitter.com/2/users/${userId}/metrics/private`,
{
headers: {
Authorization: `Bearer ${token}`
}
}
);
return response.json();
};
Fetch LinkedIn Analytics
const getLinkedInAnalytics = async (organizationId, dateRange) => {
const token = await getLinkedInToken();
const response = await fetch(
`https://api.linkedin.com/v2/organizationalEntityFollowerStatistics?q=organizationalEntity&organizationalEntity=${organizationId}`,
{
headers: {
Authorization: `Bearer ${token}`
}
}
);
return response.json();
};
Fetch Instagram Insights
const getInstagramAnalytics = async (igAccountId, dateRange) => {
const token = await getInstagramToken();
const metrics = [
'impressions',
'reach',
'engagement',
'profile_views',
'follower_count'
];
const params = new URLSearchParams({
metric: metrics.join(','),
period: 'day',
since: dateRange.from,
until: dateRange.until
});
const response = await fetch(
`https://graph.facebook.com/v18.0/${igAccountId}/insights?${params}`,
{
headers: {
Authorization: `Bearer ${token}`
}
}
);
return response.json();
};
Use a normalization layer before calculating totals, because raw API response structures differ across platforms.
function sum(analytics, metric) {
return Object.values(analytics).reduce((total, platform) => {
return total + (platform.data?.[metric] || 0);
}, 0);
}
Team Management
Add Role-Based Access Control
A social publishing tool should restrict who can publish, approve, delete, or manage connected accounts.
const TEAM_ROLES = {
ADMIN: 'admin',
MANAGER: 'manager',
CONTRIBUTOR: 'contributor',
VIEWER: 'viewer'
};
const ROLE_PERMISSIONS = {
[TEAM_ROLES.ADMIN]: [
'create',
'read',
'update',
'delete',
'manage_team',
'billing'
],
[TEAM_ROLES.MANAGER]: [
'create',
'read',
'update',
'approve_posts'
],
[TEAM_ROLES.CONTRIBUTOR]: [
'create',
'read'
],
[TEAM_ROLES.VIEWER]: [
'read'
]
};
const checkPermission = (userRole, requiredPermission) => {
const permissions = ROLE_PERMISSIONS[userRole] || [];
return permissions.includes(requiredPermission);
};
Check permissions before sensitive actions:
if (!checkPermission(currentUser.role, 'approve_posts')) {
throw new Error('You do not have permission to approve posts.');
}
Rate Limiting
Platform Rate Limits
Rate limits differ by platform, endpoint, application type, and account plan. Use the applicable platform documentation as the source of truth.
| Platform | Limit | Window |
|---|---|---|
| Facebook Graph | 200 calls | Per hour per user |
| X/Twitter API v2 | 300 tweets | Per 15 minutes |
| 100–500 calls | Per day | |
| 200 calls | Per hour |
Implement Basic Rate Limit Handling
A central request wrapper prevents one platform adapter from exceeding its configured capacity.
class SocialMediaRateLimiter {
constructor() {
this.limits = {
facebook: { limit: 200, window: 3600000 },
twitter: { limit: 300, window: 900000 },
linkedin: { limit: 500, window: 86400000 },
instagram: { limit: 200, window: 3600000 }
};
this.counters = {};
}
async request(platform, endpoint, options) {
await this.waitForCapacity(platform);
const response = await fetch(endpoint, options);
this.incrementCounter(platform);
return response;
}
async waitForCapacity(platform) {
const limit = this.limits[platform];
const counter = this.counters[platform] || {
count: 0,
resetTime: Date.now()
};
if (Date.now() > counter.resetTime + limit.window) {
counter.count = 0;
counter.resetTime = Date.now();
}
if (counter.count >= limit.limit) {
const waitTime = counter.resetTime + limit.window - Date.now();
await new Promise((resolve) => setTimeout(resolve, waitTime));
}
this.counters[platform] = counter;
}
incrementCounter(platform) {
if (!this.counters[platform]) {
this.counters[platform] = {
count: 0,
resetTime: Date.now()
};
}
this.counters[platform].count++;
}
}
For production workloads, also capture rate-limit response headers when platforms provide them and log failed requests for retries.
Production Deployment Checklist
Before going live, verify that your integration includes:
- [ ] OAuth 2.0 flows for every supported platform
- [ ] Encrypted token storage
- [ ] Automatic token refresh where supported
- [ ] Rate limiting per platform
- [ ] API error handling and retry logic
- [ ] Logging for API requests and responses
- [ ] Post approval workflows
- [ ] Content moderation
- [ ] Analytics aggregation
- [ ] Backup publishing mechanisms
- [ ] Per-platform publishing status records
- [ ] Monitoring for failed scheduled jobs
Real-World Use Cases
Social Media Dashboard
A marketing agency needs to manage 50+ client accounts across multiple platforms.
- Challenge: Switching between platform dashboards and manually publishing content.
- Implementation: Build a central dashboard with connected accounts, a unified composer, scheduled jobs, and aggregated analytics.
- Result: 60% time savings and more consistent brand presence.
Automated Content Distribution
A publisher wants each new article distributed across social channels automatically.
- Challenge: Manually sharing every article after publishing.
- Implementation: Trigger a job when an article is published, generate a shared post payload, then send it through platform-specific publishing adapters.
- Result: Instant distribution and 3x social traffic.
Conclusion
Although Hootsuite’s public API is deprecated, native platform APIs still provide the building blocks for social media management products.
The implementation priorities are:
- Implement OAuth 2.0 separately for each platform.
- Store access tokens securely and handle refresh flows.
- Use platform-specific publishing adapters behind a shared post model.
- Queue scheduled posts instead of relying on application timers.
- Respect platform-specific rate limits.
- Normalize analytics before building cross-platform reports.
- Apply role-based permissions to publishing and team-management actions.
- Use Apidog to streamline API testing and team collaboration.
FAQ
Does Hootsuite still have an API?
Hootsuite deprecated its public API in 2024. Use native platform APIs or alternative management platforms such as Buffer, Sprout Social, or Agorapulse.
How do I post to multiple platforms at once?
Implement OAuth for each platform, create a shared post payload, and call a platform-specific publishing function for every selected network.
What are the rate limits for social media APIs?
Limits vary by platform. Examples include Facebook at 200 calls per hour, X/Twitter at 300 tweets per 15 minutes, LinkedIn at 100–500 calls per day, and Instagram at 200 calls per hour.
How do I schedule posts?
Store posts with a scheduledTime in your database, then use a job queue such as Bull or Agenda to publish them at the scheduled time.
Can I get analytics from all platforms?
Yes. Each platform provides analytics APIs. Fetch platform-specific metrics, normalize the responses, and aggregate the results for cross-platform reporting.
Top comments (0)