We build systems that post to Facebook and Instagram on their own — an article goes live, and a few hours later the right posts appear on the client’s Page and profile, forever, without anyone touching it. The hard part isn’t the posting. It’s the one credential that stands between “works in the demo” and “works in six months.” Below is the whole routine with the actual commands — not a description of them. Copy, paste, swap in your values.
The 30-second mental model
Everything talks to Meta’s Graph API. Every request is a URL against https://graph.facebook.com/v26.0/… with an access_token on the end. If you can run curl, you can do all of this by hand before you ever touch n8n, Make, or code — and you should, because doing it by hand is how you catch the wrong-token traps while they’re still cheap.
You need, in this order: a long-lived user token → the Page ID + Page token derived from it → the connected Instagram business account ID. Get those three right and posting is trivial.
Step 1 — get a token, then immediately make it long-lived
Grab a starter token from the Graph API Explorer with these permissions: pages_show_list, pages_read_engagement, pages_manage_posts, business_management, and for Instagram instagram_basic + instagram_content_publish. That token is short-lived — roughly one hour. This is the single most-skipped step, so do it now, before anything else: exchange it for a ~60-day token.
curl -G https://graph.facebook.com/v26.0/oauth/access_token \
--data-urlencode "grant_type=fb_exchange_token" \
--data-urlencode "client_id=YOUR_APP_ID" \
--data-urlencode "client_secret=YOUR_APP_SECRET" \
--data-urlencode "fb_exchange_token=SHORT_LIVED_TOKEN"The response gives you a new access_token — that’s your long-lived user token. Everything below uses it.
Step 2 — get the REAL Page ID and Page token (never from the URL)
The number in a Page’s browser URL is a decoy — use it and you get "(#803) Object does not exist", which sends you hunting for a permissions bug that isn’t there. Ask the API which Pages you manage and read the ID straight off the answer:
curl -G https://graph.facebook.com/v26.0/me/accounts \
--data-urlencode "access_token=LONG_LIVED_USER_TOKEN"The response looks like this — the ID you want is data[].id, and each Page comes with its own access_token, which is your Page token:
{
"data": [{
"id": "1253492794504809", <- the real Page ID
"name": "Your Business",
"access_token": "EAAG..." <- the Page token, use this to post
}]
}Step 3 — verify the token is a PAGE token before you trust it
Here’s the trap that eats an afternoon. A user token can list the Page, read it, even show “manage content” permission — then reject your post with "(#200) requires admin permission", while you sit there being an admin. The permission being present isn’t the same as holding the kind of token that can use it. Only a Page token posts as the Page. Inspect it and check one field — type:
curl -G https://graph.facebook.com/v26.0/debug_token \
--data-urlencode "input_token=TOKEN_TO_CHECK" \
--data-urlencode "access_token=LONG_LIVED_USER_TOKEN"In the response, data.type must read PAGE, not USER. Also check data.data_access_expires_at (not just expires_at) — that’s the real death clock, and it’s the one to diarise a reminder against.
Step 4 — post to the Page
Now it’s one call. The URL structure is always /{PAGE_ID}/feed, posting as the Page by using the Page token:
curl -X POST https://graph.facebook.com/v26.0/PAGE_ID/feed \
-d "message=Your post text here" \
-d "link=https://yoursite.com/the-article" \
-d "access_token=PAGE_TOKEN"A real post ID comes back ({"id":"PAGE_ID_POSTID"}). Open the Page and see it. Saved is not the same as working.
Step 5 — Instagram, and the look-alike ID that posts into a void
Instagram posting hangs off the same Page token, but you need the connected business account ID. There are two IDs on offer that both look valid; the page_backed_instagram_accounts one is a legacy stand-in that fails silently. Pull the right one explicitly:
curl -G https://graph.facebook.com/v26.0/PAGE_ID \
--data-urlencode "fields=instagram_business_account{id,username}" \
--data-urlencode "access_token=PAGE_TOKEN"Use instagram_business_account.id — and assert it is not equal to your Page ID (if they match, something upstream is wrong). Instagram posting is two calls: create a media container from a public image URL, then publish it.
# 1. create the container
curl -X POST https://graph.facebook.com/v26.0/IG_ACCOUNT_ID/media \
-d "image_url=https://yoursite.com/img/cover.png" \
-d "caption=Your caption here" \
-d "access_token=PAGE_TOKEN"
# -> returns { "id": "CREATION_ID" }
# 2. publish it
curl -X POST https://graph.facebook.com/v26.0/IG_ACCOUNT_ID/media_publish \
-d "creation_id=CREATION_ID" \
-d "access_token=PAGE_TOKEN"Note: image_url must be a publicly reachable URL — Meta fetches it server-side. If your image endpoint returns anything but a clean HTTP 200, IG posting dies with a vague error. Curl your own image URL first and confirm the 200.
The failure that doesn’t announce itself
Even a properly long-lived Page token isn’t immortal. It dies on a password change, a security checkpoint, an admin being removed, or the app being switched off — and it dies the same silent way the short-lived one does. No error on screen, no alert; the posts just quietly stop and you find out days later. So the token is never the whole answer. The whole answer is the token plus a watchdog: a daily job that checks the newest post isn’t stale and emails a human the moment posting stops. We treat that watchdog as part of the build, not an add-on. A system that can fail silently isn’t finished.
The whole routine, in order
- Exchange for long-lived first —
oauth/access_token?grant_type=fb_exchange_token. This is the step that prevents the silent hour-later death. - Real IDs from the API, never a URL — Page ID from
me/accounts, IG ID frominstagram_business_account{id}. - Verify with
debug_token—typemust bePAGE; read the expiry offdata_access_expires_at. - Store it in the credential store — never a chat, a note, or a commit.
- Prove it with a real post you can see, then wire the watchdog before you call it done.
Why we give this away
None of these are hard once you’ve met them — they’re expensive exactly once, the first time a lying error message sends you debugging the wrong thing for an hour. We’ve paid for every one of these gotchas already, which is why the next build is boring. If you’ve got a workflow that should be posting itself and quietly isn’t, that gap is almost always a token that died without saying so. The way we build assumes it will, and watches for it — and if you’d rather not run the token dance yourself, that’s the part we take off your plate.