Skip to content

Connect a Facebook Page

The platform can publish approved social drafts to a Facebook Page the same way it posts to X and LinkedIn: a draft targets a connected Page, and the publish worker posts it via the Meta Graph API. This guide covers the one-time Meta-app setup that unlocks it, how a user connects a Page, and — most importantly — how this works when your own clients (clubs, festivals, …) need to publish to their Pages.

The multi-tenant question (read this first)

Section titled “The multi-tenant question (read this first)”

Do your clients each need to create their own Meta app? No. The recommended and standard model — the one Buffer, Hootsuite, Later, and Mixpost all use — is:

One Meta app, owned by you (the platform operator), serves every client’s Page. Each client signs in with Facebook, grants access to their own Page, and the platform stores a per‑Page token for that tenant. A client only needs to be an admin of their own Facebook Page — no developer account, no app, no business verification on their side.

This is exactly how the platform is wired: the Meta app credentials live in server env (FACEBOOK_CLIENT_ID / FACEBOOK_CLIENT_SECRET / …), and every connected Page token is stored on the connecting user’s own owner‑scoped channel. One app, many tenants, per‑tenant tokens.

The connect provider is therefore owner-principal (like YouTube and Reddit), not admin: connecting a Page does not require being named in ADMIN_EMAILS, so onboarding a client is not a redeploy. What still gates a client today is Meta, not us — at Standard Access the Page permissions only work for users holding a role on the app. See Access levels.

Shared app (recommended)Per‑tenant app (BYO)
Who creates the Meta appYou, onceEvery client
Client setupClick “Connect Facebook”, pick a PageCreate a developer app, configure login, paste secrets
App Review / Business VerificationYou do it once (see below)Each client avoids review by staying at Standard Access
Fits non‑technical clients✅ Yes❌ No (clubs won’t do this)
OAuth consent showsyour brand (“Viite is requesting…”)the client’s brand
Rate limitsPer‑Page (scales fine — see Rate limits)Per their own app
Status in code✅ Implemented (server env creds)🔜 Not wired (would follow the Gmail BYO‑creds channel pattern)

The rest of this guide is your one‑time setup as the platform operator.


  • A Facebook account registered as a Meta developer (accept the platform terms; verify by phone/email).
  • A Meta Business portfolio (the rebranded “Business Manager,” at business.facebook.com). The create‑app wizard prompts you to attach one, and Business Verification is performed on the portfolio, not the app. You can skip attaching it for pure local testing, but you’ll need it before real clients can connect.
  • A Facebook Page you administer, to test against (publishing to Pages you admin needs no App Review — see Access levels).
  • For production: a legally registered business with documents for Business Verification.

Step 1 — Create the Meta app (2025/2026 “use case” flow)

Section titled “Step 1 — Create the Meta app (2025/2026 “use case” flow)”

Meta replaced the old “pick a type → add products → add permissions” wizard with a use‑case model: you choose what the app should do and Meta auto‑adds the products, permissions, and APIs.

  1. Open the App DashboardCreate app.
  2. Name the app and confirm a contact email.
  3. On “What do you want your app to do?”, pick the Page‑management use case — currently surfaced as “Manage everything on your Page.” If no Page tile fits, choose “Other”, which routes you to the classic type picker → select Business. Both paths converge on the same capability and provision Facebook Login for Business + the Pages permission set.
  4. Attach your Business portfolio when prompted (you may defer this for early local testing, but it’s required before App Review).
  5. Create. You land in the dashboard with the use case configurable under Use cases (each has its own Permissions and Settings tabs).

Docs: Create an App · App Dashboard · Pages API

Step 2 — Configure Facebook Login for Business

Section titled “Step 2 — Configure Facebook Login for Business”

