# Instagram

Publish feed posts, carousels, Reels and Stories to Instagram professional accounts.


## Overview

The `instagram` platform publishes to Instagram **professional** (Business or Creator) accounts. Personal accounts cannot publish through the API. You can post:

- A single image (JPEG) to the feed
- A single video, published as a Reel
- A carousel of 2–10 images and/or videos
- A Reel (with optional cover, thumbnail frame, trial-reel mode, and "share to feed" toggle)
- A Story (exactly one image or video)
- Up to 3 collaborators on feed posts, carousels and Reels

Instagram does not support text-only posts: every post needs at least one image or video.

## Connect the account

In **[Integrations](https://social-publish-kappa.vercel.app/dashboard/integrations)**, choose **Instagram**. There are two ways to connect, and both end up as the same Instagram account in Relaycast (reconnecting through either one updates the existing account):

| Connect option | When to use it | Requirements |
| --- | --- | --- |
| **Log in with Instagram** (default) | Most accounts. No Facebook Page needed. | Instagram Business or Creator account |
| **Log in with Facebook** | The Instagram account is linked to a Facebook Page you manage | Instagram professional account linked to a Facebook Page |

Both options use the redirect URI `https://social-publish-kappa.vercel.app/api/integrations/instagram/callback` (override with `INSTAGRAM_REDIRECT_URI`).

### Instagram Login setup (self-hosted)

1. In your Meta app, add the **Instagram** product and open **API setup with Instagram login**.
2. Use the **Instagram** app ID and secret shown there (not the Facebook app ID) for `INSTAGRAM_APP_ID` and `INSTAGRAM_APP_SECRET`.
3. Under **Set up Instagram business login → Business login settings**, add the redirect URI above.

Scopes requested: `instagram_business_basic`, `instagram_business_content_publish`, `instagram_business_manage_messages`, `instagram_business_manage_comments`, `instagram_business_manage_insights`. Tokens are long-lived (60 days) and refreshed automatically before they expire.

### Facebook Login setup (self-hosted)

1. Use the Facebook app (`FACEBOOK_APP_ID`, `FACEBOOK_APP_SECRET`, the same one as [Facebook Pages](/docs/platforms/facebook)) with the Instagram use case **Manage messages and content on Instagram**.
2. Add `https://social-publish-kappa.vercel.app/api/integrations/instagram/callback` to **Facebook Login → Settings → Valid OAuth Redirect URIs**.

Always requested: `instagram_basic`, `instagram_content_publish`, `pages_show_list`, `pages_read_engagement`. Optional features request more permissions only when their flag is set, because Meta rejects the whole login if a requested permission isn't added to the app:

| Flag | Adds | Enables |
| --- | --- | --- |
| `INSTAGRAM_FB_ENABLE_COMMENTS=true` | `instagram_manage_comments` | Comment replies and comment-to-DM automations |
| `INSTAGRAM_FB_ENABLE_MESSAGES=true` | `instagram_manage_messages`, `pages_manage_metadata` | DM inbox |
| `INSTAGRAM_FB_ENABLE_INSIGHTS=true` | `instagram_manage_insights` | Post insights |
| `FACEBOOK_ENABLE_BUSINESS_MANAGEMENT=true` | `business_management` | Pages owned through a Business portfolio |

> [!NOTE]
> In development mode, only accounts with a role on the Meta app (and accepted Instagram testers) can connect. Publishing for other people needs Meta App Review for the permissions above.

## Post types & media rules

Choose the post type with `options.publishAs`:

| `publishAs` | Media | Result |
| --- | --- | --- |
| `feed` (default) | 1 image | Feed photo |
| `feed` | 1 video | Reel, also shown in the feed |
| `feed` | 2–10 images and/or videos | Carousel |
| `reel` | Exactly 1 video | Reel (`shareToFeed` controls whether it also appears in the feed grid) |
| `story` | Exactly 1 image or video | Story |

| Rule | Limit |
| --- | --- |
| Caption | 2,200 characters |
| Hashtags | 30 per caption |
| @mentions | 20 per caption |
| Image format | JPEG only (URLs ending in `.png`, `.gif`, `.webp`, `.heic`, `.avif`, `.bmp`, `.tiff` are rejected) |
| Alt text | Single-image feed posts only, up to 1,000 characters |
| Collaborators | Up to 3; not on Stories |
| Publishing quota | Instagram's rolling 24-hour publishing limit (100 posts by default). When it is used up, the target is retried later. |

Media is fetched by Instagram from its URL, so it must be publicly reachable (media library files are served as signed URLs automatically). Size, duration and aspect-ratio checks are done by Instagram while it processes the media.

## Options

| Option | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `publishAs` | `feed` \\| `reel` \\| `story` | No | `"feed"` | Post type. Feed: 1 image, 1 video (published as a Reel) or a carousel of 2-10 items. Story: exactly 1 image or video. |
| `shareToFeed` | boolean | No | `true` | Also share reel to feed Only when `publishAs` is `reel`. |
| `collaborators` | string[] | No |  | Collaborators. Invite accounts to co-author this post. They must accept the invitation in Instagram before it appears on their profile. Only when `publishAs` is `feed` or `reel`. |
| `coverUrl` | string (URL) | No |  | Reel cover image URL. Videos only. Public JPEG used as the cover in the Reels tab. Only when `publishAs` is `feed` or `reel`. |
| `thumbOffset` | number | No |  | Thumbnail frame (ms). Videos only. Millisecond offset of the frame used as the thumbnail. Ignored when a cover URL is set. Only when `publishAs` is `feed` or `reel`. |
| `trialReel` | `off` \\| `MANUAL` \\| `SS_PERFORMANCE` | No | `"off"` | Trial reel. Trial reels are shown to non-followers first. Only when `publishAs` is `reel`. |
`coverUrl` and `thumbOffset` apply to videos only. `trialReel` requires `publishAs: "reel"`.

## Examples

### Reel with collaborators and a cover

```bash tab="cURL"
curl -X POST https://social-publish-api.onrender.com/v1/posts \
  -H "x-access-key: $RELAYCAST_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "content": "Behind the scenes of our spring shoot #bts",
    "media": [{ "url": "https://cdn.example.com/bts.mp4", "type": "video" }],
    "targets": [{
      "accountId": "1d7e3f52-8c4a-4b9e-a0f1-7e6d5c4b3a29",
      "options": {
        "publishAs": "reel",
        "shareToFeed": true,
        "collaborators": ["studio.north"],
        "coverUrl": "https://cdn.example.com/bts-cover.jpg"
      }
    }],
    "scheduledFor": "2026-11-03T17:30:00Z"
  }'
```

```ts tab="Node.js"
const res = await fetch("https://social-publish-api.onrender.com/v1/posts", {
  method: "POST",
  headers: {
    "x-access-key": process.env.RELAYCAST_API_KEY!,
    "content-type": "application/json",
  },
  body: JSON.stringify({
    content: "Behind the scenes of our spring shoot #bts",
    media: [{ url: "https://cdn.example.com/bts.mp4", type: "video" }],
    targets: [
      {
        accountId: "1d7e3f52-8c4a-4b9e-a0f1-7e6d5c4b3a29",
        options: {
          publishAs: "reel",
          shareToFeed: true,
          collaborators: ["studio.north"],
          coverUrl: "https://cdn.example.com/bts-cover.jpg",
        },
      },
    ],
    scheduledFor: "2026-11-03T17:30:00Z",
  }),
});
const { data } = await res.json();
```

```python tab="Python"
import os, requests

res = requests.post(
    "https://social-publish-api.onrender.com/v1/posts",
    headers={"x-access-key": os.environ["RELAYCAST_API_KEY"]},
    json={
        "content": "Behind the scenes of our spring shoot #bts",
        "media": [{"url": "https://cdn.example.com/bts.mp4", "type": "video"}],
        "targets": [{
            "accountId": "1d7e3f52-8c4a-4b9e-a0f1-7e6d5c4b3a29",
            "options": {
                "publishAs": "reel",
                "shareToFeed": True,
                "collaborators": ["studio.north"],
                "coverUrl": "https://cdn.example.com/bts-cover.jpg",
            },
        }],
        "scheduledFor": "2026-11-03T17:30:00Z",
    },
)
data = res.json()["data"]
```

### Carousel

```json title="Request body"
{
  "content": "Three looks from the new collection",
  "media": [
    { "url": "https://cdn.example.com/look-1.jpg", "type": "image" },
    { "url": "https://cdn.example.com/look-2.jpg", "type": "image" },
    { "url": "https://cdn.example.com/look-3.mp4", "type": "video" }
  ],
  "targets": [{ "accountId": "1d7e3f52-8c4a-4b9e-a0f1-7e6d5c4b3a29" }],
  "publishNow": true
}
```

### Story

```json title="Request body"
{
  "content": "Story",
  "media": [{ "url": "https://cdn.example.com/story.jpg", "type": "image" }],
  "targets": [{ "accountId": "1d7e3f52-8c4a-4b9e-a0f1-7e6d5c4b3a29", "options": { "publishAs": "story" } }],
  "publishNow": true
}
```

The request still needs `content` (it is required for every post), but Stories have no caption.

## Analytics & comments

- **Analytics:** likes and comment counts for every post, plus Instagram insights (views, reach, shares, saves and more, depending on the media type). With Facebook Login, insights need `INSTAGRAM_FB_ENABLE_INSIGHTS=true`.
- **Inbox:** Instagram DMs can be read and answered from the dashboard inbox.
- **Comments:** comment-to-DM automations and public comment replies. See [Inbox & Comment Automation](/docs/inbox).

Accounts connected before a permission was added must reconnect to use the feature.

## Limitations & common errors

| Error | Cause |
| --- | --- |
| `Instagram posts need at least one image or video` | Text-only posts are not supported. |
| `A reel needs exactly one video` | `publishAs: "reel"` with images or several items. |
| `A story needs exactly one image or video` | Stories take a single item. |
| `A carousel allows 2-10 images and/or videos` | Too many items for a carousel. |
| `Instagram only accepts JPEG images` | Convert PNG/WebP/HEIC images to JPEG. |
| `Collaborators are not supported on stories` | Remove `collaborators` for Stories. |
| `Trial reels require post type Reel` | Set `publishAs: "reel"` to use `trialReel`. |
| Connect fails: only professional accounts | Switch the account to Business or Creator in the Instagram app. |
| Connect fails: no linked Instagram account (Facebook Login) | Link the Instagram account to a Facebook Page and reconnect. |
| Target retried: publishing limit reached | The account hit Instagram's 24-hour publishing quota. |
| Target fails with a media error | Instagram could not fetch or process the media (format, size, duration or aspect ratio). |
