Skip to Content
ProvidersInstagram (Business API)

Instagram (Business API)

Category: Social Publishing
Integration type: Tenant OAuth via Facebook Login (stored in integrations table)
External API: Meta Graph API v19.0 — Instagram Graph API


Purpose

Instagram integration enables organic content publishing to a tenant’s Instagram Business or Creator account. The Social Post Writer agent generates Instagram captions and hashtags; the Social Publisher worker pushes feed posts, carousels, and Reels covers to Instagram via the Graph API.

The platform also pulls Instagram Insights (reach, impressions, saves, profile visits) for reporting.

Prerequisite: The Instagram account must be a Business or Creator account connected to a Facebook Page. Personal Instagram accounts do not have API access.


Config Structure

OAuth flow

Instagram Graph API access is granted through the same Facebook OAuth app as the Facebook Pages integration. A separate instagram integration record is created:

scope: instagram_basic,instagram_content_publish,instagram_manage_insights,pages_show_list Stored in integrations: provider: 'instagram' api_key: encrypt(page_access_token) // Page token associated with the connected FB Page metadata: { igUserId: '17841400000000000', // Instagram Business Account User ID (not the @handle) igUsername: 'acmeplumbing', facebookPageId: '111222333', accessToken: string, tokenExpiresAt: string, }

Getting the Instagram User ID

The Instagram User ID is retrieved from the Facebook Page connection:

GET /v19.0/{facebookPageId}?fields=instagram_business_account&access_token={token} → { instagram_business_account: { id: '17841400000000000' } }

Integration Pattern

Publishing a feed post (image required)

Instagram requires at least one image for feed posts. Text-only posts are not supported on Instagram.

class InstagramTool { constructor( private igUserId: string, private accessToken: string, private baseUrl: string = 'https://graph.facebook.com/v19.0', ) {} async publishFeedPost(options: { caption: string; // Post caption including hashtags imageUrl: string; // Public URL of image (must be accessible by Facebook crawlers) published?: boolean; // false = keep as draft (default: false) }): Promise<{ postId: string; permalink: string }> { // Step 1: Create media container const containerResponse = await axios.post( `${this.baseUrl}/${this.igUserId}/media`, { image_url: options.imageUrl, caption: options.caption, access_token: this.accessToken, }, ); const containerId = containerResponse.data.id; // Step 2: Wait for container to finish processing await this.waitForContainer(containerId); // Step 3: Publish the container if (options.published === false) { return { postId: containerId, permalink: '' }; // Drafts not supported natively — store containerId } const publishResponse = await axios.post( `${this.baseUrl}/${this.igUserId}/media_publish`, { creation_id: containerId, access_token: this.accessToken, }, ); const postId = publishResponse.data.id; // Fetch permalink const detailResponse = await axios.get(`${this.baseUrl}/${postId}`, { params: { fields: 'permalink', access_token: this.accessToken }, }); return { postId, permalink: detailResponse.data.permalink, }; } async publishCarousel(options: { caption: string; imageUrls: string[]; // 2–10 images }): Promise<{ postId: string; permalink: string }> { // Create individual media containers for each image const childContainerIds = await Promise.all( options.imageUrls.map(url => axios.post(`${this.baseUrl}/${this.igUserId}/media`, { image_url: url, is_carousel_item: true, access_token: this.accessToken, }).then(r => r.data.id), ), ); // Create carousel container const carouselResponse = await axios.post( `${this.baseUrl}/${this.igUserId}/media`, { media_type: 'CAROUSEL', caption: options.caption, children: childContainerIds.join(','), access_token: this.accessToken, }, ); const containerId = carouselResponse.data.id; await this.waitForContainer(containerId); // Publish const publishResponse = await axios.post( `${this.baseUrl}/${this.igUserId}/media_publish`, { creation_id: containerId, access_token: this.accessToken }, ); const postId = publishResponse.data.id; const detailResponse = await axios.get(`${this.baseUrl}/${postId}`, { params: { fields: 'permalink', access_token: this.accessToken }, }); return { postId, permalink: detailResponse.data.permalink }; } private async waitForContainer( containerId: string, maxAttempts = 10, delayMs = 3000, ): Promise<void> { for (let i = 0; i < maxAttempts; i++) { const status = await axios.get(`${this.baseUrl}/${containerId}`, { params: { fields: 'status_code', access_token: this.accessToken, }, }); if (status.data.status_code === 'FINISHED') return; if (status.data.status_code === 'ERROR') throw new Error(`Instagram media container failed: ${status.data.status}`); await new Promise(r => setTimeout(r, delayMs)); } throw new Error('Instagram media container timed out'); } async getInsights(options: { metrics: string[]; // e.g. ['impressions', 'reach', 'profile_views'] startDate: string; endDate: string; period: 'day' | 'week' | 'month'; }): Promise<InsightRow[]> { const response = await axios.get( `${this.baseUrl}/${this.igUserId}/insights`, { params: { metric: options.metrics.join(','), since: options.startDate, until: options.endDate, period: options.period, access_token: this.accessToken, }, }, ); return response.data.data; } async verify(): Promise<void> { const response = await axios.get(`${this.baseUrl}/${this.igUserId}`, { params: { fields: 'id,username', access_token: this.accessToken }, }); if (!response.data.id) throw new Error('Instagram Business Account not found'); } }

Image Hosting Requirement

Instagram requires that image URLs be publicly accessible (Meta’s servers crawl the URL). The flow is:

  1. Social Post Writer outputs a caption + image prompt/reference
  2. Image is stored on S3 with a public URL (or fetched from the content brief’s supplied image)
  3. Instagram tool passes the public S3/CDN URL as image_url

Images hosted on localhost or behind auth will fail with OAuthException: (#2207026).


Posting Limits

LimitValue
Posts per 24 hours per IG account50
Carousel images2–10
Caption length2,200 characters
Hashtag limit30 per post
Media container processing timeUsually < 30 seconds

Test Cases

Unit tests (packages/tools/src/instagram.test.ts)

TestApproach
publishFeedPost() creates container then publishesMock two axios.post calls; assert order
waitForContainer() retries until FINISHEDMock first two calls return IN_PROGRESS, third returns FINISHED
waitForContainer() throws on ERROR statusMock { status_code: 'ERROR' }; assert throws
publishCarousel() creates child containers firstAssert one axios.post per image + carousel container + publish
getInsights() passes correct metric paramAssert comma-joined metric string
verify() throws when IG user ID invalidMock 400 response; assert throws

© 2026 Leadmetrics — Internal use only