This is the modern, asset‑oriented login product (vs. classic “log in a consumer”). It is driven by a named configuration rather than an ad‑hoc scope string.

  1. In the app, open Facebook Login for Business → Configurations → Create configuration.

  2. Set the access token type to User access token.

  3. Select the asset type the app needs: Pages.

  4. Add the permissions (next step) to the configuration.

  5. Save — Meta returns a Configuration ID (config_id). Copy it; it becomes the FACEBOOK_LOGIN_CONFIG_ID env var. The connect flow then passes config_id (and omits scope, per Meta’s recommendation).

  6. Open Facebook Login for Business → Settings → Client OAuth Settings and add your Valid OAuth Redirect URI (HTTPS only):

    https://platform-api.viite.ai/api/connect/facebook/callback

    It must match the FACEBOOK_OAUTH_REDIRECT_URI env var byte‑for‑byte. For local testing add http://localhost:4000/... (or your API port) too.

Docs: Facebook Login for Business · Manual login flow

Add these to the use case’s Permissions tab (and to the FLB configuration):

PermissionWhyDefault
public_profileBasic identityauto‑granted
pages_show_listList the user’s Pages (/me/accounts → Page tokens)review for Advanced Access
pages_read_engagementRead Page data (dependency of posting)review for Advanced Access
pages_manage_postsCreate/edit/delete Page posts (the publish grant)review for Advanced Access

Request only these — App Review rejects permissions it can’t tie to a visible feature. (Add pages_manage_engagement later only if you want to manage comments/the “first comment.”)

Docs: Permissions Reference

App Settings → Basic: the App ID is shown plainly (it’s the OAuth client_id); the App Secret is behind a Show button (re‑auth required). Treat the secret as server‑side only. Regenerating it immediately invalidates the old one, so rotate deliberately.


Meta gates each permission by access level, independent of Development/Live mode:

  • Standard Access (no App Review): the permission works only for users who hold a role on the app — Admins, Developers, Testers — and who admin the target Page. This is enough to fully build, test, and dogfood publishing to your own Pages, and to run early pilots by adding clients as app Testers. The posts are real and public.
  • Advanced Access (App Review + Business Verification): required the moment real clients who are not app‑role‑holders connect their own Pages. This is what makes the shared‑app model work for the public.

Docs: Access levels · App modes · App roles

Going to production: App Review + Business Verification

Section titled “Going to production: App Review + Business Verification”

To let arbitrary clients connect their Pages, complete both (verification gates the review):

Business Verification — on your Business portfolio (Business Settings → Security Center / Business Info → Verify):

  • Legal business registration or tax document or a utility bill in the business’s legal name, showing legal name + address (typically dated within 12 months).
  • A verifiable business phone/email; admin 2FA enabled.
  • Typically a few days (blurry/mismatched docs add round‑trips).

App Review — for pages_manage_posts, pages_read_engagement, pages_show_list at Advanced Access:

  • A detailed use‑case description per permission (what it does, why you need it).
  • A screencast of the complete flow: a user logs in via Facebook Login for Business, grants the permission, selects a Page, and your app creates a post — reviewers must see the permission exercised end‑to‑end.
  • Test credentials / a reproducible path.
  • Typically a few days to ~2 weeks; budget 2–4 weeks for verification + review with one rejection cycle.

Docs: App Review · Business Verification (Meta Business Help)


Set these on the API (Coolify). The integration stays dormant until the first three (plus SOCIAL_TOKEN_ENCRYPTION_KEY) are present, so a deploy without them changes nothing.

Env varRequiredNotes
FACEBOOK_CLIENT_IDThe Meta App ID. Non‑secret.
FACEBOOK_CLIENT_SECRETThe Meta App Secret. 🔒 secret — never expose.
FACEBOOK_OAUTH_REDIRECT_URIByte‑for‑byte one of the app’s Valid OAuth Redirect URIs, e.g. https://platform-api.viite.ai/api/connect/facebook/callback.
SOCIAL_TOKEN_ENCRYPTION_KEYShared with X/LinkedIn; encrypts stored tokens at rest.
FACEBOOK_LOGIN_CONFIG_IDoptionalThe Facebook Login for Business configuration id. Set it to pass config_id; leave empty for classic scope‑based login. Non‑secret.
FACEBOOK_PAGE_IDoptionalA preference, not a filter: breaks the tie when the connecting account administers several Pages and administers this one. An account that does not administer it is asked to choose instead — so a single deploy‑wide value can never fail someone else’s connect. Leave it unset in a multi‑tenant deployment.

