Native API· Social accounts

Native APITools

Preview

Social accounts

Send a client a link to connect a social account, handle what comes back to your app, and check that the account can publish.

A brand publishes through the social accounts connected to it. Your client connects each account themselves: they log in with the platform and grant access there. You never handle their password.

You start a connect with a link from create_connect_link, and you confirm it with get_profile.

#
DetailLink to Native’s connect pageHeadless link
ArgumentsbrandId, and optionally returnUrlbrandId, platform and returnUrl
What the client seesA Native page that lists every platform. They pick one and log in.The platform’s own login and consent screen, straight away. No Native page.
How you learn the resultCall get_profile.The browser comes back to returnUrl with a status. Then confirm with get_profile.
StatusIn the gateway today for signed-in Native users, without returnUrl.In development.

A headless link suits an app where your client never sees Native: they click "Connect Instagram" in your app, consent on Instagram, and land back in your app.

How a headless connect works

#

Headless links are in development. This is how they are planned to work:

  1. Your backend calls create_connect_link with brandId, platform and returnUrl.
  2. You send your client’s browser to the url it returns. Your backend never opens it.
  3. The client logs in with the platform and grants access.
  4. The browser comes back to returnUrl with status and platform added to it.
  5. On selection_required, you list the client’s Pages or organisations with get_connect_options, they choose one, and you call complete_connect.
  6. You call get_profile until the account’s channel is connected and readiness.canPublish is true.
#
TOOLcreate_connect_linkPreview

Makes a single-use link your client opens to connect or reconnect a social account for one brand. It changes data, so the token needs the mcp:write scope.

  • Hand the url to your client. Do not open or fetch it yourself: loading it uses it up.
  • If nobody opens it, the link expires about 10 minutes after you make it. Once it is opened, the client has about 15 minutes to finish at the platform.
  • The link only connects an account to this brand. It gives nobody access to the brand or the workspace.
  • When a link expires, make a fresh one. A fresh link can always be made.

Arguments

  • brandIdstringRequired

    The brand to connect an account to, as a UUID.

  • platformstringOptional

    New, in development: makes the link headless. It skips Native’s page and opens this platform’s own login and consent screen. Requires returnUrl. twitter is X. These are lowercase channel names, as get_profile uses them.

    One of: facebook, instagram, linkedin, twitter, tiktok, youtube, pinterest, reddit, threads, googlebusiness

  • returnUrlstringOptional

    New, in development: where the browser goes when the connect is finished. An absolute https URL of 1 to 2,048 characters, with no user name or password in it and no spaces or control characters. Plain http works only for localhost, 127.0.0.1 and [::1], and never in production.

Bluesky is not on the platform list. It connects with an app password typed into Native’s own page, so use a link without platform for it.

With returnUrl and no platform, the link opens Native’s connect page, and its Done button takes the client to returnUrl. Native adds nothing to the URL in that case. Call get_profile to see what was connected. A brand still on Native’s older social provider gets that provider’s own connect page instead, which has no Done button.

Result data

  • urlstringRequired

    The link to hand to your client. A headless link has the form https://app.native.no/e/connect/<token>/go and sends the browser straight on to the platform.

  • expiresAtISO 8601 stringRequired

    When the link expires if nobody opens it. About 10 minutes after you made it.

  • singleUsebooleanRequired

    Always true.

  • brandIdstringRequired

    The brand.

  • brandNamestringRequired

    The brand’s name.

  • platformstring | nullOptional

    New, in development: the platform a headless link opens, or null for a link to Native’s connect page. Absent when you sent neither platform nor returnUrl.

Arguments

{
  "brandId": "3f6c2a9e-8b1d-4c57-9e0a-2d4b7f1c8a63",
  "platform": "instagram",
  "returnUrl": "https://app.example.com/connect/done?client=4711"
}

Result data

{
  "url": "https://app.native.no/e/connect/q8Zt3VbN1xKc5mWp0rLs2A/go",
  "expiresAt": "2026-11-02T09:10:00.000Z",
  "singleUse": true,
  "brandId": "3f6c2a9e-8b1d-4c57-9e0a-2d4b7f1c8a63",
  "brandName": "Harbour Street Bakery",
  "platform": "instagram"
}
#
CoderetryableWhat it means
platform_not_supportedfalseplatform is not one Native can open headless. supportedChannels lists the ones it can. The tool’s input schema refuses other values first, so you are unlikely to see this.
bad_requestfalseplatform was sent without returnUrl, or returnUrl is not an address Native accepts.
headless_connect_unavailablefalseNative could not make the link you asked for with platform or returnUrl, for example because the brand is still on Native’s older social provider. You got no link. Call again without platform and returnUrl for a link to Native’s connect page.

