Skip to main content
RapidDev - Software Development Agency
API AutomationsInstagramOAuth 2.0

How to Automate Instagram Story Publishing using the API

Publish Instagram Stories via the Graph API using the same container model as feed posts but with media_type=STORIES: POST /{ig-user-id}/media with image_url or video_url, poll status until FINISHED, then POST /{ig-user-id}/media_publish. Key limitations: no interactive stickers (polls, links, questions, countdowns) via API; video Stories capped at 8MB; containers and Stories both expire after 24 hours. Counts toward the 100 posts/24h API publishing cap.

Need help automating? Talk to an expert
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate7 min read30-60 minutesInstagramMay 2026RapidDev Engineering Team
TL;DR

Publish Instagram Stories via the Graph API using the same container model as feed posts but with media_type=STORIES: POST /{ig-user-id}/media with image_url or video_url, poll status until FINISHED, then POST /{ig-user-id}/media_publish. Key limitations: no interactive stickers (polls, links, questions, countdowns) via API; video Stories capped at 8MB; containers and Stories both expire after 24 hours. Counts toward the 100 posts/24h API publishing cap.

API Quick Reference

Auth

OAuth 2.0

Rate limit

200 calls/hour per user; 100 posts/24h

Format

JSON

SDK

Available

Understanding the Instagram Stories API

Instagram Stories are published through the same Graph API container model as feed posts and Reels. The base URL is https://graph.instagram.com/v25.0. You create a container with media_type=STORIES, wait for it to process, then publish. The key OAuth scope required is instagram_business_content_publish.

The most important limitation to communicate to developers building Story automation: the API cannot publish interactive stickers. Polls, question boxes, location tags, countdowns, link stickers, and music — all the features that make Stories engaging — are not available through the API. You can only publish plain images (JPEG) and plain videos (MP4/MOV, ≤8MB). If your automation requires interactive Stories, you'll need to use a third-party solution like Buffer or Later that has direct partnerships with Meta.

Stories expire naturally after 24 hours of being live, and the container also expires after 24 hours if not published. Do not pre-create containers — always create them at publish time. For scheduled automation, store the media URL and caption in your database and create the container when the cron fires. Official docs: https://developers.facebook.com/docs/instagram-platform/instagram-graph-api/reference/ig-user/media

Base URLhttps://graph.instagram.com/v25.0

Setting Up Instagram Graph API Authentication

Stories publishing uses the same OAuth 2.0 long-lived tokens as other Instagram Graph API operations. The instagram_business_content_publish scope is what enables programmatic publishing. Short-lived tokens (1 hour) must be immediately exchanged for 60-day long-lived tokens after the OAuth flow completes.

  1. 1Create a Business App at developers.facebook.com
  2. 2Add the Instagram product and configure Instagram Business Login
  3. 3Request instagram_business_basic and instagram_business_content_publish in App Review > Permissions
  4. 4Implement OAuth redirect: https://www.facebook.com/v25.0/dialog/oauth?client_id={app-id}&redirect_uri={uri}&scope=instagram_business_basic,instagram_business_content_publish
  5. 5Exchange authorization code at https://graph.facebook.com/v25.0/oauth/access_token
  6. 6Exchange short-lived token for 60-day token: GET https://graph.instagram.com/access_token?grant_type=fb_exchange_token&client_id={app-id}&client_secret={secret}&fb_exchange_token={token}
  7. 7Get the user's Instagram Business account ID: GET https://graph.instagram.com/v25.0/me?fields=id,username&access_token={token}
  8. 8Schedule automated refresh every 50 days: GET https://graph.instagram.com/refresh_access_token?grant_type=ig_refresh_token&access_token={token}
