> ## 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.

# Create a Link Session

> Create a social-linking URL for a user profile, without sending a private key.

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>;
};

export const HeaderAPI = ({noProfileKey, profileKeyRequired}) => <>
    <ParamField header="Authorization" type="string" required>
      <a href="/docs/apis/overview#authorization">API Key</a> of the Primary Profile.
      <br />
      <br />
      Format: <code>Authorization: Bearer API_KEY</code>
    </ParamField>
    {!noProfileKey && (profileKeyRequired ? <ParamField header="Profile-Key" type="string" required>
          <a href="/docs/apis/overview#profile-key-format">Profile Key</a> of a User Profile.
          <br />
          <br />
          Format: <code>Profile-Key: PROFILE_KEY</code>
        </ParamField> : <ParamField header="Profile-Key" type="string">
          <a href="/docs/apis/overview#profile-key-format">Profile Key</a> of a User Profile.
          <br />
          <br />
          Format: <code>Profile-Key: PROFILE_KEY</code>
        </ParamField>)}
  </>;

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

Create a social-linking URL for a User Profile. Send the returned `url` to your user, and
they open it to connect their social accounts.

This is the recommended way to create a linking URL. It needs only your API key and a
`Profile-Key` — there is no private key to send and nothing to sign. Unlike a linking
URL created before, a link session is stored, so you can check whether it has been used
and revoke it before it expires.

