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:
- Social Post Writer outputs a caption + image prompt/reference
- Image is stored on S3 with a public URL (or fetched from the content brief’s supplied image)
- 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
| Limit | Value |
|---|---|
| Posts per 24 hours per IG account | 50 |
| Carousel images | 2–10 |
| Caption length | 2,200 characters |
| Hashtag limit | 30 per post |
| Media container processing time | Usually < 30 seconds |
Test Cases
Unit tests (packages/tools/src/instagram.test.ts)
| Test | Approach |
|---|---|
publishFeedPost() creates container then publishes | Mock two axios.post calls; assert order |
waitForContainer() retries until FINISHED | Mock first two calls return IN_PROGRESS, third returns FINISHED |
waitForContainer() throws on ERROR status | Mock { status_code: 'ERROR' }; assert throws |
publishCarousel() creates child containers first | Assert one axios.post per image + carousel container + publish |
getInsights() passes correct metric param | Assert comma-joined metric string |
verify() throws when IG user ID invalid | Mock 400 response; assert throws |
Related
- Facebook Provider — same OAuth app, organic Page posting
- Meta Ads Provider — paid Instagram/Facebook campaigns
- AWS S3 Provider — image hosting for Instagram posts
- Social Post Writer Agent — generates caption copy