auth.py
1import requests
2import os
3
4IG_USER_ID = os.environ['IG_USER_ID']
5ACCESS_TOKEN = os.environ['IG_ACCESS_TOKEN']
6
7def get_long_lived_token(app_id, app_secret, short_token):
8 resp = requests.get('https://graph.instagram.com/access_token', params={
9 'grant_type': 'fb_exchange_token',
10 'client_id': app_id,
11 'client_secret': app_secret,
12 'fb_exchange_token': short_token,
13 })
14 resp.raise_for_status()
15 return resp.json()['access_token']
16
17def test_stories_permission():
18 """Verify the token has content publish scope by checking account info."""
19 resp = requests.get(
20 f'https://graph.instagram.com/v25.0/{IG_USER_ID}',
21 params={'fields': 'id,username,account_type', 'access_token': ACCESS_TOKEN}
22 )
23 resp.raise_for_status()
24 data = resp.json()
25 print(f'Account: @{data["username"]} ({data["account_type"]})')
26 if data['account_type'] == 'PERSONAL':
27 raise ValueError('Personal accounts cannot use the Instagram Graph API. Switch to Business/Creator.')
28 return data

Security notes

  • Store all tokens in environment variables or a secrets manager — never in source code
  • Long-lived tokens expire after 60 days; set up an automated renewal job running every 50 days
  • Never embed App Secret in client-side applications
  • Restrict OAuth redirect URIs to your production and staging domains only
  • Log all Story publish operations with timestamps and media IDs for audit purposes
  • Revoke tokens immediately if your server or secrets store is compromised

Key endpoints

POST/{ig-user-id}/media

Creates a Story container. Set media_type=STORIES with either image_url (JPEG, proper aspect ratio) or video_url (MP4/MOV, ≤8MB). The container processes asynchronously — poll status before publishing.

ParameterTypeRequiredDescription
media_typestringrequiredMust be 'STORIES' for Story content.
image_urlstringoptionalHTTPS URL of a JPEG image. Use 9:16 aspect ratio (1080x1920px) for best results. Required if not using video_url.
video_urlstringoptionalHTTPS URL of an MP4 or MOV video. Maximum 8MB file size. H.264 video codec + AAC audio required. Required if not using image_url.

Request

json
1{"media_type": "STORIES", "image_url": "https://example.com/story.jpg", "access_token": "EAAB..."}

Response

json
1{"id": "17889615691580904"}
GET/{container-id}?fields=status_code

Polls the Story container's processing status. Images finish quickly (seconds); videos take longer. Must reach FINISHED before calling media_publish.

ParameterTypeRequiredDescription
fieldsstringrequiredSet to 'status_code'. Possible values: IN_PROGRESS, FINISHED, ERROR, EXPIRED, PUBLISHED.
access_tokenstringrequiredSame long-lived token used to create the container.

Response

json
1{"status_code": "FINISHED", "id": "17889615691580904"}
POST/{ig-user-id}/media_publish

Publishes the Story container. The Story goes live immediately and is visible for 24 hours. Returns the published media ID.

ParameterTypeRequiredDescription
creation_idstringrequiredThe container ID from the /media creation call.
access_tokenstringrequiredLong-lived User Access Token with instagram_business_content_publish scope.

Request

json
1{"creation_id": "17889615691580904", "access_token": "EAAB..."}

Response

json
1{"id": "17895695668004550"}
GET/{ig-user-id}/content_publishing_limit

Returns your current publishing quota usage. Stories count toward the same 100/day cap as feed posts and Reels.

ParameterTypeRequiredDescription
fieldsstringoptionalUse 'config,quota_usage' to get both current usage and the total cap.

Response

json
1{"data": [{"quota_usage": 2, "config": {"quota_total": 100, "quota_duration": 86400}}]}

Step-by-step automation

1

Prepare Story Media and Validate Specs

Why: Stories have specific size and format requirements, and the 8MB video cap is much smaller than other content types — catching issues before the API call saves wasted quota.

Ensure your media meets Story specs: images should be JPEG at 9:16 ratio (1080x1920px), with a safe zone in the center 75% to avoid Instagram's UI overlays. Videos must be MP4/MOV with H.264+AAC codecs, under 8MB, and 9:16 aspect ratio. A common mistake is submitting landscape images — Instagram will crop them, often badly.

request.sh
1# Verify the story image URL is accessible
2curl -I "https://example.com/story-image.jpg"
3# Check Content-Type should be image/jpeg

