Zernio
Zernio
Overview

Guides

ProfilesConnecting accountsMedia UploadsQueue SchedulingTimezones & SchedulingIdempotency & Safe RetriesPost LifecycleError HandlingRate LimitsPlatform Settings
Dashboard
llms.txtOpenAPI
OverviewPlatformsAPI ReferenceResources
Guides

Connecting accounts

Connect an account to a profile with OAuth, a hosted or headless selection step, or credentials for Bluesky, Telegram and Shopify.


When you finish this page an account is connected to a profile and you have its accountId. You need an API key, a profile id (Step 2 of the quickstart) and a login on the platform. The 16 posting platforms and Shopify each connect one of the ways below; the rules specific to a platform are on its platform page.

OAuth flow (most platforms)

Call GET /v1/connect/{platform} with profileId. Zernio returns an authUrl; send the user's browser there, and when they approve, the platform sends them back and the account is connected.

import Zernio from '@zernio/node';

const zernio = new Zernio();
const profileId = '66a1f0c2a4b9d3e8f1a2b3c4';

const { data: connect } = await zernio.connect.getConnectUrl({
  path: { platform: 'linkedin' },
  query: { profileId, redirect_url: 'https://myapp.com/callback' }
});
// Send the user's browser to connect.authUrl

Response (200):

{
  "authUrl": "https://www.linkedin.com/oauth/v2/authorization?client_id=..."
}

After approval the user lands on redirect_url with connected=linkedin&profileId=...&accountId=...&username=... appended; an existing query string is kept. redirect_url must be an absolute http(s) URL or an app scheme such as myapp://callback; a relative path is rejected with 400 INVALID_REDIRECT_URL, and a malformed profileId with 400. The Start OAuth endpoint lists every parameter.

Scopes

Zernio requests every scope a platform needs in this single OAuth flow; scopes cannot be requested one at a time. Each platform page lists what its consent screen asks for. To see what a connected account can do with the scopes the user granted, call Account health: it returns canPost and canFetchAnalytics per account.

Platforms requiring secondary selection

Six platforms, one of them conditionally, need the user to pick which Page, organization, board, location or public profile to connect after OAuth:

PlatformWhat to selectEndpoints
FacebookPageList Pages → Select Page
LinkedInOrganization or personal profileList orgs → Select org
PinterestBoardList boards → Select board
Google Business ProfileLocationList locations → Select location
SnapchatPublic profileList profiles → Select profile
Instagram (loginMethod=facebook_login only)Page with a linked Instagram accountList Pages → Select account

Instagram is conditional: the default Instagram Login (loginMethod omitted) creates the account with no selection step. Only loginMethod=facebook_login adds one, because the user has to say which Page to connect. The Instagram page explains the two methods; both modes below work for it.

Standard vs headless mode

Standard mode (default): Zernio hosts the selection screen. The user picks their Page or organization there, then lands on your redirect_url.

Headless mode: you build the selection screen. Pass headless=true when starting the flow. After OAuth, the user lands on your redirect_url with tempToken, userProfile (URL-encoded JSON), step=select_page and connect_token query params. Your backend passes them to the list and select endpoints to connect the account.

step names the selection endpoint to call next: select_page (Facebook), select_organization (LinkedIn), select_board (Pinterest), select_location (Google Business Profile), select_public_profile (Snapchat), select_phone_number (WhatsApp), select_account (Instagram via Facebook Login). Instagram sends no userProfile, because its select-account endpoint does not take one.

The headless flow for a Facebook Page starts like any OAuth flow, with headless: true:

const { data: start } = await zernio.connect.getConnectUrl({
  path: { platform: 'facebook' },
  query: { profileId, headless: true, redirect_url: 'https://your-app.com/cb' },
});
// Send the user's browser to start.authUrl

Response (200):

{
  "authUrl": "https://www.facebook.com/v21.0/dialog/oauth?client_id=..."
}

After consent, Meta sends the user to https://your-app.com/cb?profileId=...&tempToken=...&userProfile=...&platform=facebook&step=select_page&connect_token=.... In that handler, read tempToken and userProfile from the request URL, then list the Pages the user manages:

// requestUrl: the URL your handler received; searchParams decodes the values
const { tempToken, userProfile: encodedProfile } = Object.fromEntries(new URL(requestUrl).searchParams);
const userProfile = JSON.parse(encodedProfile);

