turboyap

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 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), and each value links to the Instagram documentation it comes from.

LimitValueSource
Images per post1Meta: IG User media reference
FormatsJPEG (Turboyap converts anything else to JPEG)Meta: IG User media reference
File size8 MB per imageMeta: IG User media reference
Dimensions320–1440px wide (Turboyap resizes to fit)Meta: IG User media reference
Aspect ratio4:5 to 1.91:1Meta: IG User media reference
Caption2,200 characters (sent as caption)Meta: IG User media reference
Hashtags30 per captionMeta: IG User media reference
@mentions20 per captionMeta: IG User media reference
Rate limit100 API-published posts per account per 24 hours (a carousel counts as one)Meta: content publishing

Compare with other platforms in the social media API limits tracker.

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

# 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

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:

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.

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.

CodeWhenFix
too_many_itemsMore 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_rangeAn 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_longCaption over 2,200 characters.Shorten it, or pass targets[i].caption with a version under 2,200 characters.
too_many_hashtagsMore than 30 hashtags.Keep at most 30 hashtags.
too_many_mentionsMore than 20 @mentions.Keep at most 20 mentions.
media_format_unsupportedA 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_pastschedule_at is more than a minute in the past.Use "now" or a future time with a timezone offset.
account_unavailableThe Instagram account needs to be reconnected.Reconnect the account, then retry.
unsupported_platformA target is on a platform Turboyap doesn't support yet.Remove that target. Supported platforms: tiktok, instagram, bluesky (beta), telegram (beta), discord (beta).
insufficient_scopeThe 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_allowedThe 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_scopeAn 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_outcomeTurboyap 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

Read more in the 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.