Pro tip: Design Story images with the safe zone in mind: keep all key content (text, logos) in the center 75% of the image. Instagram overlays the profile name and close button at the top, and engagement buttons at the bottom, covering ~12.5% of the frame on each side.

Expected result: Validation passes for media that meets Story specs. Issues are caught before wasting an API call on a rejected container.

2

Create the Story Container

Why: The container is the unpublished Story object — creating it triggers Instagram's media validation pipeline.

POST to /{ig-user-id}/media with media_type=STORIES and either image_url (for photo Stories) or video_url (for video Stories). The media must be at a publicly accessible HTTPS URL. Store the returned container ID for the next steps.

request.sh
1# Image Story
2curl -X POST \
3 "https://graph.instagram.com/v25.0/${IG_USER_ID}/media" \
4 -d "media_type=STORIES" \
5 -d "image_url=https://example.com/story.jpg" \
6 -d "access_token=${ACCESS_TOKEN}"
7
8# Video Story
9curl -X POST \
10 "https://graph.instagram.com/v25.0/${IG_USER_ID}/media" \
11 -d "media_type=STORIES" \
12 -d "video_url=https://example.com/story.mp4" \
13 -d "access_token=${ACCESS_TOKEN}"

Pro tip: There is no caption or hashtag field for Stories via the API — unlike feed posts, Stories do not support text captions through the API. Any text overlays must be baked into the image itself before upload.

Expected result: JSON response: {"id": "17889615691580904"} — the container ID. Save this for status polling and publishing.

3

Poll Container Status and Publish

Why: Stories must reach FINISHED status before publishing — publishing too early returns an error, and waiting too long results in container expiry.

Poll GET /{container-id}?fields=status_code until the status is FINISHED, then immediately call media_publish. Photo containers typically finish in 2-10 seconds; video containers take 30 seconds or more. Once published, the Story is live for 24 hours.

request.sh
1# Poll status
2curl -s "https://graph.instagram.com/v25.0/${CONTAINER_ID}?fields=status_code&access_token=${ACCESS_TOKEN}"
3
4# Publish when FINISHED
5curl -X POST \
6 "https://graph.instagram.com/v25.0/${IG_USER_ID}/media_publish" \
7 -d "creation_id=${CONTAINER_ID}" \
8 -d "access_token=${ACCESS_TOKEN}"

Pro tip: Fetch Story insights (impressions, reach, exits) before they expire using GET /{media-id}/insights?metric=impressions,reach,exits&period=lifetime. Set a 23-hour delayed job right after publishing to capture final metrics before the Story expires.

Expected result: The Story is published and live on Instagram for 24 hours. The response contains the media_id for later analytics retrieval.

Complete working code

Complete Story publishing scheduler: validates media specs, checks publishing quota, creates the Story container, polls until ready, publishes, and schedules a metrics collection job 23 hours later before Story expiry.