const { data: pages } = await zernio.connect.facebook.listFacebookPages({
  query: { profileId, tempToken },
});

Response (200):

{
  "pages": [
    { "id": "123456789", "name": "My Brand Page", "username": "mybrand", "category": "Brand" }
  ]
}

Connect the chosen Page with POST /v1/connect/facebook/select-page. userProfile is the decoded object, not the encoded string:

const { data: result } = await zernio.connect.facebook.selectFacebookPage({
  body: {
    profileId,
    pageId: pages.pages[0].id,
    tempToken,
    userProfile,
    redirect_url: 'https://your-app.com/final-success',
  },
});
// Send the browser to result.redirect_url

Response (200):

{
  "message": "Facebook page connected successfully",
  "redirect_url": "https://your-app.com/final-success?connected=facebook&profileId=66a1f0c2a4b9d3e8f1a2b3c4&accountId=66b2e19d8c3f5a7e9d0b1c2d&username=My+Brand+Page",
  "account": {
    "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
    "platform": "facebook",
    "username": "mybrand",
    "isActive": true,
    "selectedPageName": "My Brand Page"
  }
}

account.accountId is the new accountId.

Connect Meta Ads only (skip the Page picker)

For the classic login (loginMode=classic, the default), if your user only needs ads (audience uploads, the Conversions API, analytics, listing ads and campaigns), start at GET /v1/connect/facebook/ads instead and let your backend pick the first Page without showing a picker. The user goes from Meta's consent screen straight to your "Connected" screen. To connect ads independently of a posting account, use Facebook Login for Business.

const { data: adsStart } = await zernio.connect.connectAds({
  path: { platform: 'facebook' },
  query: { profileId, headless: true, redirect_url: 'https://your-app.com/cb' },
});
// Send the user's browser to adsStart.authUrl, then list and select
// as above with pages.pages[0].id and no picker.

Response (200):

{
  "authUrl": "https://www.facebook.com/v21.0/dialog/oauth?client_id=..."
}

The callback, list and select calls are the headless flow above. The metaads account is created alongside the Facebook account. Roughly 70% of the metaads surface (everything that does not emit object_story_spec.page_id) works whichever Page is bound, so the first Page is fine for ads-only callers. The remaining 30% (boosting Page posts, Click-to-WhatsApp ads, Lead Gen Forms) is Page-specific: when the user later needs those for a specific Page, show a picker then and POST the new pageId to /v1/connect/facebook/select-page. Zernio updates the existing Facebook account in place, with no second OAuth.

Facebook Login for Business

Call GET /v1/connect/{platform}/ads with platform set to facebook or instagram, profileId and loginMode=business. For example, the Facebook route is GET /v1/connect/facebook/ads?profileId=...&loginMode=business. Omitting loginMode uses classic.

Business login always returns an authUrl to open in the user's browser. The callback creates or reconnects an independent metaads account using a Business Integration System User token, without creating or requiring a posting account. GET /v1/accounts identifies it with metadata.tokenType: "system-user". When Meta returns no expires_in, tokenExpiresAt is absent; Zernio does not re-exchange it as a personal token.

Select a granted Facebook Page for ad creatives and lead forms:

  • Pass pageId on the connect request to choose a specific granted Page.
  • Without pageId, Zernio reuses the previous Page or chooses the sole granted Page automatically.
  • With several granted Pages and no selection, API callers receive a 400 listing the available Page IDs. Restart the connect request with pageId.
  • With zero granted Pages, the connection can manage campaigns and sync insights, but cannot create Page-based creatives or list Page forms. Grant a Page when reconnecting to use those features.

A business reconnect preserves the connection's ID, history and ad-account scope. It must grant every previously scoped ad account, or every previous grant on an unscoped connection; missing or unverifiable grants return 409 before changing the account.

Scoping sync to specific ad accounts

By default, sync covers every act_* ad account the connected Meta token can see. That is fine for one person's account but leaks for agencies and multi-Business-Manager setups, where the token sees every account in every Business Manager the user has a role on. To restrict sync to an allowlist, pass adAccountId (one) or adAccountIds (several) on GET /v1/connect/facebook/ads:

?adAccountId=act_1330190928038136
?adAccountIds=act_1330190928038136,act_3686966528111132

