> ## Documentation Index
> Fetch the complete documentation index at: https://www.ayrshare.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Social Linking Overview

> How your users connect their social accounts — the three surfaces, the link session behind all of them, and the options they share.

export const PlansAvailable = ({plans = [], maxPackRequired}) => {
  let displayPlans = plans;
  if (plans && plans.length === 1) {
    const lowerCasePlan = plans[0].toLowerCase();
    if (lowerCasePlan === "business") {
      displayPlans = ["Launch", "Business", "Enterprise"];
    } else if (lowerCasePlan === "premium") {
      displayPlans = ["Premium", "Launch", "Business", "Enterprise"];
    }
  }
  return <Note>
Available on {displayPlans.length === 1 ? "the " : ""}
{displayPlans.join(", ").replace(/\b\w/g, l => l.toUpperCase())}{" "}
{displayPlans.length > 1 ? "plans" : "plan"}.

{maxPackRequired && <span onClick={() => window.open('https://www.ayrshare.com/docs/additional/maxpack', '_self')} className="flex items-center mt-2 cursor-pointer">
 <span className="px-1.5 py-0.5 rounded text-sm" style={{
    backgroundColor: '#C264B6',
    color: 'white',
    fontSize: '12px'
  }}>
   Max Pack required
 </span>
</span>}
</Note>;
};

<PlansAvailable plans={["business"]} maxPackRequired={false} />

Your users connect their own social accounts themselves — they authenticate directly with each
network, and you never see or store their credentials. Everything on this page is about getting
them to that moment and finding out how it went.

One thing is common to every route: a **link session**, created with
[Create a Link Session](/docs/apis/profiles/create-link-session) from your API key and a `Profile-Key`.
Nothing is signed on your side and there is no private key in the flow.

## Three Ways to Connect

Three shapes, and the first two are the same integration. Pick per flow — they are not
exclusive, and plenty of integrations use the hosted page for onboarding and the widget inside
the app afterwards.

