> ## 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 Completion Events

> Get told when your user connects an account, instead of polling for it.

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

When you open a linking page in a popup, your own page can listen for what happens in it:
your user connected Reddit, they backed out, the connection failed. You get an event per
outcome, so your UI updates the moment it happens rather than on a timer.

Set [`origin`](/docs/apis/profiles/create-link-session) when you create the link, open the
returned `url` in a popup, and listen. Nothing else is required, and nothing changes for
links created without `origin` — they behave exactly as they always have.

<Note>
  **Using the [embedded widget](/docs/multiple-users/connect-widget)? This is already wired up for you.**
  The script receives these events and hands them to `connect.on("success", …)` with the
  `connect:` prefix stripped — no `message` listener, no origin check, no `popup.closed` poll to
  write. This page is the wire protocol underneath, and what you build against when **you** own the
  window: direct mode, or a popup you open yourself.
</Note>

<Note>
  Events are sent only to the exact `origin` you set on the link, and only to the window
  that opened the popup. Always check `event.origin` in your listener anyway: any page can
  post a message to your window, and the origin is the only part of a message that cannot
  be faked.
</Note>

## The Events

Every message posted to your page is an object shaped
`{ source: "ayrshare", version: 1, event, ... }`, and carries `network` whenever the event is
about one. Two events are not: a `connect:closed` from the hosted linking page, where it means your
user pressed Done rather than that one connection ended, and the widget's `connect:ready` (see
below). The outcomes your own code synthesizes — a blocked popup, or a window your user closed — do
not come from us and carry only what you give them.

<Note>
  The [embedded widget](/docs/multiple-users/connect-widget) differs on one event. Its `ready` announces
  that a slot is up rather than anything about a network, so it carries no `network` — whereas the
  popup's `ready` names the network it was opened for. If you handle both surfaces from one
  listener, read `network` defensively on `ready`.

  The widget also adds one `cancelled` reason this surface never sends: `superseded`, when a second
  `popup()` call replaces an attempt still in flight. Here there is one window and one outcome, so
  there is nothing to supersede.
</Note>

| Event               | Extra fields                            | Sent when                                                                                                                                                                                                      |
| ------------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `connect:ready`     |                                         | The page has loaded and the link has been checked.                                                                                                                                                             |
| `connect:started`   |                                         | Your user has been sent to the social network.                                                                                                                                                                 |
| `connect:selection` | `step`                                  | A picker or a form is on screen — your user has something to do.                                                                                                                                               |
| `connect:success`   | `refId`, `displayName`                  | The account is connected **and saved**. `refId` is the User Profile it was connected to. `displayName` is the account name, and is omitted when we do not have one yet.                                        |
| `connect:error`     | `message`, and `code` when there is one | The connection failed. `code` matches the [error reference](/docs/errors/overview); it is absent when the failure has no catalogued code, and on any outcome your own snippet synthesizes, such as a blocked popup. |
| `connect:cancelled` | `reason`                                | Your user backed out. `reason` is `popupClosed`, `scopesDenied`, or `userCancelled`.                                                                                                                           |
| `connect:closed`    |                                         | The popup is about to close itself. Carries no `network` when it comes from the hosted linking page, where it means your user pressed Done rather than that one connection ended.                              |

Exactly one of `connect:success`, `connect:error` or `connect:cancelled` arrives per
connection. `connect:closed` is not one of them — it follows, to tell you the popup closed
on purpose.

`connect:success` is sent only after the account has been saved, so a
[`GET /user`](/docs/apis/user/profile-details) immediately after it already shows the connected account.

<Note>
  The popup stays open after `connect:error` so your user can read what went wrong. It
  closes itself after `connect:success` and `connect:cancelled`. Add `&autoClose=false` to
  the URL to keep it open in every case while you are debugging.
</Note>

## Listening

Two things this snippet does that are easy to leave out. It checks `event.origin`, and it
watches for a popup your user closed by hand — a closed window cannot send anything, so
polling is the only way to notice.

```javascript theme={"system"}
function connectAccount(url) {
  // Derive the origin from the URL you were given rather than hard-coding one:
  // if your account uses its own linking domain, the popup runs on that.
  const popupOrigin = new URL(url).origin;

  const popup = window.open(url, "ayrshare-connect", "width=600,height=800");
  if (!popup) {
    handleOutcome({ event: "connect:error", message: "The popup was blocked." });
    return;
  }

  // Declared before anything can call `cleanup`: a message arriving early would
  // otherwise hit `poll` in its temporal dead zone and throw.
  let poll;

  const cleanup = () => {
    clearInterval(poll);
    window.removeEventListener("message", onMessage);
  };

  const onMessage = event => {
    // Both checks, not just the origin: another tab or frame on the same origin
    // could otherwise post a message your handler would believe.
    if (event.origin !== popupOrigin || event.source !== popup) return;
    const message = event.data;
    if (!message || message.source !== "ayrshare") return;

    if (["connect:success", "connect:error", "connect:cancelled"].includes(message.event)) {
      // `finally`, so your own handler throwing cannot leave the poll running —
      // it would later see the closed popup and report `cancelled` on top of the
      // outcome you already had. Cleanup also matters after `connect:error`,
      // where the popup stays open so your user can read it.
      try {
        handleOutcome(message);
      } finally {
        cleanup();
      }
    }
  };
  window.addEventListener("message", onMessage);

  // A hand-closed popup sends nothing, so watch for it. Wait a moment before
  // deciding: the popup closes itself right after sending, and the message can
  // still be in flight when you first see the window go.
  let closedAt = null;
  poll = setInterval(() => {
    if (!popup.closed) return;
    if (closedAt === null) {
      closedAt = Date.now();
      return;
    }
    if (Date.now() - closedAt < 750) return;

    cleanup();
    handleOutcome({ event: "connect:cancelled", reason: "popupClosed" });
  }, 500);
}
```

## Errors

`connect:error` carries the same codes as the rest of the API, so a code you see here means
what it means everywhere else. The one specific to this surface:

| Code  | Meaning                                                                                                                                                                                                  |
| ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `513` | The link was opened as a single-network connect window, but was not created for one. Only reachable if you build that URL yourself — the `url` this endpoint returns always matches the link it created. |

Anything else is the social network's own failure, reported with whatever code that
failure already has — for example `322` for an Instagram authorization problem, which
includes an account that is still Personal rather than Professional.

### A dead link arrives differently

A link that is **expired**, **revoked** or **unknown** is refused before the page can learn
where to send events, so it cannot send one. Your user sees the reason and its code on
screen, and your page hears `connect:cancelled` when they close the window.

To tell those apart, poll [Get a Link Session](/docs/apis/profiles/get-link-session): it reports
`expired` and `revoked` authoritatively, and returns `502` for a link that does not exist.

## If You Would Rather Not Manage a Popup

Two options, and only the second gives up events.

**Let the widget own the window.** The [embedded widget](/docs/multiple-users/connect-widget) opens and
watches it for you and delivers every event above to your handlers. You still get `success` the
moment an account is saved; you just do not write the plumbing. Its frames also mean most networks
never open a popup at all until the network's own login.

**Poll instead.** If a popup is genuinely not available — a server-rendered app, or a mobile app
opening the link in the system browser — poll
[Get a Link Session](/docs/apis/profiles/get-link-session). It reports `completedAt` and
`completedNetworks` as soon as an account is saved, which is the same moment `connect:success`
would have been sent. Telegram always finishes this way, since it completes out of band with no
browser callback at all.