See Environment variables for the full inventory and the public/secret classification.


Connecting and publishing (in the product)

Section titled “Connecting and publishing (in the product)”

Once the env vars are set:

  1. Dashboard → Settings → Connected social accounts → Facebook Page → Connect. This is a full‑page redirect to Facebook (the OAuth dance needs the callback to land on the API origin). The user authorizes and picks a Page; the platform stores the encrypted Page token on a social://<page>@facebook.com channel they own.
  2. Compose a post targeting that Facebook destination — via the unified content form, or a social-veille workflow whose dispatch destination points at facebook.com.
  3. Approve + schedule the draft in the review queue.
  4. The publish worker picks up the due draft and posts it to the Page — run by “Publish now” on the draft, or by a scheduled custom flow. (The worker is platform‑generic — it routes by the draft’s platform — so no Facebook‑specific schedule is needed.)

Link handling mirrors X/LinkedIn: keep attaches the source URL as a preview card, omit strips it, reply posts the body link‑free and drops the URL in a comment.


A single shared app does not become a single bottleneck, because Page‑token calls are rate‑limited per Page, not per app (the documented budget is on the order of 4,800 calls per engaged user per Page over a rolling 24h window). One heavy client can’t starve another. Read the X-Business-Use-Case-Usage / X-Page-Usage response headers and back off per Page; spread bulk posts. Docs: Rate limiting · Page‑level rate limits.

  • Self‑hosted deployments of the platform (the operator isn’t Viite).
  • A tenant that needs its own brand on the OAuth consent screen, its own rate‑limit bucket, or isolation from Viite’s app policy standing.
  • A tenant that wants to avoid Viite passing review by using Standard Access with their own app (only viable if that tenant is technical).

BYO‑app is not implemented yet. Wiring it means resolving the Meta app creds per org from a channel instead of (or before) the global env — the same shape the Gmail integration already uses for its optional clientId/clientSecret fallback. Until then, the platform is single‑shared‑app.

Instagram is wired — see Connect an Instagram account. It is the sibling connector this section anticipated: the same Meta app and Graph host, the same Page‑token model (an Instagram Business/Creator account publishes through the Facebook Page it is linked to), different permissions (instagram_basic, instagram_content_publish, instagram_manage_comments) and its own INSTAGRAM_* env keys, so it can be dormant while Facebook is live. It connects separately rather than riding this flow, because only a Page with a linked Instagram account qualifies.

WhatsApp Cloud API is still unwired: a separate product with its own onboarding and the whatsapp_business_messaging permission — its own auth path, not a reuse of this one.


GET /api/connect/facebook/introspect asks Meta, via GET /debug_token, which scopes are really on the stored token. Use it before trusting that a connection is complete.

It exists because nothing else can answer the question. The scope we persist on a connector is what we requested, not what was granted — so a row listing four scopes proves only that four were asked for. And when FACEBOOK_LOGIN_CONFIG_ID is set the authorize call sends config_id and no scope, handing the permission list to an App Dashboard configuration that Meta exposes through no Graph endpoint. A scope quietly missing from that configuration is therefore invisible: the connect succeeds, the row looks complete, and the first symptom is a publish or a comment read failing much later.

From the dashboard, signed in as the account holding the connection:

await (await fetch('https://platform-api.viite.ai/api/connect/facebook/introspect',
{ credentials: 'include' })).json()

missingScopes empty means the four Page scopes are genuinely on the token. granular_scopes in Meta’s own response additionally binds each scope to a Page id — useful evidence for an App Review submission.

Both were found in production on 2026-08-19/20, and both are handled in code now. They are worth knowing because neither produces an error a user would understand.

A Business Login asset grant does not appear on /me/accounts

Section titled “A Business Login asset grant does not appear on /me/accounts”

Facebook Login for Business with asset selection returns a token scoped to the Pages the user ticked in the dialog — and that grant does not necessarily show up on /me/accounts, the classic enumeration edge. The symptom is a connect that dies on “No Facebook Pages found” immediately after a dialog that plainly said “1 Asset Selected”.

