Content & Marketing

Schedule Instagram Posts Using Python and AI

This guide shows you how to write an Instagram caption with AI and then schedule the post through Instagram's official API in about 20 minutes, with no third-party posting service in the middle. You stay in control of the caption, the timing, and the image, and everything runs from a single Python script.

The approach has two halves. First, an LLM (large language model, the kind of AI that writes text) drafts a caption and hashtags. Second, the Instagram Graph API (Meta's official programming interface for Instagram Business accounts) creates the post and sets it to publish at a future time. This fits naturally into a wider Automated Social Media Posting routine and the broader AI Content Creation & Marketing Automation workflow.

Prerequisites

This guide assumes you already have Python 3.10 or newer and a working virtual environment. If you do not, follow Create a Python Virtual Environment for AI first. Beyond that, you need three Instagram-specific things:

  1. An Instagram Business or Creator account linked to a Facebook Page. Personal accounts cannot publish through the API. You can convert your account for free in the Instagram app under Settings.
  2. A Meta Developer App with the instagram_basic, instagram_content_publish, pages_read_engagement, and pages_manage_posts permissions. You create this at developers.facebook.com.
  3. A long-lived access token generated through the Meta Graph API Explorer. Short-lived tokens expire in about an hour; the long-lived version lasts roughly 60 days.

Install the libraries used here:

pip install openai httpx python-dotenv

We use httpx (a modern HTTP client that handles both regular and async requests) for all calls to Meta, and the openai SDK for the caption. Store every secret in a file named .env in your project folder:

IG_USER_ID=your_ig_business_account_id
IG_ACCESS_TOKEN=your_long_lived_token
OPENAI_API_KEY=sk-...

Then immediately add that file to your ignore list so it never reaches a public repository:

echo ".env" >> .gitignore

A leaked access token lets anyone post as you, so this one line matters more than it looks.

Step 1: Generate the caption with an LLM

Ask the model for structured output so you can read the caption and hashtags separately instead of parsing one blob of text. We request JSON and set response_format so the model is forced to return valid JSON. The same prompt-as-template idea appears in Prompt Engineering Templates for Marketers if you want to refine the wording.

import os
import json
from openai import OpenAI
from dotenv import load_dotenv

load_dotenv()
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))


def generate_caption(topic: str) -> dict:
    """Return {'caption': str, 'hashtags': [str, ...]} for a topic."""
    prompt = (
        f"Write an Instagram caption for: '{topic}'. "
        "Keep the caption under 2000 characters, friendly and concrete. "
        "Return JSON with two keys: 'caption' (string) and "
        "'hashtags' (a list of 5 to 8 short hashtag strings without the # sign)."
    )

    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        response_format={"type": "json_object"},
        temperature=0.8,
    )
    return json.loads(response.choices[0].message.content)

The temperature of 0.8 keeps captions varied across runs so your feed does not read like a template. Lower it toward 0.3 if you want a steadier, more predictable brand voice.

A small helper turns the model's output into one caption string. Instagram counts hashtags toward the 2,200-character limit and allows at most 30 of them, so we keep the list short and join everything cleanly:

def build_full_caption(content: dict) -> str:
    tags = " ".join(f"#{t.lstrip('#')}" for t in content["hashtags"])
    caption = content["caption"].strip()
    return f"{caption}\n\n{tags}"[:2200]

Step 2: Prepare the media

Meta does not accept image uploads from your machine for feed posts. Instead, you give it a public URL and Meta's servers download the file. That means the image must live somewhere reachable by anyone, over HTTPS, with no login and no redirect. Common hosts are an S3 bucket, a Cloudflare R2 bucket, or any plain static web server.

Before you try to publish, confirm the URL behaves the way Meta expects:

import httpx


def check_image_url(img_url: str) -> None:
    """Raise if the URL is not a directly reachable image."""
    resp = httpx.head(img_url, follow_redirects=False, timeout=15)
    if resp.status_code != 200:
        raise ValueError(f"Image URL returned {resp.status_code}, expected 200")

    content_type = resp.headers.get("content-type", "")
    if not content_type.startswith(("image/jpeg", "image/png")):
        raise ValueError(f"Unexpected content-type: {content_type!r}")

Running this check first turns a confusing Graph API error later into a clear message now. Instagram feed images also work best as JPEG between 320 and 1440 pixels wide, with an aspect ratio from 4:5 to 1.91:1.

Step 3: Create and schedule the post

