DEV Community

JustJinoIT
JustJinoIT

Posted on

Instagram Graph API lies to you: 3 things that broke my automation

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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}]}
Enter fullscreen mode Exit fullscreen mode

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}}
Enter fullscreen mode Exit fullscreen mode

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']
Enter fullscreen mode Exit fullscreen mode

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)