Connect

Get OAuth connect URL

Changed on

Initiate an OAuth connection flow. Returns an authUrl to redirect the user to. Standard flow: Zernio hosts the selection UI, then redirects to your redirect_url. Headless mode (headless=true): user is redirected to your redirect_url with OAuth data for custom UI. Use the platform-specific selection endpoints to complete.

A profile can hold several accounts of the same platform: connecting a different account adds it as a new account, and reconnecting the SAME account keeps it and all of its history. Ads connections stay one per profile. On a legacy plan limited to N profiles, a new account is refused with 403 platform_account_limit once N accounts of its platform are connected.

TikTok: every connection now goes through the TikTok for Business app. An authorization that leaves out a permission the connected account needs changes nothing at all and comes back as missing_tiktok_permissions; connect again and accept every permission on TikTok's screen.

get/v1/connect/{platform}

Request

  • Base URL: https://zernio.com/api
  • URL: https://zernio.com/api/v1/connect/{platform}
  • Auth: HTTP bearer

Path parameters

platform'facebook' | 'instagram' | 'linkedin' | 'twitter' | 'tiktok' | 'youtube' | 'threads' | 'reddit' | 'pinterest' | 'bluesky' | 'googlebusiness' | 'telegram' | 'snapchat' | 'discord' | 'slack' | 'whatsapp' required

Social media platform to connect. snapchat is a closed beta with no public release date: it returns 403 PLATFORM_BETA_RESTRICTED until the account is approved.

Query parameters

profileIdstring required

Your Zernio profile ID (get from /v1/profiles). For WhatsApp, a Zernio-provisioned number can only be connected on the profile it was provisioned to; connecting from any other profile is rejected with a 409.

reconnectAccountIdstring

Refresh this existing account (a Zernio account id of the same platform on this profile; otherwise 400). The OAuth callback and the selection endpoints (select-page, select-organization, select-board, select-location, Instagram and Snapchat selection) refuse, with reconnect_account_mismatch, a login that would write to a different account of the platform on this profile instead of this one. In headless mode the marker travels in the redirect_url we hand you, so pass that URL back unchanged to the selection endpoint. On X it counts toward the OAuth state limit described under redirect_url.

expectedPlatformUserIdstring

Only connect if the login lands on this platform account; any other account ends the flow with error=account_mismatch (a 409 account_mismatch on POST /v1/connect/instagram/select-account) and nothing is written. Compared with every id the platform reports for the authorized account: the platformUserId of a Zernio account on this platform, or on Instagram either the app-scoped id or the professional account id (metadata.instagramScopedId, the 17841... id that Facebook Login accounts hold as platformUserId). TikTok open_ids are app-scoped, so an id from your own TikTok app never matches; use expectedUsername there. Honoured by the OAuth callback (every platform that connects without a selection step, Instagram Login included) and by the Instagram selection step; on the other selection endpoints you choose the destination yourself. In headless mode the marker travels in the redirect_url we hand you, so pass that URL back unchanged. On X it counts toward the OAuth state limit described under redirect_url.

expectedUsernamestring

Only connect if the authorized account's handle is this one (case-insensitive, a leading @ is ignored); otherwise the flow ends with error=account_mismatch, error_message naming the handle that was authorized, and nothing is written. Same coverage and transport as expectedPlatformUserId; when both are sent both must match. Use this on a first connection, where you hold the handle the user typed but no Zernio id yet.

forceReauthboolean

Instagram Login only (the default loginMethod); ignored elsewhere. Makes Instagram show its login page and ask for the credentials of the account to connect even when another Instagram account is logged in on the device or in the app, and hides the Facebook login option on that page. Use it when the person connecting manages several Instagram accounts on one phone: Instagram otherwise logs in, and converts to a professional account, whichever account is active, before any consent screen. Pair it with expectedUsername to also refuse a wrong account at the end.

redirect_urlstring uri