What comes back to your return URL

#

When a headless connect ends, Native sends the browser to returnUrl with query parameters added. status says how it ended, and platform is always there.

statusOther parametersWhat to do
connectedplatformConfirm with get_profile.
selection_requiredplatform, connectTokenThe client manages several Facebook Pages, LinkedIn organisations, Pinterest boards or Google Business locations, and has to pick one. Pass connectToken to get_connect_options, let the client choose, then call complete_connect.
failedplatform, errorNothing was connected. error says why. Make a fresh link to try again.

Examples

https://app.example.com/connect/done?client=4711&status=connected&platform=instagram
https://app.example.com/connect/done?client=4711&status=selection_required&platform=linkedin&connectToken=...
https://app.example.com/connect/done?client=4711&status=failed&platform=tiktok&error=oauth_denied

Native first removes any status, platform, connectToken or error already on your URL, then adds its own. Your other query parameters stay. So you can put your own reference to the client in returnUrl, as client=4711 does above.

If your client opens a headless link after it has expired or been used, Native still sends the browser back to returnUrl, with status=failed and error=link_expired. It does this for up to an hour after you made the link. After that, the link opens Native’s own expired-link page.

Failure codes

#
errorWhat happened
oauth_deniedThe client declined or cancelled on the platform’s screen.
link_expiredThe link had expired or been used before the client got through.
account_in_another_workspaceThe account is already connected to a profile in another Native workspace, so it was not added here. Instagram, TikTok and X connect whichever account the browser is logged in to, so the client may need to log out there and log in to the right account first.
no_facebook_pagesFacebook gave access to no Pages. The client should connect again and select their Pages on Facebook’s screen.
no_google_locationsThe Google account manages no Google Business location.
missing_google_permissionsGoogle did not grant every permission Native asked for. The client should connect again and tick every box on Google’s screen.
connection_failedAnything else, at the platform or at Native.

Treat error as an open set. Some codes come from Native’s social provider, and new ones can appear. Whatever the code, a fresh link is the way to try again.

List what the client can connect

#
TOOLget_connect_optionsIn development

After status=selection_required, lists the Pages, organisations, boards or locations the client can connect to the brand. Show them to the client and let them choose. Reading the options changes nothing, and you can call it again.

Arguments

  • brandIdstringRequired

    The brand, as a UUID.

  • connectTokenstringRequired

    The connectToken query parameter from returnUrl, exactly as it came. 1 to 8,192 characters. It lasts about 10 minutes, and only the caller that made the link can use it.

Result data

  • brandIdstringRequired

    The brand.

  • platformstringRequired

    The platform, such as linkedin.

  • optionsobject[]Required

    What the client can choose from.

    • idstringRequired

      Pass the chosen one to complete_connect as selectionId.

    • namestringRequired

      The name to show.

    • kindstringRequired

      page: a Facebook Page. organization: a LinkedIn organisation. personal: the client’s own LinkedIn profile. board: a Pinterest board. location: a Google Business location.

      One of: page, organization, personal, board, location

    • pictureUrlstring | nullRequired

      A picture to show beside the name, when the platform gives one.

    • detailstring | nullRequired

      A Page’s category or a location’s address. null for the others.

For LinkedIn, the client’s own profile comes first, with kind: "personal" and id: "__personal__".

Result data

{
  "brandId": "3f6c2a9e-8b1d-4c57-9e0a-2d4b7f1c8a63",
  "platform": "linkedin",
  "options": [
    {
      "id": "__personal__",
      "name": "Kari Nordmann",
      "kind": "personal",
      "pictureUrl": null,
      "detail": null
    },
    {
      "id": "urn:li:organization:81234567",
      "name": "Harbour Street Bakery",
      "kind": "organization",
      "pictureUrl": null,
      "detail": null
    }
  ]
}

Errors

#
CoderetryableWhat it means
connect_session_expiredfalseThe connectToken has expired, or its link has died. Nothing was connected. Make a fresh link with create_connect_link and have the client go through the platform again.

Finish the connect

#
TOOLcomplete_connectIn development

Connects the option your client chose to the brand. It changes data, so the token needs the mcp:write scope.

