# Instagram image post API

Publish single-image Instagram posts through an API or MCP server: JPEG, size and aspect ratio limits, caption limits, example requests, and errors with fixes.

**Early access:** publishing image posts to Instagram is built and being tested in the REST API, `@turboyap/sdk` and the `@turboyap/mcp` server. It opens to early-access users once Turboyap passes Meta App Review. Meanwhile, [test mode](https://turboyap.com/docs/test-mode) runs the same requests against a simulated Instagram account.

## What an Instagram image post is

An Instagram image post is a single photo in the feed. The Instagram API creates a media container from a public image URL, then publishes it. In Turboyap it's `type: "image"` with one media id.

Turboyap hosts the image, converts it to JPEG, fits it inside Instagram's width and aspect limits when you allow it, and publishes. A slideshow with only one slide is posted to Instagram as an image post automatically.

## Limits

What Instagram accepts for this post type. Turboyap's planner checks every one of these before a post is scheduled (the same data as [platform rules](https://turboyap.com/docs/platform-rules)), and each value links to the Instagram documentation it comes from.

| Limit | Value | Source |
|---|---|---|
| Images per post | 1 | [Meta: IG User media reference](https://developers.facebook.com/docs/instagram-platform/instagram-graph-api/reference/ig-user/media) |
| Formats | JPEG (Turboyap converts anything else to JPEG) | [Meta: IG User media reference](https://developers.facebook.com/docs/instagram-platform/instagram-graph-api/reference/ig-user/media) |
| File size | 8 MB per image | [Meta: IG User media reference](https://developers.facebook.com/docs/instagram-platform/instagram-graph-api/reference/ig-user/media) |
| Dimensions | 320–1440px wide (Turboyap resizes to fit) | [Meta: IG User media reference](https://developers.facebook.com/docs/instagram-platform/instagram-graph-api/reference/ig-user/media) |
| Aspect ratio | 4:5 to 1.91:1 | [Meta: IG User media reference](https://developers.facebook.com/docs/instagram-platform/instagram-graph-api/reference/ig-user/media) |
| Caption | 2,200 characters (sent as `caption`) | [Meta: IG User media reference](https://developers.facebook.com/docs/instagram-platform/instagram-graph-api/reference/ig-user/media) |
| Hashtags | 30 per caption | [Meta: IG User media reference](https://developers.facebook.com/docs/instagram-platform/instagram-graph-api/reference/ig-user/media) |
| @mentions | 20 per caption | [Meta: IG User media reference](https://developers.facebook.com/docs/instagram-platform/instagram-graph-api/reference/ig-user/media) |
| Rate limit | 100 API-published posts per account per 24 hours (a carousel counts as one) | [Meta: content publishing](https://developers.facebook.com/docs/instagram-platform/content-publishing) |

Compare with other platforms in the [social media API limits tracker](https://turboyap.com/tools/social-api-limits#instagram).

## Example requests

All three do the same thing: upload the media, preview the post with a dry run, then schedule it. The idempotency key makes retries safe: send the same request again with the same key and you get the original post back, not a second one.

**Early access:** `api.turboyap.com` isn't public yet, and `@turboyap/sdk` and `@turboyap/mcp` aren't on npm yet. These examples show the API as built.

### curl

```bash
# 1. Upload each image. The body is the raw file; Content-Type is its type.
curl https://api.turboyap.com/v1/media \
  -H "Authorization: Bearer $TURBOYAP_API_KEY" \
  -H "Content-Type: image/png" \
  -H "X-Filename: slide-1.png" \
  --data-binary @slide-1.png
# → { "media": { "id": "med_…", "width": 1080, "height": 1920, … }, "deduplicated": false }

# 2. Preview. Remove "dry_run" to create and schedule the post.
curl https://api.turboyap.com/v1/posts \
  -H "Authorization: Bearer $TURBOYAP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "image",
    "media_ids": ["med_…"],
    "caption": "Read the label. #ingredients #grocery",
    "targets": [{ "account_id": "acct_instagram…" }],
    "schedule_at": "2026-10-20T18:00:00-04:00",
    "fit": { "aspect": "pad" },
    "idempotency_key": "instagram-image-post-2026-10-20",
    "dry_run": true
  }'
```

### TypeScript with @turboyap/sdk

```ts
import { readFile } from "node:fs/promises";
import { TurboyapClient } from "@turboyap/sdk";

const ty = new TurboyapClient({ apiKey: process.env.TURBOYAP_API_KEY! });
const account = (await ty.listAccounts()).find((a) => a.platform === "instagram")!;

const media_ids: string[] = [];
for (const file of ["image.png"]) {
  const { media } = await ty.uploadMedia(await readFile(file), { filename: file, contentType: "image/png" });
  media_ids.push(media.id);
}

const post = {
  type: "image" as const,
  media_ids,
  caption: "Read the label. #ingredients #grocery",
  targets: [{ account_id: account.id }],
  schedule_at: "2026-10-20T18:00:00-04:00",
  fit: { aspect: "pad" as const },
  idempotency_key: "instagram-image-post-2026-10-20",
};

// Preview first: every problem comes back with a fix.
const preview = await ty.createPost({ ...post, dry_run: true });
if (!preview.ok) throw new Error(preview.errors.map((e) => `${e.code}: ${e.fix}`).join("\n"));

const { post: created } = await ty.createPost(post);
console.log(created.targets[0]?.status, created.targets[0]?.next_step);
```

### Claude Code (MCP)

Add the Turboyap MCP server once, with an agent key from your workspace:

```bash
claude mcp add turboyap -e TURBOYAP_API_KEY=ty_agent_… -- npx -y @turboyap/mcp
```

Then ask:

> Post ~/content/label-check.png to our Instagram tomorrow at 8am ET with the caption "Read the label. #ingredients #grocery". If the aspect ratio doesn't fit, pad it. Dry-run first.

The agent calls `list_accounts`, `get_platform_rules`, `upload_media`, `create_post` with `dry_run: true`, applies any fixes, calls `create_post` again to schedule, and reports each account's `next_step` from `get_post`. The same server works in Cursor; see the [MCP quickstart](https://turboyap.com/docs/mcp-quickstart).

## How publishing works

Instagram has one publishing mode. When the scheduled time arrives, Turboyap creates the media container, waits for Instagram to finish processing, then publishes. The target moves `scheduled` → `publishing` → `processing` → `published`, and `platform_url` links to the live post.

JPEG only; Turboyap converts PNG/WebP automatically.

Vertical 9:16 slides are outside Instagram's 4:5–1.91:1 range: set `fit.aspect` to `"pad"` (keeps all content) or `"crop"`.

## Common errors and fixes

Every error has a `code`, the `field` that caused it, and a `fix` an agent can apply without asking you. A request with several problems returns all of them in `issues`; a dry run returns them without creating anything.

| Code | When | Fix |
|---|---|---|
| `too_many_items` | More than one media id on an image post. | Send one media id, or set `fit.overflow` to `"truncate"` to post only the first. For several images, use `type: "slideshow"`. |
| `aspect_ratio_out_of_range` | An image is outside 4:5 to 1.91:1 and `fit.aspect` is `"strict"` (the default). | Set `fit.aspect` to `"pad"` to letterbox (keeps all text visible) or `"crop"` to center-crop. |
| `caption_too_long` | Caption over 2,200 characters. | Shorten it, or pass `targets[i].caption` with a version under 2,200 characters. |
| `too_many_hashtags` | More than 30 hashtags. | Keep at most 30 hashtags. |
| `too_many_mentions` | More than 20 @mentions. | Keep at most 20 mentions. |
| `media_format_unsupported` | A media id points to a video, or the upload isn't an image Turboyap can read. | Upload PNG, JPEG or WebP images. Video posts are coming soon. |
| `schedule_in_past` | `schedule_at` is more than a minute in the past. | Use `"now"` or a future time with a timezone offset. |
| `account_unavailable` | The Instagram account needs to be reconnected. | Reconnect the account, then retry. |
| `unsupported_platform` | A target is on a platform Turboyap doesn't support yet. | Remove that target. Supported platforms: tiktok, instagram, bluesky (beta), telegram (beta), discord (beta). |
| `insufficient_scope` | The key lacks a scope the request needs, such as `publish` for `schedule_at: "now"` (agent keys don't have it by default). | Schedule a future time instead of `"now"`, omit `schedule_at` to save a draft, or use a key with the missing scope. `GET /v1/keys/me` lists the key's scopes. |
| `account_not_allowed` | The key is limited to some accounts, and a target isn't one of them. | Remove that target, or use a key whose `account_ids` include the account. |
| `forbidden_scope` | An agent key tries to approve or reject a post. | A human approves agent posts. To skip review for an account, a human sets its autonomy level to 3. |
| `unknown_outcome` | Turboyap stopped partway through publishing (for example, a restart) and can't confirm whether the post went out. It doesn't publish it again. | Check the account on the platform. If the post isn't there, create it again. |

Errors from Instagram itself appear on the target's `error` with Instagram's own code. Turboyap retries rate limits and temporary failures with backoff, and marks the account for reconnection when Instagram rejects its token.

## Approval and audit

- **Level 1, review everything (default):** a post an agent schedules starts as `pending_approval` and waits until a human approves it with a human key (`POST /v1/posts/:id/approve`). Agent keys can't approve.
- **Level 3, full autopilot:** agent posts schedule directly. Every action is still logged. Level 2, guardrails, is coming soon.
- **Posts created by humans** schedule directly at any level.
- **Edits re-open review:** if an agent changes the caption or time of an approved post on a level-1 account, it goes back to `pending_approval`.
- **Dry runs show the outcome:** each target's `status_on_create` tells the agent whether the post will wait for approval.
- **Audit log:** every create, approval, edit, publish and failure is recorded with whether a human or an agent did it, the key id, and the MCP client name. The kill switch (`POST /v1/workspace/pause`) stops every publish to Instagram and every other platform at once.

Read more in [the autonomy dial](https://turboyap.com/docs/autonomy-dial).

## FAQ

### What image size does the Instagram API accept?

JPEG only, up to 8 MB, between 320 and 1440 pixels wide, with an aspect ratio from 4:5 (portrait) to 1.91:1 (landscape). Turboyap converts and resizes automatically; set `fit.aspect` to pad or crop images outside the ratio range.

### Can I post a PNG to Instagram through the API?

Instagram's API takes JPEG only. Upload the PNG to Turboyap and it's converted to JPEG before publishing.

### Can I post to a personal Instagram account through the API?

No. Instagram's publishing API works with professional accounts (business or creator). Switching a personal account to a professional one is free in the Instagram app.

### How long can an Instagram caption be?

2,200 characters, with at most 30 hashtags and 20 @mentions. Turboyap checks all three before scheduling.

## Related

- [Instagram carousel API](https://turboyap.com/api/instagram/carousel)
- [TikTok photo post API](https://turboyap.com/api/tiktok/photo-post)
- [Bluesky image post API](https://turboyap.com/api/bluesky/image-post) (beta)
- [Telegram photo post API](https://turboyap.com/api/telegram/photo) (beta)
- [Discord image message API](https://turboyap.com/api/discord/image-message) (beta)
- [Post one slideshow to TikTok and Instagram](https://turboyap.com/docs/slideshows)
- [REST API reference](https://turboyap.com/docs/api)
- [Platform rules](https://turboyap.com/docs/platform-rules)
- [Social media API limits tracker](https://turboyap.com/tools/social-api-limits)