automate_ig_story.py
1#!/usr/bin/env python3
2"""Instagram Story Publisher - cron job or queue worker."""
3import os
4import time
5import logging
6import requests
7
8logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
9log = logging.getLogger(__name__)
10
11IG_USER_ID = os.environ['IG_USER_ID']
12ACCESS_TOKEN = os.environ['IG_ACCESS_TOKEN']
13BASE = 'https://graph.instagram.com/v25.0'
14
15def ig_get(path, params):
16 params['access_token'] = ACCESS_TOKEN
17 r = requests.get(f'{BASE}{path}', params=params, timeout=30)
18 r.raise_for_status()
19 return r.json()
20
21def ig_post(path, data):
22 data['access_token'] = ACCESS_TOKEN
23 r = requests.post(f'{BASE}{path}', data=data, timeout=30)
24 r.raise_for_status()
25 return r.json()
26
27def check_quota():
28 d = ig_get(f'/{IG_USER_ID}/content_publishing_limit', {'fields': 'config,quota_usage'})
29 item = d['data'][0]
30 log.info(f'Quota: {item["quota_usage"]}/{item["config"]["quota_total"]}')
31 return item['quota_usage'] < item['config']['quota_total']
32
33def create_story(image_url=None, video_url=None):
34 body = {'media_type': 'STORIES'}
35 if image_url:
36 body['image_url'] = image_url
37 elif video_url:
38 body['video_url'] = video_url
39 else:
40 raise ValueError('Must provide image_url or video_url')
41 return ig_post(f'/{IG_USER_ID}/media', body)['id']
42
43def wait_ready(container_id, timeout=300):
44 deadline = time.time() + timeout
45 delay = 5
46 while time.time() < deadline:
47 status = ig_get(f'/{container_id}', {'fields': 'status_code'})['status_code']
48 log.info(f'Container {container_id}: {status}')
49 if status == 'FINISHED':
50 return
51 if status in ('ERROR', 'EXPIRED'):
52 raise ValueError(f'Container failed: {status}')
53 delay = min(delay * 2, 30)
54 time.sleep(delay)
55 raise TimeoutError('Container did not finish in time')
56
57def publish_story(container_id):
58 return ig_post(f'/{IG_USER_ID}/media_publish', {'creation_id': container_id})['id']
59
60def get_story_insights(media_id):
61 """Fetch Story insights. Call within 23 hours of publishing."""
62 try:
63 data = ig_get(f'/{media_id}/insights', {'metric': 'impressions,reach,exits', 'period': 'lifetime'})
64 return {item['name']: item['values'][0]['value'] for item in data['data']}
65 except Exception as e:
66 log.warning(f'Could not fetch insights for {media_id}: {e}')
67 return {}
68
69def publish_story_flow(image_url=None, video_url=None):
70 if not check_quota():
71 log.warning('Quota exhausted')
72 return None
73 container_id = create_story(image_url=image_url, video_url=video_url)
74 log.info(f'Container created: {container_id}')
75 wait_ready(container_id, timeout=120 if video_url else 60)
76 media_id = publish_story(container_id)
77 log.info(f'Story live! Media ID: {media_id} (expires in 24h)')
78 return media_id
79
80if __name__ == '__main__':
81 media_id = publish_story_flow(image_url='https://your-cdn.com/story.jpg')
82 if media_id:
83 print(f'Published: {media_id}')

Error handling

400{"error":{"code":24,"message":"(#24) Invalid media type","type":"OAuthException"}}
Cause

For video Stories: file exceeds 8MB limit, wrong codec (not H.264/AAC), or unsupported container format. For image Stories: not JPEG format, or invalid dimensions.

Fix

For video: re-encode with `ffmpeg -i input.mp4 -c:v libx264 -c:a aac -fs 8000000 output.mp4` (8MB limit flag). For images: convert to JPEG with PIL/sharp. Check that the URL is publicly accessible.

Retry strategy

Do not retry — fix the media format and dimensions first.

400{"error":{"code":9004,"message":"(#9004) The media container has expired","type":"OAuthException"}}
Cause

The Story container was created more than 24 hours ago and expired before being published.

Fix

Create a new container. Never pre-create containers for scheduled Stories — generate them at publish time, within minutes of the target time.

Retry strategy

Create a new container and restart the flow.

400{"error":{"code":17,"message":"User request limit reached","type":"OAuthException"}}
Cause

Hit either the 200 calls/hour per user limit or the 100 posts/24h publishing cap. Stories count toward both.

Fix

Check X-Business-Use-Case-Usage header. For 200/hr: use exponential backoff. For 100/day publishing cap: check content_publishing_limit before creating containers.

Retry strategy

Exponential backoff starting at 60 seconds for rate limit; delay posting to the next day for the 100/day cap.

400{"error":{"code":190,"message":"Invalid OAuth access token","type":"OAuthException"}}
Cause

The 60-day long-lived access token has expired or the user revoked the app's permissions.

Fix

Refresh via GET https://graph.instagram.com/refresh_access_token?grant_type=ig_refresh_token&access_token={token}. If fully expired, the user must re-authorize.

Retry strategy

Do not retry — refresh the token first.

Rate Limits for Instagram Stories API