It replaces any account the brand already had on that platform. Only call it with your client’s own choice.

Arguments

  • brandIdstringRequired

    The brand, as a UUID.

  • connectTokenstringRequired

    The same connectToken you passed to get_connect_options. 1 to 8,192 characters.

  • selectionIdstringRequired

    The id of the option your client chose, from get_connect_options. 1 to 200 characters. __personal__ connects a LinkedIn member’s own profile.

Result data

  • brandIdstringRequired

    The brand.

  • platformstringRequired

    The platform.

  • connectedbooleanRequired

    Always true.

  • accountobjectRequired

    The account that is now connected.

    • displayNamestring | nullRequired

      The account’s name, when the platform gives one.

    • usernamestring | nullRequired

      The account’s handle, when it has one.

Arguments

{
  "brandId": "3f6c2a9e-8b1d-4c57-9e0a-2d4b7f1c8a63",
  "connectToken": "c2VhbGVkLWNvbm5lY3QtdG9rZW4tZXhhbXBsZQ",
  "selectionId": "urn:li:organization:81234567"
}

Result data

{
  "brandId": "3f6c2a9e-8b1d-4c57-9e0a-2d4b7f1c8a63",
  "platform": "linkedin",
  "connected": true,
  "account": {
    "displayName": "Harbour Street Bakery",
    "username": null
  }
}

Errors

#
CoderetryableWhat it means
connect_session_expiredfalseThe connectToken has expired, or its link has died. Nothing was connected. Make a fresh link with create_connect_link.
bad_requestfalseselectionId is not one of the options get_connect_options returned. invalidFields names selectionId.

Confirm the connection

#
TOOLget_profilePreview

get_profile shows what a brand can do right now, including each connected account and its health. After status=connected, or after complete_connect, call it once. If the account is not ready yet, call it every 10 seconds for up to 2 minutes.

The account is ready when its channel has health: "connected" and readiness.canPublish is true. A failed status needs a fresh link, not more polling.

Arguments

  • brandIdstringRequired

    The brand.

  • includeScopesbooleanOptional

    Also fetch the permissions each account granted. It costs one extra call to Native’s social provider per connected account, so leave it off for routine checks.

    Default: false

The result data has more than this, such as brandName and generationPlatforms. Profiles has the full reference. These are the fields that tell you whether a connect worked:

Result data, the connection fields

  • readinessobjectRequired

    Whether the brand can make and publish posts.

    • canGeneratebooleanRequired

      Whether Native can make posts for the brand.

    • canPublishbooleanRequired

      Whether the brand has an account it can publish to.

    • blockersstring[]Required

      What stands in the way, as codes. Treat the list as open. no_channel_connected and all_channels_need_reauth both mean: send your client a connect link. connection_check_failed means Native could not check the accounts: call again later, and do not send a connect link. New, in development: profile_not_ready while Native is still building a profile from a website. Readiness blockers lists every code.

  • channelsobject[]Required

    One entry per connected account, including accounts that need reconnecting and accounts that were disconnected.

    • platformstringRequired

      The lowercase channel name, such as instagram.

    • displayNamestring | nullRequired

      The account’s name.

    • usernamestring | nullRequired

      The account’s handle.

    • profileUrlstring | nullRequired

      A link to the account on the platform.

    • healthstringRequired

      connected and expiring_soon publish. needs_reauth and disconnected do not.

      One of: connected, expiring_soon, needs_reauth, disconnected

    • healthReasonstring | nullRequired

      A short reason for the health, when there is one.

    • connectedAtISO 8601 string | nullRequired

      When the account was connected.

    • tokenRefreshInDaysnumber | nullRequired

      Days until the platform’s access needs renewing. null when the platform does not report it.

    • tokenExpiresAtISO 8601 string | nullOptional

      When the platform’s access runs out. May be absent.

    • scopesstring[] | nullRequired

      The permissions the account granted. Filled only with includeScopes. null means unknown, not none.

    • missingScopesstring[] | nullOptional

      Permissions Native needs that the account did not grant. May be absent.

    • canPostboolean | nullOptional

      Whether Native can post to the account. May be absent.

    • canFetchAnalyticsboolean | nullOptional

      Whether Native can read the account’s analytics. May be absent.

  • storyDestinationsstring[] | nullOptional

    The story destinations the brand can use, such as instagram_stories and facebook_stories. May be absent.

Part of the result data