FacebookOAuthClient now falls back to addressing the asset directly (GET /{page-id}?fields=access_token) when /me/accounts comes back empty and a candidate Page id is configured. The failure message also carries what Meta says about the token — identity and granted permission names — because “no Pages” has three very different causes: no admin role, a Business-Portfolio-owned Page the classic edge hides, or an asset grant the client failed to follow.

from{id,name} on a Page-authored comment fails the whole edge

Section titled “from{id,name} on a Page-authored comment fails the whole edge”

Reading a commenter’s identity needs the Page Public Content Access feature when that commenter is a Page. Without it Graph does not omit the field — it fails the entire /comments request with code 100. One comment written by a Page therefore used to blank an entire campaign read, silently, since the step reports “no comments” rather than a permission problem.

FacebookCommentReader now asks for the author and, on failure, re-asks once without it: the comment survives, only the identity is lost. Expect anonymous commenters as the norm — Meta discloses a commenter’s identity only to apps that person has themselves used, which is never true of someone a campaign just reached.

While the app is in Development, content published through the API is visible only to accounts holding a role on the app — the post is public by its own privacy setting and Facebook still refuses to render it to anyone else. In Live with Standard Access, the comment read is refused instead. The two halves of a publish-then-read demo therefore do not fit in one mode until Advanced Access is granted:

post visible to another accountcomments readable by the API
Development
Live + Standard Access
Live + Advanced Access
SymptomLikely cause
Connect redirect errors with redirect_uri mismatchFACEBOOK_OAUTH_REDIRECT_URI doesn’t byte‑for‑byte match a Valid OAuth Redirect URI on the app (scheme, host, trailing slash).
“No Facebook Pages found” on connectThe signed‑in user doesn’t admin any Page, or pages_show_list wasn’t granted. If you’ve confirmed both and it still fails, see When the normal flow can’t see the Page — this is a known Meta bug for Business‑Portfolio‑owned Pages, not a config error.
“Application inactive” on the Facebook OAuth dialogThe app is in Development mode and the Facebook profile you’re logging in with has no App role (Admin/Developer/Tester). App Dashboard → App roles → Roles → Add People.
Business Settings actions (“Assign access”, editing app↔asset links) throw “You don’t have access to take this action”Meta blocks all Business Portfolio management actions until the account has two‑factor authentication enabled — even for accounts with “Full access” roles. Business Settings → Users → People → your profile will show the exact banner.
Only you/testers can connect; clients get a permissions errorStill at Standard Access — submit App Review for Advanced Access.
Publish fails with a 401/190 token errorThe stored Page token was invalidated (password change, app removed, permission revoked) — the user must reconnect.
The connect returns to /channels asking which PageWorking as intended: the account administers several Pages and none is preferred. Pick one; connect again to add another as its own channel.
facebook_page_unavailable when settling that choiceThe chosen Page is no longer administered by the account (role removed, or a stale picker left open past the 10‑minute window). Connect again.

When the normal flow can’t see the Page: System User workaround

Section titled “When the normal flow can’t see the Page: System User workaround”

Symptom: you’ve done everything right — the connecting Facebook profile is an App role holder, has “Full access: Everything” on the Page in Business Manager, 2FA is on, Business Verification is Verified, and Graph API Explorer confirms the token carries pages_show_list/pages_manage_posts/pages_read_engagement — and yet /me/accounts still returns an empty list, so the connect callback fails with “No Facebook Pages found for this account.”

This happened connecting the Viite.ai Page (2026‑07‑02) and took a long diagnostic chain to confirm. It is a genuine, unresolved Meta platform limitation, not a misconfiguration: for Pages that live natively inside a Business Portfolio (never migrated from a classic personally‑owned Page), the legacy /me/accounts lookup — which personal‑OAuth Page connection has always relied on — does not reliably reflect Business‑Portfolio page‑role grants. Confirmed independently of our code by testing the same personal account directly against Graph API Explorer: both GET /me/accounts and Explorer’s own “Get Page Access Token” picker returned zero Pages. No amount of re‑clicking Business Settings fixes it.