ScopeLimitWindow
Per user (app+user pair)200 API callsper hour (rolling)
Publishing cap100 API-published posts (Stories + feed posts + Reels combined)per 24-hour sliding window per account
retry-handler.ts
1import time
2import requests
3
4def with_retry(fn, max_retries=4):
5 """Execute fn with exponential backoff on rate limit errors."""
6 for attempt in range(max_retries):
7 try:
8 return fn()
9 except requests.HTTPError as e:
10 if e.response.status_code in (400, 429):
11 code = e.response.json().get('error', {}).get('code')
12 if code in (17, 4, 32, 613): # Rate limit codes
13 wait = min(60 * (2 ** attempt), 3600)
14 print(f'Rate limited. Waiting {wait}s')
15 time.sleep(wait)
16 continue
17 raise
18 raise Exception('Max retries exceeded')
  • Check the 100 posts/day cap before creating Story containers — not after a failed publish attempt
  • Monitor X-Business-Use-Case-Usage header on every response; pause when call_count percentage hits 80%
  • For bulk Story campaigns (posting multiple Stories in quick succession), add 5-second delays between publish calls
  • Schedule Story analytics collection jobs 23 hours after publish to capture insights before the 24-hour expiry
  • Use the content_publishing_limit endpoint at the start of your publishing run to plan how many Stories you can post

Security checklist

  • Store access tokens in environment variables or a secrets manager — never hardcode in source files
  • Never expose App Secret in client-side or mobile code
  • Ensure Story image/video URLs use HTTPS — HTTP is rejected by the API
  • Validate media URLs are publicly accessible before creating containers — use a HEAD request check
  • Log all Story publish operations with media IDs and timestamps for audit trails
  • Implement automated token refresh every 50 days to prevent silent authentication failures
  • Restrict OAuth redirect URIs to your production and staging domains in the Meta App Dashboard

Automation use cases

Daily News or Product Update Stories

intermediate

Generate daily Story images from a template with current data (product of the day, quote, weather) and publish them automatically each morning.

Event Countdown Story Series

intermediate

Publish a series of countdown Stories in the days leading up to an event, each generated from a template with the current days-remaining count.

Story Cross-Post from Other Content

advanced

When a new blog post or YouTube video publishes, automatically generate a Story preview card and post it to Instagram to drive traffic.

Portfolio or Menu Showcase Stories

beginner

Cycle through a collection of portfolio items or restaurant menu items as individual Stories, refreshed weekly from a Google Sheets or Airtable source.

No-code alternatives

Don't want to write code? These platforms can automate the same workflows visually.

Zapier

Free tier (limited); Starter from $19.99/month

Zapier's Instagram for Business integration supports Story creation triggered by other app events — Google Sheets rows, RSS feeds, or custom webhooks.

Pros
  • + No code required
  • + Easy trigger setup
  • + Good for simple automation
Cons
  • - Cannot add interactive stickers (same API limitation applies)
  • - Higher cost at scale
  • - Less timing control

Make (formerly Integromat)

Free tier (1,000 ops/month); Core from $9/month

Make supports Instagram Story publishing with visual scenario building and better scheduling granularity than Zapier.

Pros
  • + More affordable than Zapier
  • + Better data transformation
  • + Precise scheduling
Cons
  • - Same API sticker limitations
  • - Learning curve
  • - Limited free operations

n8n

Free (self-hosted); Cloud from $20/month

n8n self-hosted can automate Story publishing with full API control via HTTP request nodes, combined with image generation steps.

Pros
  • + Free self-hosted
  • + Full control over the flow
  • + Can chain with image generation APIs
Cons
  • - Requires server infrastructure
  • - More setup required
  • - Same sticker API limitations apply

Best practices

  • Design Stories for the 9:16 aspect ratio (1080x1920px) — landscape or square images will be cropped by Instagram
  • Keep key content in the center 75% of the Story frame — Instagram UI overlays cover the top and bottom ~12.5% each
  • Do not try to add interactive stickers (polls, questions, links, countdowns) via API — they are not supported and will be silently dropped
  • For video Stories, stay well under 8MB — compress videos aggressively since mobile Story viewers don't benefit from high bitrates
  • Never use captions for Stories via the API — Story captions are not supported and will be ignored without an error
  • Schedule Story analytics collection 23 hours post-publish — wait until near-expiry to capture the most complete engagement data
  • Create containers at publish time (within minutes of the target time) — never pre-create Story containers hours or days ahead