The returned `url` signs your user into their profile, so treat it like a password and send
each one to a single user. See
[Sending the Linking URL](/docs/apis/profiles/social-linking-overview#sending-the-linking-url).

<Note>
  The URL is valid for **5 minutes** by default. Use `expiresIn` to set a different
  window, up to 2880 minutes (48 hours).
</Note>

<Info>
  [Generate a Linking URL](/docs/apis/profiles/generate-jwt) performs the same operation and keeps
  working unchanged. It accepts the legacy `privateKey`, `base64` and `verify` parameters
  and ignores them. `domain` is not ignored on either endpoint - it stays optional and is
  still validated. New integrations should use this endpoint.

  One difference in the response: `generateJWT` returns a top-level `token` for backwards
  compatibility, and this endpoint does not return one **alongside** a `url` — the token in a
  `url` lives inside it. If you are migrating and your code reads `token`, read `url` instead.
  ([Connect mode](#connect-mode) for the embedded widget is the one shape that returns a bare
  `token`, because it returns no URL for the token to live in.)
</Info>

## Header Parameters

<HeaderAPI profileKeyRequired={true} />

<Note>
  The `Profile-Key` is a header on this endpoint — there is no `profileKey` body
  parameter. If it is missing you get `code: 188`, whose message lists `privateKey`,
  `profileKey` and other legacy field names because it is shared with
  [Generate a Linking URL](/docs/apis/profiles/generate-jwt). Read it as "the Profile-Key header is
  missing or wrong"; none of the other names in it are parameters of this endpoint.
</Note>

<ParamField header="X-Twitter-OAuth1-Api-Key" type="string">
  Your X API Key (Consumer Key) from the X Developer Portal. When provided, the linking
  URL will use your own X Developer App for OAuth linking.
</ParamField>

<ParamField header="X-Twitter-OAuth1-Api-Secret" type="string">
  Your X API Secret (Consumer Secret) from the X Developer Portal. Required when
  `X-Twitter-OAuth1-Api-Key` is provided.
</ParamField>

<h2 id="body-parameters">
  Body Parameters
</h2>

<ParamField body="mode" type="string" default="grid">
  Which linking surface this session drives.

  * `grid` — the hosted linking page, showing every network you permit. This is the default, so a
    request that omits `mode` creates one.
  * `connect` — one network at a time, opened from your own dashboard. See
    [Connect Mode](#connect-mode) below and [Direct Mode](/docs/multiple-users/connect-direct-mode).

  Never inferred: passing `origin` or `network` does not put you in connect mode, so a grid
  session cannot become a gated one by accident. Any other value returns `code: 188` with
  `details` naming the two.
</ParamField>

<ParamField body="expiresIn" type="number" default={5}>
  Longevity of the link in minutes. Range: 1 to 2880 minutes.

  Requires the Max Pack.

  See [Link Expiry](/docs/apis/profiles/social-linking-overview#jwt-expires-in) for more information.
</ParamField>

<ParamField body="logout" type="boolean" default={false}>
  Automatically log out the current session. Not recommended in production, since it
  affects performance.

  See [Automatic Logout of a Profile Session](/docs/multiple-users/api-integration-business#automatic-logout-of-a-profile-session).
</ParamField>

<ParamField body="redirect" type="string">
  A URL to redirect to when the "Done" button or logo image is clicked. Add the query
  parameter `origin=true` to redirect the opener window.
</ParamField>

<ParamField body="allowedSocial" type="array">
  The social networks to display on the linking page. Overrides the networks configured
  on the [Social Networks](/docs/multiple-users/manage-user-profiles#set-social-networks-access)
  page.

  ```json Only display Facebook, X/Twitter, LinkedIn, and TikTok theme={"system"}
  {
    "allowedSocial": ["facebook", "twitter", "linkedin", "tiktok"]
  }
  ```
</ParamField>

<ParamField body="network" type="string">
  Connect mode only. The single social network this session connects, which is what makes it a
  **direct-mode** session. Omit it for a session your own dashboard drives across several
  networks.

  One of `bluesky`, `facebook`, `gmb`, `instagram`, `instagramApi`, `linkedin`, `pinterest`,
  `reddit`, `snapchat`, `telegram`, `threads`, `tiktok`, `twitter`, `whatsapp`, `x`, `youtube`.
  Anything else returns `code: 508` — including `fbg`, which is not a link target here.

  Cannot be combined with `allowedSocial` (`code: 507`): a single-network session is already its
  own allowlist. A network your account has not enabled returns `code: 509`, which is a different
  answer from 508 because it is fixable on your
  [Social Networks](/docs/multiple-users/manage-user-profiles#set-social-networks-access) page.

  In grid mode it is ignored.
</ParamField>

<ParamField body="instagramLinkMethod" type="string">
  Override which Instagram linking flow is used for this link. Valid values:

  * `instagram`: Direct Instagram Login, no Facebook Page required.
  * `facebook`: Link Instagram via a connected Facebook Page.

  When omitted, the linking page uses your account-wide
  [Instagram Login](/docs/multiple-users/manage-user-profiles#instagram-login) setting.
</ParamField>

<ParamField body="origin" type="string">
  The exact origin of the page that opened the linking window, so it can be told when
  linking finishes.

  When set, the linking page posts events to that origin with `window.postMessage` as
  your user connects each account, and your page can react without polling. Events are
  only ever sent to this exact value, so it must match your page's origin character for
  character, including the scheme and any port.

  Three shapes are accepted: an `https` origin (`https://app.example.com`),
  `http://localhost:3000` for local development, and a native custom scheme
  (`myapp://connected`). Anything else — a plain `http://` origin other than localhost, or
  something that is not an origin at all — is ignored on the links this endpoint creates:
  the link still works, it simply sends no events. It is optional, so omitting it is not an
  error either.

  Of the three, only the first two receive events. A custom scheme is a return target for
  a mobile app and **cannot receive them**, because there is no browser window to post to;
  native apps poll [Get a Link Session](/docs/apis/profiles/get-link-session) instead.

  See [Link Completion Events](/docs/multiple-users/link-completion-events).

  **In connect mode `origin` is required, and it is checked.** The leniency above is grid-mode
  behaviour. With `mode: "connect"`, omitting it returns `code: 505` and a value that is not one
  of the three accepted shapes returns `code: 506`, whose `details` repeat the shape you sent.
</ParamField>

<ParamField body="domain" type="string">
  Optional. Your linking domain, when your account has more than one. When omitted, your
  account's own domain is used. A domain not registered to your account is rejected.
</ParamField>

<ParamField body="email" type="object">
  Send a Connect Accounts email carrying the link, so your user can reach their linking
  page directly. Requires a `to` address.

  Requires the Max Pack. The response reports the outcome in `emailSent`, and a send
  failure returns `code: 333` rather than a success response.

  See [Connect Accounts Email](/docs/apis/profiles/social-linking-overview#connect-accounts-email).
</ParamField>

<h2 id="connect-mode">
  Connect Mode
</h2>

`mode: "connect"` creates a session for a linking surface you host yourself, rather than for the
hosted linking page. Which of the two connect shapes you get depends on one thing — whether you
pass `network`:

| You send | You get back | What you do with it |
| - | - | - |
| `mode: "connect"` and a `network` | a `url` pointing at a single-network connect page | open it in a popup — this is [direct mode](/docs/multiple-users/connect-direct-mode) |
| `mode: "connect"` and no `network` | a `token`, and no URL at all | hand it to your own front end |

<Note>
  **The response carries the secret exactly once.** A response has either a `url` or a `token`,
  never both and never two URLs. The token in a direct-mode session lives inside the `url`, the
  same way it does in grid mode; a session with no URL to carry it returns the bare `token`
  instead. Everything else is the same in all three modes: `sessionId`, `expiresAt`, `emailSent`,
  and `title` when the User Profile has one.
</Note>

### What Connect Mode Requires

Neither of these is a body field of its own — the first is an account entitlement and the second is
the [`origin`](#body-parameters) parameter above, which connect mode makes mandatory.

**The [Max Pack](/docs/additional/maxpack).** Without it the call returns `code: 504`, checked before
the connect-mode parameters, so correcting `origin` or `network` will not change the answer. Contact support if you
need connect mode enabled on an account without the Max Pack.

**An `origin`, on every session.** There is no allowlist and no registration step — you send it on
each call and it is stored on the session, so a new environment needs no setup on our side. Three
shapes are accepted:

* an `https` origin — `https://app.example.com`
* a native custom scheme — `myapp://connected`
* `http://localhost` or `http://localhost:3000`, for local development

Origin only: no path, query or fragment, and no credentials in it. Omitting it returns `code: 505`,
and anything that is not one of the three shapes returns `code: 506`.

<Warning>
  `email` cannot be used with a session that has no `network`, because there is no link to put in
  the email — that shape returns a token for your own front end. The call returns `code: 510`.
  Add a `network` for a direct-mode session, which does have a URL, or omit `email`.
</Warning>

Every code named above is in the [Link Session Errors](/docs/errors/errors-ayrshare#link-session-errors)
reference.

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

  ```javascript JavaScript theme={"system"}
  const API_KEY = "API_KEY";
  const PROFILE_KEY = "PROFILE_KEY";

  fetch("https://api.ayrshare.com/api/profiles/link-sessions", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${API_KEY}`,
      "Profile-Key": PROFILE_KEY,
    },
    body: JSON.stringify({ expiresIn: 60 }),
  })
    .then((res) => res.json())
    .then((json) => console.log(json))
    .catch(console.error);
  ```

  ```python Python theme={"system"}
  import requests

  payload = {'expiresIn': 60}
  headers = {'Content-Type': 'application/json',
          'Authorization': 'Bearer API_KEY',
          'Profile-Key': 'PROFILE_KEY'}

  response = requests.post('https://api.ayrshare.com/api/profiles/link-sessions',
                           json=payload, headers=headers)
  print(response.json())
  ```

  ```php PHP theme={"system"}
  <?php
  require 'vendor/autoload.php';    // Composer auto-loader using Guzzle. See .../guzzlephp.org/en/stable/overview.html

  $client = new GuzzleHttp\Client();
  $res = $client->request(
      'POST',
      'https://api.ayrshare.com/api/profiles/link-sessions',
      [
          'headers' => [
              'Content-Type'  => 'application/json',
              'Authorization' => 'Bearer API_KEY',
              'Profile-Key'   => 'PROFILE_KEY'
          ],
          'json' => [
              'expiresIn' => 60,
          ]
      ]
  );

  echo json_encode(json_decode($res->getBody()), JSON_PRETTY_PRINT);
  ```

  ```csharp C# theme={"system"}
  using System;
  using System.Net.Http;
  using System.Text;
  using System.Threading.Tasks;
  using Newtonsoft.Json;

  namespace CreateLinkSession_csharp
  {
    class CreateLinkSession
    {
        static async Task Main(string[] args)
        {
            string API_KEY = "API_KEY";
            string PROFILE_KEY = "PROFILE_KEY";
            string url = "https://api.ayrshare.com/api/profiles/link-sessions";

            try
            {
                using (var client = new HttpClient())
                {
                    client.DefaultRequestHeaders.Add("Authorization", "Bearer " + API_KEY);
                    client.DefaultRequestHeaders.Add("Profile-Key", PROFILE_KEY);

                    var sendData = new { expiresIn = 60 };
                    string jsonData = JsonConvert.SerializeObject(sendData);
                    var content = new StringContent(jsonData, Encoding.UTF8, "application/json");

                    HttpResponseMessage response = await client.PostAsync(url, content);
                    response.EnsureSuccessStatusCode();

                    string responseBody = await response.Content.ReadAsStringAsync();
                    Console.WriteLine(responseBody);
                }
            }
            catch (HttpRequestException e)
            {
                Console.WriteLine($"HTTP request error: {e.Message}");
            }
        }
    }
  }
  ```
</RequestExample>

<ResponseExample>
  ```json 200 theme={"system"}
  {
      "status": "success",
      "sessionId": "c7a2434e72e91bde27579efde0fd6dd0b74ceee29a471fd368407f273708c2e8",  // Identifier for this link. Use it with Get and Revoke a Link Session.
      "url": "https://profile.ayrshare.com?session=ayr_ls_kJ8mQ2vX9pLnR4tYwZ6aBcD1eFgH3iJkLmN0oPqRsTu&domain=YOUR_DOMAIN",  // Send this to your user exactly as returned. The token exists only in here.
      "expiresAt": "2026-09-02T08:03:26.838Z",  // When the link stops working, as an ISO 8601 timestamp.
      "emailSent": false,  // Whether the connect-accounts email was sent. false means none was requested; a send failure returns code: 333 instead.
      "title": "Acme Client"  // The User Profile's title. Omitted when the profile has none.
  }
  ```

  ```json 200: Direct Mode (mode: "connect" with a network) theme={"system"}
  {
      "status": "success",
      "sessionId": "c7a2434e72e91bde27579efde0fd6dd0b74ceee29a471fd368407f273708c2e8",
      "url": "https://profile.ayrshare.com/connect?session=ayr_ls_kJ8mQ2vX9pLnR4tYwZ6aBcD1eFgH3iJkLmN0oPqRsTu",  // Open this in a popup. One network, no domain parameter.
      "expiresAt": "2026-09-02T08:03:26.838Z",
      "emailSent": false,
      "title": "Acme Client"
  }
  ```

  ```json 200: Connect Mode Without a Network theme={"system"}
  {
      "status": "success",
      "sessionId": "c7a2434e72e91bde27579efde0fd6dd0b74ceee29a471fd368407f273708c2e8",
      "token": "ayr_ls_kJ8mQ2vX9pLnR4tYwZ6aBcD1eFgH3iJkLmN0oPqRsTu",  // No url: this shape returns the bare token instead. Treat it like a password.
      "expiresAt": "2026-09-02T08:03:26.838Z",
      "emailSent": false,  // Always false here - email needs a link to send, so it returns code: 510.
      "title": "Acme Client"
  }
  ```

  ```json 401: Connect Mode Requires the Max Pack theme={"system"}
  {
    "action": "link session",
    "status": "error",
    "code": 504,
    "message": "Connect mode requires the Max Pack. Activate it on your Account page: https://app.ayrshare.com/account"
  }
  ```

  ```json 400: Missing Profile-Key Header theme={"system"}
  {
    "action": "JWT",
    "status": "error",
    "code": 188,
    "message": "Missing or incorrect privateKey, profileKey, domain, email 'to', or expiresIn fields."
  }
  ```

  ```json 400: Domain Not Registered to Your Account theme={"system"}
  {
    "action": "JWT",
    "status": "error",
    "code": 189,
    "message": "Error generating JWT. Check the sent parameters.",
    "details": "Missing or incorrect domain."
  }
  ```

  ```json 403: expiresIn Requires the Max Pack theme={"system"}
  {
    "action": "JWT",
    "status": "error",
    "code": 340,
    "message": "Max Pack required. Go to your dashboard to add the Max Pack."
  }
  ```
</ResponseExample>