| Surface                                                                                                             | Your user sees                                                          | White-labelling                                                                 | Choose it when                                                                                |
| ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| **Widget — embedded frames** ([mount](/docs/multiple-users/connect-widget#mount-a-slot))                                 | our buttons inline in your own layout; no popup until the network's own | **Strongest.** Your page, your fonts and colours, and your user never leaves it | you have a dashboard with a row per network and want linking to happen in place. Max Pack.    |
| **Widget — your own button** ([popup](/docs/multiple-users/connect-widget#your-own-button))                              | your button, then one popup for the network                             | **Strong.** The popup is ours, but it is brief and inherits your appearance     | you want your own button and styling, and no frames in your layout. Max Pack.                 |
| **Hosted linking page** ([how to](/docs/multiple-users/api-integration-business#single-sign-on-with-jwt-authentication)) | a page we host, carrying your logo, colours and custom CSS              | **Weakest.** It is our page, and your user leaves yours to use it               | you want one link to hand out, or you are onboarding by email. Nothing to build, no Max Pack. |

<Note>
  The two widget rows are **one integration**, not two. A single `init` gives you both: mount frames
  where you want our buttons, and call `popup()` from your own button everywhere else. They share
  one session and report on the same handlers.

  [Direct mode](/docs/multiple-users/connect-direct-mode) is the same popup **without** our script — for
  a page with a strict Content-Security-Policy, a server-rendered page, or a native app. There you
  open and watch the popup yourself.

  **Not sure?** Start with the hosted linking page. It needs no Max Pack and no extra parameters,
  and it is the fastest route to something working — moving to the widget later does not change how
  sessions are created.
</Note>

## Creating a Link

Call [Create a Link Session](/docs/apis/profiles/create-link-session) with the user's `Profile-Key` in
the header. For the hosted page that is the whole request:

```bash cURL theme={"system"}
curl -X POST https://api.ayrshare.com/api/profiles/link-sessions \
  -H "Authorization: Bearer $AYRSHARE_API_KEY" \
  -H "Profile-Key: $PROFILE_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```

You get back a `url` carrying a short-lived, opaque token:

```javascript Linking URL theme={"system"}
https://profile.ayrshare.com?session=ayr_ls_kJ8mQ2vX9pLnR4tYwZ6aBcD1eFgH3iJkLmN0oPqRsTu&domain=YOUR_DOMAIN
```

You can also [check whether a link has been opened](/docs/apis/profiles/get-link-session) and
[revoke it](/docs/apis/profiles/revoke-link-session) before it expires.

<Note>
  This one-minute video walks through creating a link. It predates link sessions, so it still shows
  a Private Key being sent — that step is no longer needed, and everything else it shows is
  unchanged.

  <div class="video-container">
    <iframe width="380" height="200" src="https://www.youtube.com/embed/JI232HBWHWc" title="Create a linking URL" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" />
  </div>
</Note>

<h3 id="sending-the-linking-url">
  Sending the Linking URL
</h3>

<Warning>
  A linking URL signs your user into their profile, so treat it like a password. Send it over a
  channel you trust, don't log it, and don't pass it on to a third party. It stays usable for its
  whole window, so a reload or an OAuth retry works — but send each link to one user only, and
  create a separate link per person.
</Warning>

### Opening the Linking URL

Open it in a new browser tab, window, or view controller. You may control the
[closing or redirecting](/docs/multiple-users/api-integration-business#opening-and-closing-the-social-linking-url)
of that window.

<Note>
  The social networks do not allow the hosted linking page to be opened inside an iFrame, or the
  approved partner origin `profile.ayrshare.com` to be obfuscated. If you want linking to happen
  inside your own page, that is what the [embedded widget](/docs/multiple-users/connect-widget) is for:
  its frames are served from an Ayrshare origin and are the supported way to do it.
</Note>

### Knowing When It Is Done

Two signals, and you can use either:

* **[Link completion events](/docs/multiple-users/link-completion-events)** — set an `origin` when you
  create the link and the linking window posts `connect:success`, `connect:error` and
  `connect:cancelled` to your page as they happen. No polling.
* **[Get a Link Session](/docs/apis/profiles/get-link-session)** — reports `completedAt`,
  `lastCompletedAt` and `completedNetworks`. This is the signal for native apps, and the only one
  for Telegram, which completes out of band.

<h2 id="jwt-expires-in">
  Link Expiry
</h2>

A link is valid for **5 minutes** by default. After that, create a new one.

<PlansAvailable plans={["business"]} maxPackRequired />

With the Max Pack, set `expiresIn` in minutes to widen that window — up to **2880 minutes
(48 hours)**, which is the maximum the API accepts:

```json Expires In theme={"system"}
{
  "expiresIn": 30
}
```

A longer window is what makes [emailing the link](#connect-accounts-email) practical — a user
reconnecting a dropped account can go straight from your email to the network, without visiting
your app first.

<Warning>
  Review with your security team how long a link should stay alive. A longer window is a longer
  period in which an intercepted link still works. If one gets out, you can
  [revoke it](/docs/apis/profiles/revoke-link-session) rather than waiting for it to expire.
</Warning>

## Profile Key

The `Profile-Key` identifies which User Profile the link is for. Find it in the Ayrshare developer
dashboard by switching to that profile.

<Note>
  **The Private Key is no longer used.** Links are not signed, so there is nothing to read from a
  file or paste into your code. The legacy `privateKey` parameter is still accepted and ignored, so
  existing integrations keep working, and the `private.key` file in your Integration Package can be
  left unused.
</Note>

## Switching Profiles

If a profile is already signed in, opening another profile's link does not switch profiles — that
is deliberate, and it keeps the experience fast for a user who is already there. To force a switch,
see
[Automatic Logout of a Profile Session](/docs/multiple-users/api-integration-business#automatic-logout-of-a-profile-session).

<h2 id="instagram-link-method">
  Instagram Link Method
</h2>

Instagram accounts can be linked in [two ways](/docs/dashboard/connect-social-accounts/instagram):
directly with **Instagram Login**, or via a **connected Facebook Page**. Which flow starts when a
user clicks the Instagram button is normally controlled by the account-wide
[Instagram Login](/docs/multiple-users/manage-user-profiles#instagram-login) setting.

The `instagramLinkMethod` body parameter overrides that setting for a single link:

| Value       | Instagram Linking Flow                             |
| ----------- | -------------------------------------------------- |
| `instagram` | Direct Instagram Login. No Facebook Page required. |
| `facebook`  | Link via a connected Facebook Page.                |

```json Instagram Link Method theme={"system"}
{
  "instagramLinkMethod": "instagram"
}
```

The override applies for the life of that link, including across the Instagram/Facebook
authorization redirect. A few things to know:

* It does not change your account-wide setting or affect any other link.
* Omit it and the account-wide setting applies, exactly as before.
* An invalid value returns `400` listing the valid values (`instagram`, `facebook`).
* Review the
  [feature differences](/docs/multiple-users/manage-user-profiles#direct-instagram-login-vs-facebook-page-authentication)
  before choosing — some Instagram features, such as hashtag search and collaborations, are only
  available with Facebook Page authentication.

<h2 id="connect-accounts-email">
  Connect Accounts Email
</h2>

<PlansAvailable plans={["business"]} maxPackRequired />

Ayrshare can email the link to your user for you, so they can reach their linking page without
visiting your app. Pair it with a longer [`expiresIn`](#jwt-expires-in) — the default five minutes
rarely survives an inbox.

### Connect Accounts JSON

**Every field inside `email` is required.** A missing one fails the send.

```json Example Contact Email Request theme={"system"}
{
  "expiresIn": 60,
  "email": {
    "to": "john@user.com",
    "contactEmail": "support@mycompany.com",
    "company": "ACME",
    "termsUrl": "https://www.ayrshare.com/terms",
    "privacyUrl": "https://www.ayrshare.com/privacy"
  }
}
```

<Warning>
  `expiresIn` is a **top-level** parameter, not part of the `email` object. Nested inside `email` it
  is ignored, and your user gets a link that expires in five minutes.
</Warning>

The response reports the outcome in `emailSent`:

```json Example Contact Email Response theme={"system"}
{
  "status": "success",
  "sessionId": "c7a2434e72e91bde27579efde0fd6dd0b74ceee29a471fd368407f273708c2e8",
  "url": "https://profile.ayrshare.com?session=ayr_ls_kJ8mQ2vX9pLnR4tYwZ6aBcD1eFgH3iJkLmN0oPqRsTu&domain=YOUR_DOMAIN",
  "expiresAt": "2026-09-02T09:03:26.838Z",
  "emailSent": true,
  "title": "Acme Client"
}
```

A send **failure** does not come back as `emailSent: false` — it returns `code: 333` instead. So
`false` means no email was requested.

### Connect Accounts Email Example

Here is an example of the email that opens the social linking page:

<img src="https://mintcdn.com/ayrshare-docs/Nmrhj2Gh7WSf62Bh/images/apis/profiles/jwt-email.webp?fit=max&auto=format&n=Nmrhj2Gh7WSf62Bh&q=85&s=fbe4ee86ca59c26a5bd5b289fda96b8b" alt="Connect Accounts email" width="563" class="center" data-path="images/apis/profiles/jwt-email.webp" />

The email comes from:

`Social Connect Hub <connect@socialconnecthub.com>`

<h2 id="mobile-jwt">
  Mobile Apps
</h2>

Open the linking URL in the **system browser**, never an embedded webview: Google rejects sign-in
in one with `disallowed_useragent`, and Meta blocks it outright. Your user would see the network's
own error page, and nothing on your side fixes it.

* **iOS** — `ASWebAuthenticationSession`, or `SFSafariViewController`.
* **Android** — Chrome Custom Tabs.

Because a native app has no browser window to post events to, get the result from
[Get a Link Session](/docs/apis/profiles/get-link-session) instead. Set `origin` to your custom scheme
(`myapp://connected`) so the page has a way back into your app.

<h3 id="mobile-code-examples">
  Mobile Code Examples
</h3>

Replace `linkingURL` with the `url` returned from
[Create a Link Session](/docs/apis/profiles/create-link-session).

<CodeGroup>
  ```swift Swift theme={"system"}
  import UIKit
  import SafariServices

  class ViewController: UIViewController, SFSafariViewControllerDelegate {

      var linkingURL = "https://profile.ayrshare.com?session=ayr_ls_kJ8mQ2vX9pLnR4tYwZ6aBcD1eFgH3iJkLmN0oPqRsTu&domain=acme"

      override func viewDidLoad() {
          super.viewDidLoad()
          setupButton()
      }

      func setupButton() {
          let button = UIButton(type: .system)
          button.frame = CGRect(x: (view.bounds.width - 200) / 2, y: (view.bounds.height - 50) / 2, width: 200, height: 50)
          button.setTitle("Open URL", for: .normal)
          button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
          view.addSubview(button)
      }

      @objc func buttonTapped() {
          openURLInInAppBrowser()
      }

      func openURLInInAppBrowser() {
          if let url = URL(string: linkingURL) {
              let safariVC = SFSafariViewController(url: url)
              safariVC.delegate = self
              present(safariVC, animated: true, completion: nil)
          }
      }

      // Optional: If you want to handle when the in-app browser is closed
      func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
          controller.dismiss(animated: true, completion: nil)
      }
  }
  ```

  ```dart Flutter theme={"system"}
  /** yaml dependencies
    dependencies:
      flutter:
        sdk: flutter
      url_launcher: ^6.2.1
  */

  import 'package:flutter/material.dart';
  import 'package:url_launcher/url_launcher.dart';

  void main() {
    runApp(MyApp());
  }

  class MyApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return MaterialApp(
        title: 'URL Launcher Example',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: MyHomePage(),
      );
    }
  }

  class MyHomePage extends StatelessWidget {
    final String linkingURL = "https://profile.ayrshare.com?session=ayr_ls_kJ8mQ2vX9pLnR4tYwZ6aBcD1eFgH3iJkLmN0oPqRsTu&domain=acme";

    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: Text('URL Launcher Example'),
        ),
        body: Center(
          child: ElevatedButton(
            onPressed: () {
              openURLInBrowser(context);
            },
            child: Text('Open URL'),
          ),
        ),
      );
    }

    void openURLInBrowser(BuildContext context) async {
      if (await canLaunch(linkingURL)) {
        await launch(linkingURL);
      } else {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('Could not launch $linkingURL'),
          ),
        );
      }
    }
  }
  ```

  ```jsx React Native theme={"system"}
  /**
  • Using the API provided by expo-web-browser,
  • which opens a URL in a modal browser window that shares cookies
  • with the system browser.

  • Learn more about expo: https://reactnative.dev/docs/environment-setup?guide=quickstart
  • and running the following command:
  • expo install expo-web-browser
  */

  import React from 'react';
  import { StyleSheet, Button, View } from 'react-native';
  import * as WebBrowser from 'expo-web-browser';

  export default function App() {
    const linkingURL = 'https://profile.ayrshare.com?session=ayr_ls_kJ8mQ2vX9pLnR4tYwZ6aBcD1eFgH3iJkLmN0oPqRsTu&domain=acme';

    const openURLInBrowser = async () => {
      try {
        await WebBrowser.openBrowserAsync(linkingURL);
        // Optional: WebBrowser.openBrowserAsync returns a promise that resolves with an object containing
        // 'type' that can be 'cancelled' or 'dismissed'. You can use this to handle when the browser is closed.
      } catch (error) {
        console.error(error);
      }
    };

    return (
      <View style={styles.container}>
        <Button title="Open URL" onPress={openURLInBrowser} />
      </View>
    );
  }

  const styles = StyleSheet.create({
    container: {
      flex: 1,
      justifyContent: 'center',
      alignItems: 'center',
    },
  });
  ```
</CodeGroup>

## Testing

It is **recommended** to first create a link in [Postman](/docs/testing/postman). Your Integration
Package — on the Primary Profile API Key page of the dashboard — includes a sample Postman config.
Import it, fill in your Profile Key in the `profileKey` *body* field, and click *Send*.

The sample config still pre-fills `privateKey` and `domain`. `privateKey` is ignored, and you can
clear `domain` unless your account has more than one linking domain.

You can also [generate the code from Postman](/docs/testing/postman#auto-generate-api-code-with-postman).

### Bubble.io

<Card title="Bubble linking URL" icon="link" href="/docs/packages-guides/bubble#generate-a-linking-url-in-bubble" horizontal />

## Legacy: generateJWT

<Info>
  [Generate a Linking URL](/docs/apis/profiles/generate-jwt) (`generateJWT`) does the same job and is
  **deprecated** — fully supported, no removal date, and unchanged for links you have already
  handed out. Its own page documents its parameters, including the three that are now accepted and
  ignored.

  Both endpoints run the same validator, so everything on this page applies to either. The one
  difference worth knowing when you migrate: `generateJWT` tolerates three things Create a Link
  Session rejects — an unrecognised `allowedSocial` network, a lone X credential, and a non-string
  `redirect`.
</Info>