Zernio validates each id against the token's /me/adaccounts and stores the list. The account.ads.initial_sync_completed webhook then carries account.platformAdAccountId (when the scope is exactly one account) and account.platformAdAccountIds (always), so you can confirm what was synced. Omit both params to keep the "sync everything visible" behaviour. The latest call wins: a new connect with new ids replaces the earlier allowlist.

Platforms without OAuth

Bluesky

Bluesky connects with an app password. state is {userId}-{profileId}: userId is currentUserId from GET /v1/users, profileId from GET /v1/profiles.

const { data: bluesky } = await zernio.connect.bluesky.connectBlueskyCredentials({
  body: {
    identifier: 'yourhandle.bsky.social',
    appPassword: 'your-app-password',
    state: '66a0e8b1c2d3e4f5a6b7c8d9-66a1f0c2a4b9d3e8f1a2b3c4',
  },
});
console.log(`Connected ${bluesky.account.username}`);

Response (200):

{
  "message": "Bluesky connected successfully",
  "account": {
    "platform": "bluesky",
    "username": "yourhandle.bsky.social",
    "displayName": "Your Name",
    "isActive": true
  }
}

The Connect Bluesky endpoint lists every field.

Telegram

Telegram connects with an access code:

  1. Call GET /v1/connect/telegram to generate an access code, valid for 15 minutes.
  2. The user adds the Zernio Telegram bot as an admin of the channel or group and sends it the code.
  3. Poll PATCH /v1/connect/telegram to check the status until it reports connected.

If the bot is already an admin of the channel or group, skip the code and connect directly with POST /v1/connect/telegram and the chat id.

Shopify

Shopify is OAuth, but the authorization URL is built per store, so you need the merchant's myshopify.com domain before you start. Shopify offers no store picker and no lookup from a merchant to their shops.

  1. Collect the store domain from the merchant (your-store.myshopify.com; the bare your-store prefix is accepted).
  2. Call GET /v1/connect/shopify with profileId and shop to get the authorization URL.
  3. Redirect the merchant to the returned authUrl; after approval they land on your redirect_url.

A merchant who installs from the Shopify App Store never types the domain, because Shopify supplies it to Zernio directly.

To skip the browser flow, the merchant can create a custom app in their Shopify admin with the read_content and write_content scopes and hand you its Admin API access token, which you exchange for a connected store with POST /v1/connect/shopify/token. shop is required there too: an Admin token does not identify its own store.

A connected store publishes no social posts. It powers the Blogs API; see the Shopify page.

Change a selection without reconnecting

Change the selected Page, organization, board, location or subreddit on an existing account without a second OAuth:

  • Update Facebook Page
  • Update LinkedIn organization
  • Update Pinterest board
  • Update Google Business Profile location
  • Update Reddit subreddit

If it fails

When the user denies consent or the platform rejects the callback, the browser lands on your redirect_url with error and platform appended, for example ?error=oauth_denied&platform=linkedin. Treat an unknown error value as a generic failure; new values are added without notice.

The connect call itself returns 402 when a billing gate blocks it:

{
  "error": "X (Twitter) requires a payment method due to API pass-through costs. Add a payment method to connect an X account.",
  "code": "PAYMENT_REQUIRED",
  "reason": "twitter_passthrough",
  "dashboard_url": "https://zernio.com/dashboard?tab=billing"
}

reason is free_tier_exceeded (more than the free connected accounts and no card on file), twitter_passthrough (any X account without a card) or enterprise_required (a contract cap). Send the user to dashboard_url to fix it.

Related

  • List accounts: every connected account and its isActive state.
  • Update an account: change settings such as default Pages or boards.
  • Account health: verify tokens and permissions.
  • Disconnect an account: remove it from the profile.
  • Instagram: Instagram Login versus Facebook Login.
Was this page helpful?

Profiles

Create a profile, connect a second account of the same platform into it, and rename or delete it later.

Media Uploads

Upload an image, video or document with a presigned URL and attach it to a post with mediaItems.

On this page

OAuth flow (most platforms)ScopesPlatforms requiring secondary selectionStandard vs headless modeConnect Meta Ads only (skip the Page picker)Facebook Login for BusinessScoping sync to specific ad accountsPlatforms without OAuthBlueskyTelegramShopifyChange a selection without reconnectingIf it failsRelated