Your custom redirect URL after connection completes. MUST be an absolute http(s) URL or a custom app scheme for mobile deeplinks (e.g. myapp://callback); a relative path is rejected with 400 INVALID_REDIRECT_URL. X (twitter) caps the OAuth state at 500 characters and the redirect is carried inside it, so the URL-encoded redirect_url must be at most 258 characters for API callers (310 for dashboard sessions; in headless mode the appended headless=true counts toward it); a longer one is rejected with 400 INVALID_REDIRECT_URL. Result params are appended with the URL API, so an existing query string is preserved. Standard mode appends connected={platform}&profileId=X&accountId=Y&username=Z. Headless mode appends OAuth data params for platforms requiring selection (e.g. LinkedIn orgs, Facebook pages). If no selection is needed, the account is created directly and the redirect includes accountId.

On failure, the browser is sent to the same redirect_url with error and platform appended. error and platform are always present. error_message, is_user_fixable, reason, dashboard_url, missing_scopes, error_reason and the platform_error* params are conditional and must be treated as optional. Your own query params are kept on every redirect, but ours overwrite a param of yours with the same name. On an error redirect the internal headless, adsConnect, adsScope, reconnectAccountId, expectedPlatformUserId and expectedUsername markers we add during the flow are removed.

Correlation (every redirect from an OAuth callback, success and failure, and the redirect_url returned by the selection endpoints such as POST /v1/connect/facebook/select-page):

  • request_id: the id we log that request under. Quote it when reporting a problem.
  • stage: where the flow ended. authorize = the platform's consent dialog returned an error or denial instead of a code. callback = we processed the returned code (success, a selection step, or a failure). select_page = the destination-selection endpoint (page, account, organization, board, location, profile or phone number) completed it.

oauth_denied carries error_message and, when the platform sent them, its own values as platform_error (e.g. access_denied), platform_error_reason (e.g. user_denied) and platform_error_description (truncated to 500 characters). is_user_fixable=true is set when the platform reported that the user cancelled or declined.

no_facebook_pages comes with is_user_fixable=true, an error_message telling the user to click "Edit previous settings" in Meta's dialog and tick the Page (or create one), and, when Meta's token debug answered, error_reason:

  • pages_permission_declined: the user declined the pages_show_list permission.
  • no_pages_granted: the permission was granted with no Page ticked. Meta reports a user who manages no Page the same way, so this covers both.
  • granted_pages_not_listed: Pages were ticked but Meta listed none the user can manage, and a direct read of each ticked Page returned no access token.

Headless Facebook success (step=select_page) and headless Instagram facebook_login success (step=select_account): userProfile is JSON that was percent-encoded once before being set as a query param, so it is encoded twice on the wire. After your framework decodes the query string once, run one more decodeURIComponent (or equivalent) and then JSON.parse it. tempToken and connect_token are plain values.

code_already_redeemed means the same authorization code arrived more than once and an earlier request already processed it (the code is never sent to the platform twice). It is not a failure: check GET /v1/accounts or wait for the account.connected webhook.

missing_google_permissions (YouTube, Google Business and Google Ads) means the user unchecked one or more permissions on Google's consent screen. It always comes with is_user_fixable=true. When Google reported the granted scopes, missing_scopes is also present: a comma-separated list of the requested Google scopes that were not granted. Ask the user to connect again and keep every permission checked.

no_youtube_channel means Google authorized the account but it has no YouTube channel we can connect. It always comes with is_user_fixable=true and an error_message. The usual causes: the user picked their personal Google identity instead of the Brand Account in Google's account chooser, they only have YouTube Studio access (not Brand Account owner or manager), or the account has no channel yet.

This list is NOT exhaustive and new values may be added at any time. Treat an unrecognized value as a generic failure rather than matching it exhaustively. Existing values are not renamed or removed without notice.

OAuth and callback: oauth_denied, invalid_callback, invalid_state, unsupported_platform, connection_failed, internal_error, token_exchange_failed, byok_config_error, personal_account_not_supported, missing_google_permissions, missing_tiktok_permissions, platform_requires_destination, reconnect_account_mismatch, account_mismatch, instagram_login_method_mismatch, invalid_request, code_already_redeemed

Access and limits: profile_not_found, invalid_profile_id, access_denied, account_limit_exceeded, profile_limit_exceeded, payment_required

Destination selection: no_facebook_pages, facebook_pages_error, no_google_locations, google_locations_error, google_permission_denied, no_snapchat_public_profiles, snapchat_profiles_error, no_youtube_channel, discord_no_guild, slack_no_team

WhatsApp: whatsapp_error, one_whatsapp_per_profile, whatsapp_number_already_connected, whatsapp_number_pinned_to_profile, whatsapp_coexistence_not_registered, connection_cancelled

Google Ads (platform=googleads): google_ads_auth_failed, google_ads_invalid_state, google_ads_config_error, google_ads_token_failed, google_ads_quota_exhausted, google_ads_callback_error

TikTok Ads (platform=tiktokads): tiktok_ads_auth_failed, tiktok_ads_invalid_state, tiktok_ads_access_denied, tiktok_ads_config_error, tiktok_ads_token_failed, tiktok_ads_account_not_found, tiktok_ads_callback_error

X Ads (platform=xads): x_ads_denied, x_ads_auth_failed, x_ads_config_error, x_ads_account_not_found, x_ads_state_error, x_ads_token_failed, x_ads_token_missing, x_ads_callback_error

Reddit Ads (platform=redditads): reddit_ads_auth_failed, reddit_ads_invalid_state, reddit_ads_access_denied, reddit_ads_scope_not_granted, reddit_ads_no_ad_accounts, reddit_ads_callback_error

Shopify (platform=shopify): shopify_auth_failed, shopify_config_error, shopify_invalid_state, shopify_invalid_hmac, shopify_invalid_shop, shopify_missing_scopes, shopify_callback_error

  1. On this endpoint every upstream OAuth error is collapsed into oauth_denied, with the provider's own values in the platform_error* params described above. The dedicated ads flows below are different: they use their own denial slugs and google_ads_auth_failed and tiktok_ads_auth_failed may carry the provider's raw error string in error_message.

  2. On the tiktok and twitter ads flows platform carries the ads platform id (tiktokads, xads), not the value used in the request path. The googleads, redditads and shopify flows report googleads, redditads and shopify.

  3. missing_tiktok_permissions means the TikTok authorization left out a permission the already-connected account needs, so nothing was changed and it keeps working as before. It is user-fixable: connect again and accept every permission on TikTok's screen.

  4. instagram_login_method_mismatch means an Instagram Login authorization landed on a profile whose Instagram account is connected through Facebook Login, so nothing was changed and it keeps working as before. To refresh it, connect again with loginMethod=facebook_login. To move it to Instagram Login, disconnect it first.

scopesstring
Example:posting,analytics

Comma-separated permission areas to request instead of the platform's full permission set. Values: posting, analytics, comments, messaging, ads. Omit it (the default, and what the dashboard does) to request everything the platform supports.

When present, the consent dialog asks only for the platform scopes behind those areas plus the scopes every connection needs (identity, token refresh, and listing the pages, organizations or channels the user picks from). Scopes the user was never asked for are absent from the account's permissions, and the health endpoints report them as not granted, so a posting-only account cannot read analytics or the inbox until it is connected again with more areas. First comments need comments: a posting-only account publishes the post and skips the first comment. Scopes the platform lists under none of the areas (X likes, bookmarks and follows, Pinterest ads-only extras) are requested only when the parameter is omitted.

Supported on facebook, instagram (both login methods), linkedin, twitter, tiktok, youtube, threads, reddit, pinterest, googlebusiness and slack (via GET /v1/connect/slack). Rejected with 400 INVALID_FIELD_VALUE (param: scopes) on bluesky, telegram, discord, snapchat and whatsapp, whose dialog cannot be reduced, and for an empty list or an unknown area.

What each area asks for, per platform (Google scopes shortened to their last path segment):

PlatformpostinganalyticscommentsmessagingadsAlways requested
facebookpages_manage_postsread_insights, pages_read_user_contentpages_manage_engagement, pages_read_user_contentpages_messagingads_management, ads_read, leads_retrieval, pages_manage_ads, instagram_basic, business_managementpages_show_list, pages_read_engagement, pages_manage_metadata
instagram (Instagram Login)instagram_business_content_publishinstagram_business_manage_insightsinstagram_business_manage_commentsinstagram_business_manage_messagesnoneinstagram_business_basic
instagram (loginMethod=facebook_login)instagram_content_publishinstagram_manage_insightsinstagram_manage_commentsinstagram_manage_messagesads_management, ads_read, leads_retrieval, pages_manage_adsinstagram_basic, pages_show_list, pages_read_engagement, business_management, pages_messaging, pages_manage_metadata
linkedinw_member_social, w_organization_social, r_organization_social, r_organization_followersr_member_postAnalytics, r_member_profileAnalytics, rw_organization_admin, r_organization_social, r_organization_followersw_member_social, w_member_social_feed, w_organization_social, w_organization_social_feed, r_organization_social, r_organization_social_feednoner_ads, rw_ads, r_ads_reporting, r_marketing_leadgen_automation, rw_conversionsopenid, profile, email, r_basicprofile, rw_organization_admin
twittertweet.write, media.write(in the always-requested set)tweet.write, like.write, tweet.moderate.writedm.read, dm.write, media.writenonetweet.read, users.read, offline.access
tiktokvideo.publishuser.info.stats, user.insights, video.list, video.insightscomment.list, comment.list.manage, video.listmessage.list.read, message.list.send, message.list.managenoneuser.info.basic, user.info.username, user.info.profile, user.account.type, video.publish, video.upload, video.list
youtubeyoutube.uploadyt-analytics.readonlyyoutube.force-sslnonenoneyoutube
threadsthreads_content_publish, threads_manage_replies, threads_deletethreads_manage_insightsthreads_content_publish, threads_read_replies, threads_manage_replies, threads_deletenonenonethreads_basic
redditsubmit, read, mysubreddits, flair, history, editread, historyread, history, edit, voteprivatemessagesnoneidentity
pinterestboards:write, pins:read, pins:writepins:read, user_accounts:readnonenoneads:read, ads:write, boards:write, pins:read, pins:writeboards:read, user_accounts:read
googlebusinessbusiness.managebusiness.managebusiness.managenonenonebusiness.manage, userinfo.profile, userinfo.email
slackchat:write, chat:write.public, chat:write.customize, files:write, files:readnonenonechat:write, chat:write.customize, files:write, files:read, channels:history, groups:history, im:history, mpim:history, im:read, im:write, mpim:read, users:read, reactions:read, reactions:writenonechannels:join, channels:read, groups:read, team:read
headlessboolean

When true, the user is redirected to your redirect_url with raw OAuth data (code, state) instead of Zernio's default account selection UI. Use this to build a custom connect experience.

loginMethod'instagram_login' | 'facebook_login'

Instagram only. Which of the two Instagram connection methods to use. Ignored for every other platform.

instagram_login (the default, and what you get if you omit this): the Instagram Login dialog. The user authorizes their Instagram professional account directly, no Facebook Page required.

facebook_login: the Facebook Login dialog, i.e. "Instagram API with Facebook Login". The user authorizes a Facebook Page that has a linked Instagram professional account, and every API call for that account then runs through the Page. Use this when the customer manages Instagram through a Page and expects the Facebook consent screen. Because the user has to pick which Page to connect, the callback continues at the account-selection step, /v1/connect/instagram/select-account.

facebook_login supports headless=true like the other selection platforms: the callback redirects to your redirect_url with profileId, tempToken, platform=instagram, step=select_account, userProfile (the Facebook user's profile, same shape and encoding as the Facebook step=select_page redirect) and connect_token. Pass profileId, tempToken and connect_token into the select-account endpoints to finish. The default instagram_login has no selection step, so it connects the account directly.

onboarding'api' | 'business_app'

WhatsApp only. Ignored for every other platform. Controls which screen Meta's Embedded Signup popup shows.

If omitted, the connection defaults to coexistence (same as business_app below), preserving existing behavior for numbers already on the WhatsApp Business app.

api: standard Embedded Signup, showing Meta's WABA/number picker. Use this to connect a phone number already on Cloud API elsewhere.

business_app: coexistence, i.e. 'Connect existing WhatsApp Business app' (a number shared between Cloud API and the consumer WhatsApp Business app).

signup'hosted'

WhatsApp only. Rejected with 400 INVALID_FIELD_VALUE on any other platform.

hosted: authUrl points at a Zernio-hosted page on zernio.com instead of Meta's OAuth dialog, and the response carries authUrl only (no state). That page opens Meta's Embedded Signup popup itself, so it learns which WhatsApp Business Account and number the user picked inside the popup and connects exactly that one. Use it when your users' Facebook logins manage several WhatsApp accounts: on the default redirect flow Meta only returns an authorization code, so when that login can see more than one number the user lands on Zernio's number picker and has to choose again. Nothing to embed on your side and no domain setup: send the user to authUrl, and they come back to redirect_url with the same params as the redirect flow. Success: connected=whatsapp, profileId, accountId, username (plus connect_token for API-key callers). Failure: error and platform=whatsapp, with the same values and extras as the redirect flow (one_whatsapp_per_profile, whatsapp_number_already_connected, whatsapp_number_pinned_to_profile and whatsapp_coexistence_not_registered with is_user_fixable=true; payment_required with reason and dashboard_url; whatsapp_error with error_message when Meta reported one), plus two of its own: connection_cancelled when the popup was closed before finishing (error_message carries Meta's last reported step or error when there is one) and session_expired when the user took longer than the 60 minute window the hosted page is valid for; restart the flow with a new call in that case. onboarding is carried through and a pre-verified Zernio-provisioned number is attached like on the redirect flow. headless has no effect here because there is no selection step left to hand you. When a previously disconnected account for this profile can simply be re-enabled, this endpoint re-enables it and returns the account directly instead of a URL, exactly like the redirect flow. The page shows the same guidance as the Zernio dashboard: a pre-verified Zernio-provisioned number is called out by name ("choose it in Meta's list, no code will be asked"), coexistence and standard signups get their explainer video, and a step-by-step follow-along checklist stays visible while Meta's popup is open. Skin it with brandName, primaryColor and language below.

brandNamestring

Your product name, for the two branded pages. With signup=hosted (WhatsApp) it replaces Zernio in the page title ("Connect your WhatsApp number to <brandName>"). With welcome=true it fills the heading ("<brandName> wants to connect to your <platform>"); without it the screen shows Zernio and the Zernio mark. Both pages keep a Zernio mark: they are co-branded, not white-label. Trimmed; 1 to 60 characters. Rejected with 400 INVALID_FIELD_VALUE without signup=hosted or welcome=true. Sealed into the page link at issue time, so editing the URL cannot change it.

primaryColorstring

Hex colour (#RRGGBB) for the primary button on the hosted WhatsApp signup page (signup=hosted) or the welcome screen (welcome=true). Validated server-side; anything else is a 400 INVALID_FIELD_VALUE. Rejected without one of the two.

welcomeboolean

Any platform. When true, authUrl points to a Zernio-hosted welcome screen that shows your brand next to the platform ("<brandName> wants to connect to your <platform>") whose button reads Connecting for about a second and then sends the user on to the platform's own authorization screen, with no click; the rest of the flow is unchanged. brandName, logoUrl, primaryColor and welcomeTheme are optional and default to Zernio's own name, mark and look. The link is valid for 30 minutes, after which the page asks the user to start again. The brand also carries to the account and channel pickers Zernio shows after authorization, and when you pass no redirect_url the flow ends on a branded Zernio-hosted page saying whether the account connected, instead of the Zernio dashboard. Cannot be combined with headless=true or signup=hosted (400 INVALID_FIELD_VALUE). The platform's authorization screen still names the Zernio app. Recommended. When left out, the screen is shown if the workspace saved it as on by default (PUT /v1/connect/welcome-screen), except with headless=true or signup=hosted; pass false to skip it. Branding you leave out of the call comes from those saved settings, and a welcomeTheme you send is merged key by key over the saved theme.

welcomeThemestring

Welcome screen only (welcome=true): a URL-encoded JSON object that restyles and rewords the screen. All keys are optional. Copy: heading (up to 120 characters), trustLine, consentLine and statusLine (up to 200 each, statusLine being the line above the button), buttonLabel (up to 30, the label of the link the button becomes if the redirect stalls); each may contain the placeholders {brand} and {platform}. Style: primaryColor (the button), accentColor (the header gradient, built as a scale of that colour, and the icons), pageColor, cardColor and textColor as #RRGGBB, cornerRadius as an integer from 0 to 24 (pixels), font as sans, serif or mono. A primaryColor here wins over the primaryColor parameter. Unknown keys, invalid JSON or an invalid value are a 400 INVALID_FIELD_VALUE. Rejected when the welcome screen is not shown. Example: {"heading":"Link your {platform} to {brand}","cardColor":"#FFF7ED","cornerRadius":12}.

logoUrlstring uri

Welcome screen only (welcome=true): https URL of your logo, shown in a square tile, so use a square image. Without it the tile shows the first letter of brandName, or the Zernio mark when there is no brandName either. Rejected with 400 INVALID_FIELD_VALUE when the welcome screen is not shown.

language'en' | 'es' | 'pt-BR'

Hosted signup page only (signup=hosted, WhatsApp): language of the page and its follow-along guide. Explainer videos stay in English. Default en. Rejected without signup=hosted.

Response

OAuth authorization URL to redirect user to

authUrlstring uri

URL to redirect your user to for OAuth authorization

statestring

State parameter for security (handled automatically)

Changes

    • breaking

      for the query request parameter welcome, default value false was removed

    • info

      added the new optional query request parameter logoUrl

    • info

      added the new optional query request parameter welcome

    • info

      added the new optional query request parameter welcomeTheme