Publishing is a two-call dance. First you create a media container (Meta's staging slot that holds the image plus caption). The container is not instant: Meta downloads and processes the image, so you poll a status field until it reads FINISHED. Then you publish the container, and that is where the scheduled time goes.

Set scheduled_publish_time to a Unix timestamp (a plain integer count of seconds) between 10 minutes and 75 days in the future. It must be an int — floating-point values are rejected.

Instagram scheduled-post flow Caption and image feed into a media container, which is polled until finished, then published with a future timestamp. AI caption + image URL Create media container Poll until FINISHED Publish with future time
The caption and image become a container; once Meta marks it FINISHED, you publish it with a future timestamp.
import time

IG_USER_ID = os.getenv("IG_USER_ID")
TOKEN = os.getenv("IG_ACCESS_TOKEN")
BASE_URL = f"https://graph.facebook.com/v18.0/{IG_USER_ID}"


def create_container(client: httpx.Client, img_url: str, caption: str) -> str:
    resp = client.post(
        f"{BASE_URL}/media",
        params={"image_url": img_url, "caption": caption, "access_token": TOKEN},
    )
    resp.raise_for_status()
    return resp.json()["id"]


def wait_until_ready(client: httpx.Client, container_id: str) -> None:
    for _ in range(10):
        resp = client.get(
            f"https://graph.facebook.com/v18.0/{container_id}",
            params={"fields": "status_code", "access_token": TOKEN},
        )
        if resp.json().get("status_code") == "FINISHED":
            return
        time.sleep(3)
    raise RuntimeError("Media container did not reach FINISHED in time")


def schedule_post(img_url: str, caption: str, hours_from_now: int = 24) -> dict:
    with httpx.Client(timeout=30) as client:
        container_id = create_container(client, img_url, caption)
        wait_until_ready(client, container_id)

        publish_at = int(time.time()) + hours_from_now * 3600
        resp = client.post(
            f"{BASE_URL}/media_publish",
            params={
                "creation_id": container_id,
                "scheduled_publish_time": publish_at,
                "access_token": TOKEN,
            },
        )
        resp.raise_for_status()
        return resp.json()

If you want a post to go out immediately instead, simply omit scheduled_publish_time from the second call.

Step 4: Verify the scheduled post

A successful publish call returns JSON with an id. Print it and confirm the post shows up under your Instagram scheduled content (in the Meta Business Suite planner). Wiring the verify step into the run makes failures loud instead of silent:

def main() -> None:
    content = generate_caption("Our new Python automation course launch")
    caption = build_full_caption(content)
    check_image_url("https://your-cdn.example.com/launch.jpg")

    result = schedule_post(
        "https://your-cdn.example.com/launch.jpg",
        caption,
        hours_from_now=24,
    )
    print(f"Scheduled. Post creation id: {result['id']}")


if __name__ == "__main__":
    main()

That is the full loop: caption, media check, schedule, confirm. Because Meta holds the scheduled post on its own servers, your computer can be off when the post actually goes live.

Key parameters quick reference

ParameterWhereEffect
scheduled_publish_timemedia_publish callUnix timestamp (int) for go-live; must be 10 min to 75 days ahead.
image_urlmedia container callPublic HTTPS image Meta downloads; no login or redirect allowed.
temperaturegenerate_captionHigher (0.8) varies caption wording; lower (0.3) stays on-brand.

Troubleshooting

  1. OAuthException code 190. Your access token expired or was revoked. Long-lived tokens last about 60 days, so regenerate one in the Graph API Explorer and update IG_ACCESS_TOKEN in .env.
  2. Invalid parameter code 100 on publish. Usually scheduled_publish_time is outside the 10-minute-to-75-day window or was sent as a float. Wrap it in int() and confirm it is in the future.
  3. Container stuck in IN_PROGRESS. Meta cannot fetch your image. Run check_image_url and make sure the link is public HTTPS, returns 200, has an image/jpeg or image/png type, and never redirects.
  4. Application request limit reached. You hit the 50-posts-per-24-hours cap. Read the x-business-use-case-usage response header to see your usage and slow down before retrying.

When to use this vs. alternatives

  • Use this script when you want one Instagram account on autopilot with AI captions and full control over timing, and you are comfortable managing a Meta access token.
  • Use Bulk-Schedule Social Posts with Python when you are queuing many posts at once across a content calendar, where reading rows from a spreadsheet matters more than per-post tuning.
  • Use a hosted scheduler (Buffer, Later, Meta Business Suite by hand) when you do not want to maintain code or tokens at all and a monthly fee is acceptable. You trade flexibility and AI integration for convenience.

Back to Automated Social Media Posting.