Approval & comparisons

Migrating from the Instagram Graph API to a unified API

Why teams that integrated directly against the Instagram Graph API move to a unified API, and how media containers, publishing, and token refresh map onto PostGate.

Quick answer

Migrating from the Instagram Graph API to PostGate collapses three maintenance loads: the container create-poll-publish sequence becomes a single POST /api/posts taking a media URL or ID; the 60-day long-lived token refresh cycle runs internally, exposed through a profile health endpoint and expiry webhooks; and per-account publish calls become one request fanned out per target, with an Idempotency-Key honored for 72 hours.

Integrating directly against the Instagram Graph API works — plenty of production apps do it — but it means owning a specific set of maintenance problems forever: refreshing long-lived tokens before they expire, polling media containers through an asynchronous state machine, decoding OAuthException subcodes that Meta's own error messages describe inconsistently, and absorbing whatever changes Meta ships to rate limits or review requirements next. None of that is specific to your product — it is undifferentiated integration work that a unified API abstracts away once, instead of every team rebuilding it separately.

This guide maps the three Graph API concepts that generate the most maintenance load — media containers, the publish step, and OAuth token lifecycle — onto their PostGate equivalents, and points to where we've documented the specific Graph API failure modes that make direct integration painful in the first place.

PostGate handles OAuth, publishing and delivery evidence for every network through one API. Start free or read the docs.

01

What does direct integration make you own?

Three categories of pain show up repeatedly for teams running the Graph API directly, and we've covered each in depth because each one costs real debugging time. Token failures: error 190 is an OAuthException indicating the access token failed validation, and it arrives with a subcode — 460, 463, or 467 — that determines whether it is a security-driven invalidation, a plain expiry, or a revoked authorization, and none of those subcodes are fixed by retrying. Container failures: a media container stuck in QUEUE looks identical to normal processing for most of its wait, and Meta's own guidance to poll "once per minute, for no more than 5 minutes" is shorter than what containers — especially longer Reels under platform load — commonly need in practice, which makes the slow-vs-stuck distinction genuinely hard to automate correctly. Indexing the rest: the complete Instagram Graph API error code index exists because the number of distinct codes and subcodes a publishing integration can hit in production is large enough that most teams end up building their own internal version of that same reference — time that goes into keeping a table up to date instead of into the product.

02

Media containers become one media call

On the Graph API, publishing an image or video is a two-step, stateful process: POST /{ig-user-id}/media creates a container and starts asynchronous processing — fetching the source, transcoding video, validating format — then you poll GET /{ig-container-id}?fields=status_code until it reports FINISHED (or ERROR, or EXPIRED after 24 hours), before a separate publish call goes out.

PostGate collapses that into POST /api/media, which accepts a URL, a base64 payload, or a multipart upload, and moves the resulting asset through pending, uploaded, processing, and ready — the same shape of asynchronous processing, but as one resource shared across every platform in a post, not a container you create and poll per Instagram account. A failed asset carries structured evidence under error instead of a bare status code to interpret.

03

The publish endpoint becomes one post envelope

Once a container is FINISHED, the Graph API publish step is POST /{ig-user-id}/media_publish with the container ID — a call scoped to one Instagram account. Reaching multiple platforms from the same logical post means repeating the entire container-create, poll, and publish sequence per network, each with its own asynchronous shape and error surface.

PostGate's POST /api/posts takes the media ID (or a raw URL) and a list of target profile IDs once, and fans delivery out asynchronously per target with a stable Idempotency-Key making retries of the whole logical request safe for 72 hours.

Publish a photo: Graph API sequence vs. PostGatePostGate API
# Graph API — create container, poll, then publish (per account)
curl -X POST "https://graph.facebook.com/v21.0/{ig-user-id}/media" \
  -d "image_url=https://cdn.example.com/launch.jpg" \
  -d "caption=Launch day. 🚀" \
  -d "access_token={access-token}"
# -> { "id": "{container-id}" }

curl "https://graph.facebook.com/v21.0/{container-id}?fields=status_code&access_token={access-token}"
# poll until status_code == "FINISHED"

curl -X POST "https://graph.facebook.com/v21.0/{ig-user-id}/media_publish" \
  -d "creation_id={container-id}" \
  -d "access_token={access-token}"

# PostGate — one request, every target, one delivery model
curl -X POST "https://postgate.studio/api/posts?profile_group_id=grp_acme123" \
  -H "Authorization: Bearer pg_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: launch-post-001" \
  -d '{
    "post": { "body": "Launch day. 🚀" },
    "profiles": ["pro_instagram123"],
    "media": ["https://cdn.example.com/launch.jpg"]
  }'