{
  "readiness": {
    "canGenerate": true,
    "canPublish": true,
    "blockers": []
  },
  "channels": [
    {
      "platform": "instagram",
      "displayName": "Harbour Street Bakery",
      "username": "harbourstreetbakery",
      "profileUrl": "https://www.instagram.com/harbourstreetbakery",
      "health": "connected",
      "healthReason": null,
      "connectedAt": "2026-11-02T09:04:12.000Z",
      "tokenRefreshInDays": 58,
      "scopes": null
    }
  ],
  "storyDestinations": [
    "instagram_stories"
  ]
}

Stories have no connect link of their own. Instagram and Facebook stories publish through the Instagram and Facebook accounts, and never appear as channels.

Channel names in get_profile are lowercase, such as instagram. A post’s destinations use uppercase names, such as INSTAGRAM.

A full headless connect

#

This sketch uses client, an official MCP SDK client connected with a partner token, as in Calling a tool. Make the link and send your client’s browser to it:

Make the link

const brandId = "3f6c2a9e-8b1d-4c57-9e0a-2d4b7f1c8a63";

const minted = await client.callTool({
  name: "create_connect_link",
  arguments: {
    brandId,
    platform: "linkedin",
    returnUrl: "https://app.example.com/connect/done?client=4711",
  },
});
const link = (minted.structuredContent as { data: any }).data;
if (minted.isError) throw new Error(link.error.code);

// Redirect your client's browser to link.url. Never fetch it from your backend.

When the browser comes back, read status from the URL:

Handle the return

async function onConnectReturn(returned: URL, brandId: string) {
  const status = returned.searchParams.get("status");

  if (status === "failed") {
    // Offer a fresh link. The error parameter says why it failed.
    return { state: "failed", error: returned.searchParams.get("error") };
  }

  if (status === "selection_required") {
    const connectToken = returned.searchParams.get("connectToken") ?? "";
    const listed = await client.callTool({
      name: "get_connect_options",
      arguments: { brandId, connectToken },
    });
    const data = (listed.structuredContent as { data: any }).data;
    if (listed.isError) return { state: "failed", error: data.error.code };
    // Show data.options to your client. Keep connectToken until they choose.
    return { state: "choose", options: data.options, connectToken };
  }

  // "connected", or anything else: check the profile before you trust it.
  return { state: "check" };
}

Once your client has chosen, finish the connect. Then wait until the account can publish:

Finish and confirm

async function finishConnect(brandId: string, connectToken: string, selectionId: string) {
  const done = await client.callTool(
    {
      name: "complete_connect",
      arguments: { brandId, connectToken, selectionId },
    },
    undefined,
    { timeout: 150_000 },
  );
  const data = (done.structuredContent as { data: any }).data;
  // connect_session_expired: make a fresh link and start again.
  if (done.isError) throw new Error(data.error.code);
}

async function waitUntilPublishable(brandId: string, platform: string) {
  for (let attempt = 0; attempt < 12; attempt++) {
    const result = await client.callTool({ name: "get_profile", arguments: { brandId } });
    const profile = (result.structuredContent as { data: any }).data;
    if (result.isError) throw new Error(profile.error.code);

    const channel = profile.channels.find((c: { platform: string }) => c.platform === platform);
    if (channel?.health === "connected" && profile.readiness.canPublish) return true;

    await new Promise((resolve) => setTimeout(resolve, 10_000));
  }
  return false; // Not ready after 2 minutes: offer a fresh link.
}

Reconnecting an account

#

Platforms take access back after a while, and people change passwords. Check channel health with get_profile once a day, and before you schedule.

healthPublishesWhat to do
connectedYesNothing.
expiring_soonYesSend your client a link for that platform before tokenExpiresAt, so publishing does not stop.
needs_reauthNoSend your client a link for that platform. They log in with the same account.
disconnectedNoSend your client a link for that platform.

A reconnect uses the same create_connect_link call as a first connect. Tell your client which account to reconnect: get_profile names it.

Other errors

#

Every tool on this page can also return the errors any tool can: bad_request, not_found, forbidden, subscription_required, internal_error, upstream_timeout, upstream_unavailable, upstream_identity_failed, identity_provider_unavailable and upstream_<status>. create_connect_link and complete_connect can return writes_disabled. Errors explains each one.

The Native API is not open yet

Partner access is being built now. Tell us what you would build, and we will be in touch when it opens.

Contact sales