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

# 社交关联概览

> 你的用户如何关联他们的社交账号——三种界面、支撑所有界面的 link session，以及它们共享的选项。

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

你的用户自己关联他们的社交账号——他们直接在每个社交网络上完成身份验证，你永远不会看到或存储他们的凭据。本页的全部内容，就是如何把他们带到那个时刻，以及如何得知结果。

所有路径都有一个共同点：一个 **link session（关联会话）**，通过[创建 Link Session](/docs/apis/profiles/create-link-session)、使用你的 API Key 和 `Profile-Key` 创建。你这一侧不需要签名任何内容，流程中也没有 private key。

## 三种关联方式

三种形态，且前两种是同一个集成。请按流程选择——它们并不互斥，很多集成在接入阶段使用托管页面，之后在应用内使用小组件。

| 界面                                                                                                  | 你的用户看到的                         | 白标程度                            | 适用场景                                            |
| --------------------------------------------------------------------------------------------------- | ------------------------------- | ------------------------------- | ----------------------------------------------- |
| **小组件——嵌入式框架**（[mount](/docs/multiple-users/connect-widget#mount-a-slot)）                                | 我们的按钮内联显示在你自己的布局中；除网络自身的弹窗外没有弹窗 | **最强。** 你的页面、你的字体和配色，你的用户从不离开它  | 你的控制台为每个网络设有一行，希望关联就地完成。需要 Max Pack。            |
| **小组件——你自己的按钮**（[popup](/docs/multiple-users/connect-widget#your-own-button)）                            | 你的按钮，然后是网络对应的一个弹窗               | **强。** 弹窗是我们的，但十分短暂，并继承你的外观     | 你想使用自己的按钮和样式，且布局中不出现框架。需要 Max Pack。             |
| **托管关联页面**（[使用方法](/docs/multiple-users/api-integration-business#single-sign-on-with-jwt-authentication)） | 我们托管的页面，带有你的 logo、颜色和自定义 CSS    | **最弱。** 这是我们的页面，你的用户要离开你的页面去使用它 | 你想要一个可以直接发出去的链接，或通过邮件接入用户。无需构建任何东西，无需 Max Pack。 |

<Note>
  两个小组件行是**同一个集成**，而不是两个。一次 `init` 同时给你两者：在需要我们按钮的地方挂载框架，在其他任何地方从你自己的按钮调用 `popup()`。它们共享同一个会话，并在同一组处理器上报告结果。

  [直连模式](/docs/multiple-users/connect-direct-mode)（Direct Mode）是**不带**我们脚本的同一个弹窗——适用于带有严格 Content-Security-Policy 的页面、服务器端渲染的页面或原生应用。在那里由你自己打开并监视弹窗。

  **拿不准？** 从托管关联页面开始。它不需要 Max Pack，也不需要额外参数，是最快跑通的路径——之后迁移到小组件也不会改变会话的创建方式。
</Note>

## 创建链接

在请求头中带上用户的 `Profile-Key`，调用[创建 Link Session](/docs/apis/profiles/create-link-session)。对托管页面来说，这就是完整的请求：

```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 '{}'
```

你会得到一个携带短期有效、不透明 token 的 `url`：

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

你还可以[查看链接是否已被打开](/docs/apis/profiles/get-link-session)，并在过期前[吊销它](/docs/apis/profiles/revoke-link-session)。

<Note>
  这段 1 分钟的视频演示了如何创建链接。它录制于 link session 之前，因此仍然演示了发送 Private
  Key——该步骤已不再需要，视频中展示的其他内容均保持不变。

  <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">
  发送链接 URL
</h3>

<Warning>
  链接 URL 会让你的用户登录到他们的 profile，因此请像对待密码一样对待它。通过你信任的渠道发送，不要
  记录到日志中，也不要转交给第三方。它在整个有效期内都可以使用，因此重新加载或重试 OAuth 都没问题——
  但每个链接只发给一位用户，并为每个人单独创建链接。
</Warning>

### 打开链接 URL

请在新的浏览器标签页、窗口或 View Controller 中打开它。你可以控制该窗口的
[关闭或跳转](/docs/multiple-users/api-integration-business#opening-and-closing-the-social-linking-url)。

<Note>
  社交网络不允许在 iFrame 中打开托管关联页面，也不允许对已获批准的合作伙伴来源
  `profile.ayrshare.com` 进行混淆处理。如果你希望关联发生在你自己的页面内部，
  [嵌入式小组件](/docs/multiple-users/connect-widget)正是为此而生：它的框架由 Ayrshare
  的源提供服务，是受支持的实现方式。
</Note>

### 得知何时完成

两种信号，任选其一：

* **[关联完成事件](/docs/multiple-users/link-completion-events)** —— 在创建链接时设置 `origin`，
  关联窗口会在事件发生时向你的页面发送 `connect:success`、`connect:error` 和
  `connect:cancelled`。无需轮询。
* **[获取 Link Session](/docs/apis/profiles/get-link-session)** —— 报告 `completedAt`、
  `lastCompletedAt` 和 `completedNetworks`。这是原生应用的信号，也是 Telegram 唯一的信号——
  它在带外（out of band）完成。

<h2 id="jwt-expires-in">
  链接有效期
</h2>

链接默认有效期为 **5 分钟**。之后请创建新的链接。

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

使用 Max Pack 时，可通过 `expiresIn`（以分钟为单位）延长该窗口——最长 **2880 分钟
（48 小时）**，这是 API 接受的最大值：

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

更长的窗口正是让[通过邮件发送链接](#connect-accounts-email)变得可行的原因——需要重新连接掉线账号
的用户可以直接从你的邮件进入社交网络，而无需先访问你的应用。

<Warning>
  请与你的安全团队评估链接应保持多长的有效期。窗口越长，被截获的链接可用的时间就越长。如果链接
  泄露，你可以[吊销它](/docs/apis/profiles/revoke-link-session)，而不必等它过期。
</Warning>

## Profile Key

`Profile-Key` 指明链接对应哪个 User Profile。在 Ayrshare 开发者控制台切换到该 profile 即可
找到它。

<Note>
  **Private Key 已不再使用。** 链接不再签名，因此无需从文件读取任何内容，也无需粘贴到代码里。
  旧的 `privateKey` 参数仍会被接受并忽略，所以现有集成继续工作，Integration Package 中的
  `private.key` 文件可以不再使用。
</Note>

## 切换 Profile

如果某个 profile 已经登录，打开另一个 profile 的链接不会切换 profile——这是有意为之，它让已经
在场的用户获得更快的体验。要强制切换，请参见
[自动登出 Profile 会话](/docs/multiple-users/api-integration-business#automatic-logout-of-a-profile-session)。

<h2 id="instagram-link-method">
  Instagram 关联方式
</h2>

Instagram 账户可以通过[两种方式](/docs/dashboard/connect-social-accounts/instagram)进行关联：直接使用
**Instagram Login**，或通过一个**已关联的 Facebook Page**。当用户点击 Instagram 按钮时启动哪种
流程，通常由账户级别的 [Instagram Login](/docs/multiple-users/manage-user-profiles#instagram-login)
设置决定。

`instagramLinkMethod` body 参数可为单个链接覆盖该设置：

| 值           | Instagram 关联流程                         |
| ----------- | -------------------------------------- |
| `instagram` | 直接使用 Instagram Login，无需 Facebook Page。 |
| `facebook`  | 通过已关联的 Facebook Page 进行关联。             |

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

该覆盖在链接的整个生命周期内生效，包括跨越 Instagram/Facebook 授权跳转。需要注意几点：

* 它不会更改账户级别的设置，也不会影响任何其他链接。
* 省略它时应用账户级别的设置，与以往行为完全一致。
* 非法值会返回 `400`，并列出合法值（`instagram`、`facebook`）。
* 在选择之前，请查看
  [功能差异](/docs/multiple-users/manage-user-profiles#direct-instagram-login-vs-facebook-page-authentication)
  ——某些 Instagram 功能（例如 hashtag 搜索和合作发布）仅在使用 Facebook Page 认证时可用。

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

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

Ayrshare 可以替你把链接通过邮件发送给用户，让他们无需访问你的应用即可进入自己的关联页面。请与
更长的 [`expiresIn`](#jwt-expires-in) 搭配使用——默认的 5 分钟很少能撑过收件箱。

### Connect Accounts JSON

**`email` 对象内的每个字段都是必填的。** 缺少任何一个都会导致发送失败。

```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` 是**顶层**参数，不属于 `email` 对象。嵌套在 `email` 内会被忽略，你的用户拿到的
  链接将在 5 分钟后过期。
</Warning>

响应会在 `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"
}
```

发送**失败**不会以 `emailSent: false` 的形式返回——而是返回 `code: 333`。因此 `false` 表示
没有请求发送邮件。

### Connect Accounts 邮件示例

以下是打开社交关联页面的邮件示例：

<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" />

邮件将从以下地址发出：

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

<h2 id="mobile-jwt">
  移动应用
</h2>

请在**系统浏览器**中打开链接 URL，绝不要使用嵌入式 webview：Google 会以 `disallowed_useragent`
拒绝登录，Meta 则直接屏蔽。你的用户会看到社交网络自己的错误页面，你这一侧无论怎么改都无济于事。

* **iOS** —— `ASWebAuthenticationSession` 或 `SFSafariViewController`。
* **Android** —— Chrome Custom Tabs。

由于原生应用没有可供发送事件的浏览器窗口，请改为通过
[获取 Link Session](/docs/apis/profiles/get-link-session) 获取结果。将 `origin` 设置为你的自定义
scheme（`myapp://connected`），让页面有办法回到你的应用。

<h3 id="mobile-code-examples">
  移动端代码示例
</h3>

请将 `linkingURL` 替换为[创建 Link Session](/docs/apis/profiles/create-link-session) 返回的 `url`。

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

## 测试

**建议**首先在 [Postman](/docs/testing/postman) 中创建链接。你的 Integration Package——位于控制台
Primary Profile 的 API Key 页面——包含一个示例 Postman 配置。导入它，在 `profileKey` *body*
字段中填入你的 Profile Key，然后点击 *Send*。

示例配置仍会预填 `privateKey` 和 `domain`。`privateKey` 会被忽略；除非你的账户拥有多个关联
域名，否则可以清空 `domain`。

你也可以[从 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 />

## 旧端点：generateJWT

<Info>
  [生成链接 URL](/docs/apis/profiles/generate-jwt)（`generateJWT`）完成同样的工作，并且**已弃用**——
  完全受支持、没有移除日期，你已经发出的链接也不会有任何变化。它自己的页面记录了它的参数，
  包括如今被接受并忽略的那三个。

  两个端点使用同一个校验器，因此本页的所有内容对两者都适用。迁移时唯一值得了解的差异：
  `generateJWT` 容忍三种「创建 Link Session」会拒绝的情况——`allowedSocial` 中无法识别的网络、
  只提供一半的 X 凭据、以及非字符串的 `redirect`。
</Info>
