Instagram Graph API lies to you: 3 things that broke my automation
I spent a full day debugging an Instagram automation pipeline, convinced I had a bug. Turns out the API was reporting failures that weren't failures. Here's what I found.
1. A 403 doesn't mean the post failed
I called media_publish and got this:
{
"error": {
"message": "Application request limit reached",
"type": "OAuthException",
"code": 4,
"error_subcode": 2207051,
"is_transient": false
}
}
HTTP 403. I logged the failure, skipped updating my database, and moved on.
The post was already live on Instagram.
I found out when I retried and created a duplicate. Instagram sometimes publishes the media and then returns a 403. The request didn't fail. It succeeded with a misleading response code.
After getting a 403 on media_publish, check the user's recent media before treating it as a failure:
async def _publish_container(client, container_id):
resp = await client.post(
f"{GRAPH_URL}/{IG_USER_ID}/media_publish",
params={"creation_id": container_id, "access_token": IG_TOKEN},
)
if resp.is_success:
return resp.json().get("id")
if resp.status_code == 403:
await asyncio.sleep(3)
check = await client.get(
f"{GRAPH_URL}/{IG_USER_ID}/media",
params={"fields": "id,timestamp", "limit": 1, "access_token": IG_TOKEN},
)
if check.is_success:
data = check.json().get("data", [])
if data:
ts = datetime.fromisoformat(data[0]["timestamp"].replace("Z", "+00:00"))
if datetime.now(timezone.utc) - ts < timedelta(seconds=60):
return data[0]["id"]
resp.raise_for_status()
2. Staying under the rate limit isn't enough
Instagram's documented publish limit is 25 posts per 24 hours. You can check it:
r = await client.get(
f"{GRAPH_URL}/{user_id}/content_publishing_limit",
params={"fields": "config,quota_usage", "access_token": token}
)
# {"data": [{"config": {"quota_total": 100, "quota_duration": 86400}, "quota_usage": 0}]}
My quota_usage was 0. I had plenty of room. I was still getting 403s.
The cause: I had published 13 posts within one hour. That's within the daily limit but triggered what the API calls a behavior block, a separate undocumented restriction that kicks in when posting looks automated.
How to tell them apart:
| Rate limit | Behavior block | |
|---|---|---|
quota_usage |
Near the cap | 0, looks fine |
is_transient |
Usually true
|
false |
| Visible in Instagram app | No | No |
| Resolves with time | Yes, 24h reset | Unclear, can last days |
is_transient: false is the clearest signal. Waiting won't help.
The only real prevention is not posting in batches. Use a scheduler with gaps between posts, not a loop.
3. Adding a permission doesn't update your existing token
I needed instagram_manage_contents to delete a post via the API. I added it in the Meta developer console, confirmed it appeared in the app's permission list, then tried the delete call:
{"error": {"message": "(#10) Insufficient permissions", "code": 10}}
Still failing. The reason is obvious once you know it: OAuth tokens are issued with a fixed scope at the time of generation. Adding a new permission to your app doesn't change tokens you already have. You need to generate a new one.
Check what permissions your current token actually has:
r = await client.get(
"https://graph.facebook.com/v22.0/me/permissions",
params={"access_token": token}
)
granted = [p["permission"] for p in r.json()["data"] if p["status"] == "granted"]
# ['instagram_basic', 'instagram_content_publish', 'instagram_manage_contents']
If the new permission isn't there, regenerate via Graph API Explorer with the new scope checked.
None of this is documented clearly anywhere I could find. The 403-that-isn't-a-failure one cost me a full afternoon and a batch of duplicate posts before I figured out what was happening.
Built while automating an Instagram card news account with Graph API v22.0.
Top comments (0)