04

OAuth token lifecycle becomes automatic refresh

Running the Graph API directly means owning Instagram's 60-day long-lived access token cycle yourself: a background job to refresh tokens before expiry, handling for the case where a refresh itself fails, and — per error 190's subcodes — distinguishing a token that simply expired from one invalidated by a password change or an explicit revocation, each of which needs different handling.

PostGate runs that refresh cycle internally and exposes its result instead of making you rebuild it — see how automatic OAuth token refresh works across platforms. Call GET /api/profiles/{id}/health to read status, granted scopes, token expiry, and reconnect_required before you publish, and subscribe to profile.expiring, profile.expired, and profile.reauth_required webhooks to catch a broken connection before a scheduled post depends on it, rather than discovering it as a failed delivery.

05

Reading results: target evidence over container polling

Replace container polling with the same discipline applied at the post level: a create-post call is acceptance, not confirmation. Consume the post.published, post.partial, and post.failed webhooks, or poll the returned post ID, and read the targets array for per-destination status, attempt count, error, platform ID, and permalink. A partial result — some targets published, others failed — is a normal terminal state, the same distinction a container's FINISHED vs. ERROR status made for one platform, now generalized across every platform in the post.

06

What does a cutover plan look like?

Start in sandbox with a pg_test_ key, which exercises the full asynchronous lifecycle — media processing, target resolution, delivery logs, webhooks — against a sandbox adapter that never calls Instagram, so you can validate your client logic before any real token is involved. Reconnect Instagram accounts through PostGate's hosted connection flow; existing Graph API access tokens issued to your app cannot be transplanted, since authorization has to be re-granted against the new app registration regardless of which API sits behind it. Run a parallel period publishing the same content through both integrations for a subset of accounts, diff the resulting permalinks, then cut over by account cohort while watching delivery logs and webhook payloads before expanding.

07

What should you check before you start?

Inventory every Graph API field your integration currently sets on container creation or publish — user tags, product tags, collaborators, cover images for Reels — and confirm where each lives under PostGate's post.options.instagram object; not every Graph API field has a direct equivalent, so check the platform guide for gaps before you port a request body over. Decide your idempotency-key strategy up front so retries during the parallel-run period cannot double-publish. And if your integration currently reaches other platforms too — most Instagram integrations eventually grow a Facebook, TikTok, or LinkedIn counterpart — plan to bring those onto the same POST /api/posts envelope in the same migration rather than maintaining Graph API code for Meta and a second unified client for everything else.

Frequently asked questions

Can I transplant my existing Instagram Graph API tokens to PostGate?

No. Instagram accounts have to be reconnected through PostGate's hosted connection flow — existing Graph API access tokens issued to your app cannot be transplanted, because authorization has to be re-granted against the new app registration regardless of which API sits behind it. Plan the reconnection step into the migration timeline, since it depends on account owners acting.

What replaces the Graph API media container lifecycle?

POST /api/media, which accepts a URL, a base64 payload, or a multipart upload, and moves the asset through pending, uploaded, processing, and ready — the same shape of asynchronous processing, but as one resource shared across every platform in a post rather than a container you create and poll per Instagram account. A failed asset carries structured evidence under error instead of a bare status code.

Who handles the 60-day Instagram token refresh after migrating?

PostGate runs the refresh cycle internally. You read its result instead of rebuilding it: GET /api/profiles/{id}/health returns status, granted scopes, token expiry, and reconnect_required before you publish, and the profile.expiring, profile.expired, and profile.reauth_required webhooks catch a broken connection before a scheduled post depends on it, rather than surfacing it as a failed delivery.

Do all Graph API publishing fields have PostGate equivalents?

Not every one. Inventory every field your integration sets on container creation or publish — user tags, product tags, collaborators, cover images for Reels — and confirm where each lives under PostGate's post.options.instagram object. Not every Graph API field has a direct equivalent, so check the platform guide for gaps before porting a request body over.

How do I validate the migration before cutting real traffic over?

Start in sandbox with a pg_test_ key, which exercises the full asynchronous lifecycle — media processing, target resolution, delivery logs, webhooks — against a sandbox adapter that never calls Instagram. Then run a parallel period publishing the same content through both integrations for a subset of accounts, diff the resulting permalinks, and cut over by account cohort while watching delivery logs.

Build it with PostGate

One API for every social platform your customers care about — sandboxed, documented, and ready today.

Start building Read the docs