# Instagram

Publish feed posts, carousels, Stories and Reels to Instagram, read account and post insights, and answer DMs and comments.

Connect an Instagram professional account and you can publish every format Instagram's API allows: feed images, carousels, Stories and Reels. The same account gives you post and account insights, the DM inbox, comment moderation and comment-to-DM automations. The platform id is `instagram`.

## What you can do

| Capability | Supported | Where |
| --- | --- | --- |
| Publish | Feed images, carousels (up to 10 items), Stories, Reels, trial Reels | [Create a post](https://pozy.ai/docs/publishing) |
| Schedule | Yes, every format including Stories | [Scheduling](https://pozy.ai/docs/publishing/scheduling) |
| Analytics | Post metrics, account insights, follower history, demographics, online followers, Story insights | [Platform insights](https://pozy.ai/docs/analytics/platform-insights) |
| DMs | Read and reply, attachments, quick replies, buttons, product cards, reactions | [Messages](https://pozy.ai/docs/inbox/messages) |
| Comments | List, reply, hide, delete, private reply | [Comments](https://pozy.ai/docs/inbox/comments) |
| Automations | Comment, live comment, Story reply and Story mention triggers | [Comment to DM](https://pozy.ai/docs/automations/comment-to-dm) |
| Ads | Through a Meta Ads connection | [Meta Ads](https://pozy.ai/docs/ads/meta-ads) |

## Connect

Instagram needs a **Business** or **Creator** account. Personal accounts cannot publish through any third-party API, so ask the user to switch their account type in the Instagram app first (it is free and takes a minute).

Start the connection with [`GET /v1/connect/instagram`](https://pozy.ai/docs/api/getConnectUrl) and a `profileId`. There are two login methods, chosen with the `loginMethod` query parameter:

| `loginMethod` | What the user signs in to | Use it when |
| --- | --- | --- |
| `instagram_login` (default) | Their Instagram professional account directly. No Facebook Page needed. | You only need publishing, insights and the inbox. |
| `facebook_login` | A Facebook Page that has the Instagram account linked. Calls run through that Page. | You also need ads, catalog audio on Reels, the paid partnership label or liking comments. |

With `facebook_login` the user picks the Page in a second step after OAuth. Pozy can host that picker for you, or you can build it yourself with [`listInstagramPages`](https://pozy.ai/docs/api/listInstagramPages) and [`selectInstagramAccount`](https://pozy.ai/docs/api/selectInstagramAccount). See [Headless connect](https://pozy.ai/docs/connect/headless).

```bash cURL
curl "https://api.pozy.ai/v1/connect/instagram?profileId=6720c0f24f3b2e0018d4a6e0&loginMethod=facebook_login&redirect_url=https://app.example.com/connected" \
  -H "Authorization: Bearer $POZY_API_KEY"
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/connect/instagram?profileId=6720c0f24f3b2e0018d4a6e0&loginMethod=facebook_login&redirect_url=https:%2F%2Fapp.example.com%2Fconnected", {
  headers: { Authorization: `Bearer ${process.env.POZY_API_KEY}` },
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);

// Send the user's browser to data.authUrl
```
```python Python
import os, requests

resp = requests.get(
    "https://api.pozy.ai/v1/connect/instagram",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    params={
        "profileId": "6720c0f24f3b2e0018d4a6e0",
        "loginMethod": "facebook_login",
        "redirect_url": "https://app.example.com/connected",
    },
)
print(resp.json()["authUrl"])
```

> **Ask for less:**
> 
> Pass `scopes` to request only the permission areas you need (for example publishing and analytics, without ads). Fewer permissions on the consent screen means fewer users who stop halfway. See [Scopes](https://pozy.ai/docs/connect/scopes).

## Post types

Instagram decides the format from the media you send, with one field to override it. Every post needs media: there are no text-only Instagram posts.

| You send | Instagram publishes | Extra fields |
| --- | --- | --- |
| One image | Feed post | `altText`, `userTags`, `collaborators`, `locationId`, `firstComment` |
| 2 to 10 images or videos | Carousel | Same as feed. Every item takes the aspect ratio of the first one. |
| One video | Reel | `shareToFeed`, `instagramThumbnail` or `thumbOffset`, `audioName`, `audioConfiguration`, `trialParams` |
| Any single image or video with `contentType: "story"` | Story | `userTags` (coordinates optional) |

### Feed post

```bash cURL
curl -X POST https://api.pozy.ai/v1/posts \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "First light over the harbour. #morning",
    "mediaItems": [
      { "type": "image", "url": "https://cdn.example.com/harbour.jpg", "altText": "Fishing boats at dawn" }
    ],
    "platforms": [
      { "platform": "instagram", "accountId": "6720c1a94f3b2e0018d4a7f1" }
    ],
    "publishNow": true
  }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/posts", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    content: "First light over the harbour. #morning",
    mediaItems: [
      { type: "image", url: "https://cdn.example.com/harbour.jpg", altText: "Fishing boats at dawn" },
    ],
    platforms: [{ platform: "instagram", accountId: "6720c1a94f3b2e0018d4a7f1" }],
    publishNow: true,
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);

console.log(data.post.platforms[0].platformPostUrl);
```
```python Python
import os, requests

resp = requests.post(
    "https://api.pozy.ai/v1/posts",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    json={
        "content": "First light over the harbour. #morning",
        "mediaItems": [
            {"type": "image", "url": "https://cdn.example.com/harbour.jpg", "altText": "Fishing boats at dawn"}
        ],
        "platforms": [{"platform": "instagram", "accountId": "6720c1a94f3b2e0018d4a7f1"}],
        "publishNow": True,
    },
)
print(resp.json()["post"]["platforms"][0]["platformPostUrl"])
```

`altText` applies to feed images and carousel images. Instagram cuts it at 1,000 characters and ignores it on Reels, Stories and video slides.

### Carousel

Put up to 10 images and videos in `mediaItems`. Mixed media is fine. Crop everything to the same ratio before you upload, because Instagram uses the first item's ratio for all of them.

```json
"mediaItems": [
  { "type": "image", "url": "https://cdn.example.com/menu-1.jpg" },
  { "type": "image", "url": "https://cdn.example.com/menu-2.jpg" },
  { "type": "video", "url": "https://cdn.example.com/kitchen.mp4" }
]
```

### Reel

A single video is always a Reel. Set `shareToFeed: false` to keep it in the Reels tab only, and give it a cover with `instagramThumbnail` (a JPEG or PNG URL, 1080 x 1920 px works best) or a frame offset in milliseconds with `thumbOffset`.

```json
{
  "platform": "instagram",
  "accountId": "6720c1a94f3b2e0018d4a7f1",
  "platformSpecificData": {
    "shareToFeed": true,
    "instagramThumbnail": "https://cdn.example.com/reel-cover.jpg",
    "audioName": "Studio session"
  }
}
```

**Trial Reels** are shown to non-followers first. Add `trialParams.graduationStrategy`: `MANUAL` (you promote it yourself in the app) or `SS_PERFORMANCE` (Instagram shares it with followers if it performs well).

**Catalog audio.** `audioConfiguration` attaches a track from Instagram's audio library. Find an `audioId` with [Search Instagram audio](https://pozy.ai/docs/api/searchInstagramAudio) (leave out `q` to get what is trending), then send it with optional `audioVolume` and `videoVolume` from 0 to 100. This needs `facebook_login`. If the track is pulled before a scheduled Reel goes out, the post fails with a message naming the cause rather than publishing with different sound. [Get Instagram audio metadata](https://pozy.ai/docs/api/getInstagramAudio) re-checks a stored id before publish time.

`muteAudio: true` removes the video's own sound track before upload, so the result is permanently silent. Videos over 200 MB cannot be muted.

### Story

Add `contentType: "story"`. Stories show no caption, last 24 hours and cannot carry link stickers through the API. A Story publishes one media item, so give a Story entry its own `customMedia` when the post has several.

To publish a feed post and a Story of the same campaign at once, list the account twice:

```json
"platforms": [
  { "platform": "instagram", "accountId": "6720c1a94f3b2e0018d4a7f1" },
  {
    "platform": "instagram",
    "accountId": "6720c1a94f3b2e0018d4a7f1",
    "customMedia": [{ "type": "image", "url": "https://cdn.example.com/story-1080x1920.jpg" }],
    "platformSpecificData": { "contentType": "story" }
  }
]
```

Each entry publishes and reports its status separately.

### Platform fields

All of these go in `platformSpecificData` on the Instagram entry (schema: `InstagramPlatformData`).

| Field | Type | Applies to | Notes |
| --- | --- | --- | --- |
| `contentType` | `"story"` | Story | Leave out for feed posts and Reels. |
| `shareToFeed` | boolean, default `true` | Reels | `false` keeps the Reel off the main grid. |
| `collaborators` | array of usernames | Feed, carousel, Reel | Up to 3 public Business or Creator accounts. They get an invite to co-author. |
| `userTags` | array of `{ username, x, y, mediaIndex }` | All | `x` and `y` run from 0 to 1 and are required on images. `mediaIndex` picks the carousel slide. |
| `firstComment` | string | Feed, carousel, Reel | Posted right after publishing. Captions cannot hold clickable links, so this is where links usually go. |
| `locationId` | string | Feed, carousel, Reel | The numeric id of a Facebook Page that has an address. Not a place name. Stories reject it. |
| `trialParams` | `{ graduationStrategy }` | Reels | `MANUAL` or `SS_PERFORMANCE`. |
| `instagramThumbnail` / `reelCover` | URL | Reels | Custom cover. Wins over `thumbOffset`. |
| `thumbOffset` | integer (ms) | Reels | Frame to use as the cover. |
| `audioName` | string | Reels | Renames the original audio. Set at creation only. |
| `audioConfiguration` | `{ audioId, audioVolume, videoVolume }` | Reels | Catalog track. Needs `facebook_login`. |
| `muteAudio` | boolean | Reels, Stories, video slides | Strips the sound before upload. |
| `isAiGenerated` | boolean | All | Adds Instagram's AI label for generated images or video. |
| `isPaidPartnership` | boolean | Feed, carousel, Reel | Shows the "Paid partnership" label. Needs `facebook_login`. |
| `brandedContentSponsors` | array, up to 2 | Feed, carousel, Reel | Usernames or numeric ids of the brands. Turns the label on by itself. The brand is named once they approve the request in Instagram. |
| `commentsEnabled` | boolean, default `true` | Feed, carousel, Reel | `false` turns comments off right after publishing. |

If a collaborator cannot be invited, the post still publishes and the name comes back in `droppedCollaborators` with a reason.

## Media requirements

Pozy compresses images over 8 MB and keeps your original. Media URLs must return the file itself: sharing links from Google Drive, Dropbox, OneDrive or iCloud return a web page and fail. Upload files with [Media uploads](https://pozy.ai/docs/publishing/media) when you do not have a direct URL.

**Images**

| | Feed and carousel | Story |
| --- | --- | --- |
| Formats | JPEG, PNG | JPEG, PNG |
| Max size | 8 MB each | 8 MB |
| Aspect ratio | 4:5 (1080 x 1350) to 1.91:1 (1080 x 566); 1:1 and 9:16 also accepted | 9:16 (1080 x 1920) |
| Count | 1, or up to 10 in a carousel | 1 |

**Videos**

| | Reel | Story |
| --- | --- | --- |
| Formats | MP4, MOV (H.264) | MP4, MOV (H.264) |
| Max size | 300 MB | 100 MB |
| Duration | 3 seconds to 15 minutes | 3 to 60 seconds |
| Aspect ratio | 9:16, 1080 x 1920 | 9:16, 1080 x 1920 |
| Frame rate | 30 fps recommended | 30 fps recommended |

## Limits

| Limit | Value |
| --- | --- |
| Caption | 2,200 characters. About 125 show before "more". |
| Posts per account | 100 per rolling 24 hours, all formats combined |
| Collaborators | 3 per post |
| Branded content sponsors | 2 per post |
| Alt text | 1,000 characters |

Read how much of the 24-hour allowance is left with [Get Instagram publishing limit](https://pozy.ai/docs/api/getInstagramPublishingLimit). Compare `quotaUsage` with the returned `quotaTotal` instead of hardcoding 100, since Instagram can change the cap.

Instagram's API does not support Story stickers (polls, links, countdowns), product tags, filters, Guides or going live. Published Instagram posts cannot be edited or deleted through the API; change them in the Instagram app.

## Analytics

[`GET /v1/analytics?platform=instagram`](https://pozy.ai/docs/api/getAnalytics) returns per-post `impressions`, `reach`, `likes`, `comments`, `shares`, `saves` and `views`, plus Reel watch-time fields (`igReelsAvgWatchTime`, `igReelsVideoViewTotalTime`) where Instagram reports them.

Account-level endpoints go further:

| Endpoint | What you get |
| --- | --- |
| [Instagram insights](https://pozy.ai/docs/api/getInstagramAccountInsights) | Reach, views, accounts engaged, interactions, follows and unfollows, profile views, link taps. Only `reach` comes as a time series. |
| [Follower history](https://pozy.ai/docs/api/getInstagramFollowerHistory) | Daily follower count with `followers_gained` and `followers_lost`, built from daily snapshots. |
| [Demographics](https://pozy.ai/docs/api/getInstagramDemographics) | Audience by age, gender, city or country. Needs at least 100 followers. |
| [Online followers](https://pozy.ai/docs/api/getInstagramOnlineFollowers) | When followers are online, by hour. Useful with [Best time to post](https://pozy.ai/docs/analytics/best-time). |
| [Active Stories](https://pozy.ai/docs/api/listInstagramStories) and [Story insights](https://pozy.ai/docs/api/getInstagramStoryInsights) | Live Stories with views, reach, replies, taps forward and back, exits. |

Story numbers are only readable while the Story is live. After 24 hours you get final numbers only if Instagram sent them before expiry, which happens far more reliably on accounts connected with `facebook_login`. If you need final Story metrics, read them before the Story expires. Counts under 5 can come back as 0 because of Instagram's privacy floor.

Image and cover URLs in analytics responses are served from Pozy storage and do not expire, so you can cache them.

## Inbox

**Direct messages.** List conversations, read messages and reply with [Send message](https://pozy.ai/docs/api/sendInboxMessage). You can send text, images (PNG, JPEG up to 8 MB), video and audio (up to 25 MB), PDFs, up to 13 quick replies, up to 3 buttons (the text must then be 640 characters or less) and product cards of up to 10 elements. Reactions work both ways.

> **The 24-hour window:**
> 
> You can message someone freely for 24 hours after their last message. After that, a human-typed reply can still go out for up to 7 days with `messagingType: "MESSAGE_TAG"` and `messageTag: "HUMAN_AGENT"`. Instagram does not allow that tag on automated messages, so workflows and broadcasts refuse it.

Instagram does not expose the Message Requests folder separately, so `GET /v1/inbox/conversations?folder=requests` returns `400` with code `PLATFORM_LIMITATION`. Requests still reach you as `message.received` once the person writes, and [Accept a message request](https://pozy.ai/docs/api/acceptConversationRequest) works on them.

When another app owns a thread through Instagram's handover protocol, inbound messages arrive with `metadata.standby: true`, automations stay quiet and sends return `409` `not_thread_owner`. Move the thread with [thread control](https://pozy.ai/docs/api/setConversationThreadControl).

Conversations and senders can carry an `instagramProfile` object (`isFollower`, `isFollowing`, `followerCount`, `isVerified`). Instagram only reveals the follow relationship for people who have messaged the account, so treat a missing value as unknown, not as "not a follower".

When you connect an account, Pozy imports its existing DM history (up to 500 conversations, the newest 500 messages in each). The import runs in the background, arrives already read and fires no webhooks.

**Comments.** List comments, post a top-level comment, reply, hide, unhide and delete. [Send private reply](https://pozy.ai/docs/api/sendPrivateReplyToComment) opens a DM from a comment, once per comment, within 7 days. Liking a comment is in limited release on Instagram's side and only works on `facebook_login` accounts; other accounts get `403` `PLATFORM_BETA_RESTRICTED` or `400` `instagram_likes_require_facebook_login`.

**Conversation starters.** Set up to 4 ice breakers (80 characters each) and a persistent menu (3 top-level items, 5 nested). A menu postback with payload `pozy:workflow:<workflowId>` starts that workflow. See [Conversation starters](https://pozy.ai/docs/automations/conversation-starters).

## Errors you may see

| Message | Cause | Fix |
| --- | --- | --- |
| Cannot process video from this URL | A cloud-drive sharing link instead of a direct file URL | Use a direct URL or upload through [Media](https://pozy.ai/docs/publishing/media). |
| Maximum of 100 posts per day reached | The rolling 24-hour cap | Spread posts out. Check [the publishing limit](https://pozy.ai/docs/api/getInstagramPublishingLimit). |
| Instagram blocked your request | Instagram's automation detection | Post less often, vary the content, wait before retrying. |
| Duplicate content detected | Same caption and media posted recently | Change the caption or media. |
| `instagram_audio_requires_facebook_login` | `audioConfiguration` on an Instagram Login account | Reconnect with `loginMethod=facebook_login`. |
| `instagram_paid_partnership_requires_facebook_login` | Paid partnership label on an Instagram Login account | Reconnect with `loginMethod=facebook_login`. |
| Instagram rejected locationId | The Page id has no address or does not exist | Use the id of a Page with location data. |
| Access token expired | The user's session ended or they removed access | Reconnect the account. Listen for `account.disconnected`. |

When a post sent with `publishNow: true` is rejected, the response is `207` with `post.status: "failed"` and the reason in `platforms[].errorMessage`. A `207` is still a 2xx, so check `post.status` rather than only the HTTP status.

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`GET /v1/accounts/{accountId}/instagram/stories`](https://pozy.ai/docs/api/listInstagramStories) | List active Instagram stories |
| [`GET /v1/accounts/{accountId}/instagram/publishing-limit`](https://pozy.ai/docs/api/getInstagramPublishingLimit) | Get Instagram publishing limit |
| [`GET /v1/accounts/{accountId}/instagram/audio`](https://pozy.ai/docs/api/searchInstagramAudio) | Search Instagram audio |
| [`GET /v1/accounts/{accountId}/instagram/audio/{audioId}`](https://pozy.ai/docs/api/getInstagramAudio) | Get Instagram audio metadata |
| [`GET /v1/accounts/{accountId}/instagram/business-discovery`](https://pozy.ai/docs/api/getInstagramBusinessDiscovery) | Look up a public Instagram Business account |
| [`GET /v1/accounts/{accountId}/instagram/media/{mediaId}/collaborators`](https://pozy.ai/docs/api/listInstagramMediaCollaborators) | List the collaborators of an Instagram post |
| [`GET /v1/accounts/{accountId}/instagram/live-media`](https://pozy.ai/docs/api/listInstagramLiveMedia) | List the live broadcasts of an Instagram account |
| [`GET /v1/accounts/{accountId}/instagram/tags`](https://pozy.ai/docs/api/listInstagramTaggedMedia) | List the posts an Instagram account is tagged in |
| [`GET /v1/accounts/{accountId}/instagram/mentions/media/{mediaId}`](https://pozy.ai/docs/api/getInstagramMentionedMedia) | Get a post that mentions an Instagram account in its caption |
| [`GET /v1/accounts/{accountId}/instagram/mentions/comments/{commentId}`](https://pozy.ai/docs/api/getInstagramMentionedComment) | Get a comment that mentions an Instagram account |
| [`GET /v1/accounts/{accountId}/instagram/stories/{storyId}/insights`](https://pozy.ai/docs/api/getInstagramStoryInsights) | Get Instagram story insights |
| [`GET /v1/analytics/instagram/account-insights`](https://pozy.ai/docs/api/getInstagramAccountInsights) | Get Instagram insights |
| [`GET /v1/analytics/instagram/follower-history`](https://pozy.ai/docs/api/getInstagramFollowerHistory) | Get Instagram follower history |
| [`GET /v1/analytics/instagram/demographics`](https://pozy.ai/docs/api/getInstagramDemographics) | Get Instagram demographics |
| [`GET /v1/analytics/instagram/online-followers`](https://pozy.ai/docs/api/getInstagramOnlineFollowers) | Get Instagram online followers |
| [`GET /v1/connect/instagram/select-account`](https://pozy.ai/docs/api/listInstagramPages) | List Pages with a linked Instagram account |
| [`POST /v1/connect/instagram/select-account`](https://pozy.ai/docs/api/selectInstagramAccount) | Select the Page whose Instagram account to connect |
| [`GET /v1/accounts/{accountId}/instagram-ice-breakers`](https://pozy.ai/docs/api/getInstagramIceBreakers) | Get IG ice breakers |
| [`PUT /v1/accounts/{accountId}/instagram-ice-breakers`](https://pozy.ai/docs/api/setInstagramIceBreakers) | Set IG ice breakers |
| [`DELETE /v1/accounts/{accountId}/instagram-ice-breakers`](https://pozy.ai/docs/api/deleteInstagramIceBreakers) | Delete IG ice breakers |

## Webhook events

| Event | When it fires |
| --- | --- |
| [`post.published`](https://pozy.ai/docs/webhooks/events#post-published) | Post published. |
| [`post.failed`](https://pozy.ai/docs/webhooks/events#post-failed) | Post failed. |
| [`post.platform.published`](https://pozy.ai/docs/webhooks/events#post-platform-published) | Post platform published. |
| [`post.platform.failed`](https://pozy.ai/docs/webhooks/events#post-platform-failed) | Post platform failed. |
| [`post.platform.deleted`](https://pozy.ai/docs/webhooks/events#post-platform-deleted) | Post platform deleted. |
| [`account.disconnected`](https://pozy.ai/docs/webhooks/events#account-disconnected) | Account disconnected. |
| [`message.received`](https://pozy.ai/docs/webhooks/events#message-received) | Message received. |
| [`conversation.control_changed`](https://pozy.ai/docs/webhooks/events#conversation-control-changed) | Conversation control changed. |
| [`conversation.started`](https://pozy.ai/docs/webhooks/events#conversation-started) | Conversation started. |
| [`message.sent`](https://pozy.ai/docs/webhooks/events#message-sent) | Message sent. |
| [`message.edited`](https://pozy.ai/docs/webhooks/events#message-edited) | Message edited. |
| [`message.deleted`](https://pozy.ai/docs/webhooks/events#message-deleted) | Message deleted. |
| [`message.delivered`](https://pozy.ai/docs/webhooks/events#message-delivered) | Message delivered. |
| [`message.read`](https://pozy.ai/docs/webhooks/events#message-read) | Message read. |
| [`message.played`](https://pozy.ai/docs/webhooks/events#message-played) | Message played. |
| [`message.failed`](https://pozy.ai/docs/webhooks/events#message-failed) | Message delivery failed. |
| [`reaction.received`](https://pozy.ai/docs/webhooks/events#reaction-received) | Reaction received. |
| [`comment.received`](https://pozy.ai/docs/webhooks/events#comment-received) | Comment received. |