Ask AI to help

Copy one of these prompts to get a personalized, working implementation.

ChatGPT / Claude Prompt

I'm building an Instagram Story auto-publisher using the Instagram Graph API v25.0. The flow is: POST /{ig-user-id}/media with media_type=STORIES and image_url, poll status_code until FINISHED, then POST /{ig-user-id}/media_publish. I want to add: 1) an image generation step that creates Story-format JPEG images from templates using Pillow (text overlay, background color, logo), 2) a post-publish job that runs 23 hours later to collect insights before Story expiry, 3) proper error handling for the 8MB video limit and the 100 posts/24h cap. Show me the full pipeline.

Lovable / V0 Prompt

Build a React Story scheduling dashboard for Instagram. It needs: a story queue showing upcoming Stories with their preview images and scheduled times; an image upload component that accepts images and resizes them to 1080x1920 (Story format) before displaying a preview; a scheduler with datetime picker; a 'recent stories' section showing the last 7 days of published Stories with their impressions and reach (fetched before they expire); and a quota widget showing the 100 API posts/24h usage. Backend in Supabase with Edge Functions handling the actual Instagram API calls.

Frequently asked questions

Can I add polls, links, or question stickers to Stories via the API?

No. Interactive stickers — polls, question boxes, link stickers, countdowns, location tags, music, and GIFs — cannot be added through the Instagram Graph API. Only plain images and plain videos can be published via API. If you need interactive Stories with these features, you'll need to publish manually through the Instagram app or use third-party tools that have special partner access with Meta (rare and expensive).

What is the video file size limit for Stories via API?

8MB maximum for video Stories published via the API. This is much smaller than the native app limit. Use ffmpeg to compress your video before uploading: `ffmpeg -i input.mp4 -c:v libx264 -c:a aac -crf 28 -fs 8000000 output.mp4`. The -fs flag sets a hard file size limit of 8MB. Stories video must also use H.264 codec and AAC audio.

How long does a Story stay live after publishing via API?

Exactly 24 hours, same as manually published Stories. There is no way to extend or change this via the API. If you want to highlight a Story permanently (similar to an Instagram Highlight), that feature is not available through the API — you can only add Stories to Highlights manually in the Instagram app.

What happens when I hit the rate limit while publishing Stories?

You'll get HTTP 400 with error code 17 (User request limit reached). This can mean either the 200 calls/hour quota for API calls is exceeded, or the 100 posts/24h publishing cap is hit. Check the X-Business-Use-Case-Usage response header to see your usage percentages. For the publishing cap, use GET /{ig-user-id}/content_publishing_limit to check before creating containers. For the hourly call limit, implement exponential backoff starting at 60 seconds.

Can Stories be published to personal Instagram accounts via API?

No. The Instagram Graph API only supports Business and Creator accounts. Personal accounts have no API access. The account must also be connected to a Facebook Page. If your account is personal, go to Instagram Settings > Account > Switch to Professional Account to convert it to a Creator or Business account.

Can I delete or archive a Story published via API?

There is no API endpoint to delete or archive Stories. Stories published via API are deleted the same way as any other: they expire naturally after 24 hours. If you need to remove a Story before expiry, you must do it manually in the Instagram app. This is an important consideration for error handling — if you accidentally publish the wrong Story, you cannot programmatically remove it.

Can RapidDev build an automated Story publishing system for my brand?

Yes — RapidDev has built Story automation pipelines for brands and content teams, including template-based image generation, scheduling dashboards, and automated analytics collection. We handle the Meta App setup, App Review process, and production infrastructure. Book a free consultation at rapidevelopers.com.

RapidDev

Need this automated?

Our team has built 600+ apps with API automations. We can build this for you.

Book a free consultation

Skip the coding — we'll build it for you

Our experts have built 600+ API automations. From prototype to production in days, not weeks.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.