Diagnostic order (don’t skip steps — each has its own distinct fix)

Section titled “Diagnostic order (don’t skip steps — each has its own distinct fix)”
  1. “Application inactive” on the consent screen → the app is in Development mode and the Facebook profile has no App role. Fix: App Dashboard → App roles → Roles → Add People, add that profile as Admin/Developer.
  2. Callback fails with “No Facebook Pages found” → confirm the profile is a Page admin. Business Settings → Accounts → Pages → [Page] → Page access → confirm “Full access” is assigned.
  3. Business Settings actions blocked with “You don’t have access to take this action” → the account needs 2FA enabled before Meta allows any Business Portfolio management action. Business Settings → Users → People → your profile shows a red “Two‑factor authentication required” banner when this is the cause.
  4. Still empty after all of the above → verify independently via Graph API Explorer: select the app, generate a User Token with the three Page permissions, run GET /me/accounts?fields=id,name,access_token. If this — Meta’s own official tool — also returns {"data": []}, you’ve hit this bug. Stop trying more Business Settings clicks; move to the System User workaround below.

The workaround: a System User Page Access Token

Section titled “The workaround: a System User Page Access Token”

A System User is Meta’s recommended mechanism for server‑side access to a Business‑Portfolio‑owned asset, and it bypasses /me/accounts entirely — the token is generated with the Page already scoped in, not resolved by a personal‑profile lookup.

  1. Business Settings → Users → System users → Add. Name it (e.g. <app>-publisher), role Admin.

  2. Assign the app role. Business Settings → Accounts → Apps → [your app]Assign access → select the System User → grant it access. (Without this step, “Generate Token” shows “No permissions available.”)

  3. Assign the Page as an asset. System users → [the system user]Assigned assets tab → assign the target Page with Manage Page permission. (This is separate from step 2 — both are required, and it’s easy to do only one.)

  4. Generate the token. System users → [the system user]Generate New Token → select your app → check pages_show_list, pages_manage_posts, pages_read_engagement → generate (no expiration by default).

  5. Paste it into the platform. Dashboard → Settings → Connected social accounts → Facebook Page → “Trouble connecting? Paste a token manually.” Paste the System User token from step 4 and submit — the server does the rest:

    • Calls GET /me/accounts with the pasted token to resolve the real, Page‑scoped access_token (a System User token authenticates as the system user itself, not the Page — this hop is required).
    • If the System User administers several Pages, the form shows a picker to disambiguate instead of guessing.
    • If /me/accounts comes back empty, it falls back to treating the pasted value as an already‑resolved Page token (covers a token copied straight from Graph API Explorer’s own Page picker).
    • Validates the resolved token against the Graph API and stores it on the connecting user’s owner‑scoped channel — exactly what the OAuth callback would have written, had /me/accounts worked for the personal account.

    Code: FacebookOAuthClient.resolveManualToken() (apps/api/publishing/oauth/FacebookOAuthClient.ts), wired through POST /api/connect/facebook/manual (apps/api/social/routes/socialOAuth.routes.ts).

  6. Verify. GET /api/connectors (owner‑scoped, configs masked) should now list a facebook channel. Publishing works exactly as normal from here — the publish worker doesn’t know or care whether the token came from the OAuth dance or was seeded manually.

This is a one‑time recovery per Page, not an ongoing process — System User tokens generated without an expiration don’t need refreshing. The dashboard’s Connected social accounts card also shows each connection’s expiry (a “Long‑lived” badge for tokens over a year out, a warning badge as a real expiry approaches) so this doesn’t need to be rediscovered by digging through logs next time.

  • Graph API version pinned by the platform: v25.0 (versions). Always pinned explicitly so a new Meta version can’t silently break publishing.
  • Code: apps/api/publishing/oauth/FacebookOAuthClient.ts, apps/api/publishing/platforms/FacebookPublisher.ts, wired in buildConnect / buildSocialPublishers.