Open the link in your app
Open a hosted link in a popup or over your page with one script, follow each step the creator takes, and confirm the result on your server.
Product page: OnlyFans account linking
The connect script opens a hosted link from inside your own app. The creator signs in, types any two-factor code and takes any selfie check in the browser, in a popup or in a dialog over your page. Your page hears each step as it happens, and how the link ended.
It is one file of plain JavaScript with no dependencies, at https://betterfans.link/connect/v1.js, with types at v1.d.ts next to it. It works the same from React, Vue, Svelte or a page with no build step.
To see the whole flow first, open the sample link. It runs the real page on sample data, with no account behind it: sign in with any details, any code works, and 000000 shows a wrong one.
Make the link on your server
Your API key stays on your server. Add a route that calls Create link and returns the response as it is. The script reads the link from data.
export async function POST() {
// Check here that the signed-in user may link an account.
const res = await fetch("https://betterfans.link/v1/links", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.BFL_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ note: "From the onboarding page" }),
});
return Response.json(await res.json(), { status: res.status });
}Open it
Call connect from a click or a key press. Browsers block popups that a page opens any other way.
import { connect } from "https://betterfans.link/connect/v1.js";
const result = await connect({
link: () => fetch("/api/creator-link", { method: "POST" }).then((r) => r.json()),
display: "popup",
onEvent: (event) => console.log(event.type, event.step ?? ""),
});
if (result.status === "connected") {
console.log("Linked", result.account.username);
}link takes the link's url, the HostedLink, the Create link response, or a function that returns any of them. Pass a function when the link is made on click: the popup opens at once and shows a short wait while your server answers.
display is where the link opens.
display | Where | Use it for |
|---|---|---|
popup | A small window over yours. The default. | Desktop browsers. |
modal | A dialog over your page, with the link in a frame. Escape or the close button closes it. | Phones, where a popup opens as a new tab, and apps that keep to one window. |
If the browser blocks the popup anyway, the link opens as a dialog instead, and result.display says modal.
connect returns a promise that settles once, when the link closes. Pass a signal from an AbortController to close it from your side; the promise then rejects with the abort reason. Opening a second link closes the first.
Follow the steps
onEvent gets every event as it happens. Each carries linkId when you passed the link object or the Create link response, and null when you passed a URL.
| Event | When | Fields |
|---|---|---|
ready | The link page loaded. | display |
step | The creator moved to a new screen. | step, platform |
connected | The account is linked. The popup or dialog closes by itself a moment later. | platform, account with username, name and avatarUrl |
failed | The sign-in ended without linking. | message, in words for the creator |
expired | The link is past its 24 hours. | |
invalid | The link cannot be opened, for example because it was cancelled. | |
closed | The link closed. Always the last event, with the same fields as the result. | status, display, step, platform, account, message |
step is the screen the creator is on.
step | The creator is |
|---|---|
choose_platform | Picking OnlyFans or Fansly. |
credentials | Typing their sign-in details. |
working | Waiting while the platform answers. |
captcha | Answering the platform's human check. |
choose_method | Choosing how to confirm it is them: a code by app, text or email, or the selfie check. |
otp | Typing a two-factor code. |
face | Taking the OnlyFans selfie check in their phone's browser. |
connected, failed, expired | Done. |
The result's status is connected, failed, expired, invalid, or closed when the creator closed it before the end.
Confirm on your server
Events come from a page in the creator's browser. Use them to move your interface along, and confirm a new account on your server before you act on it: the account.connected webhook, or accountId from Get link. See follow progress.
Without a build step
The script also defines a <betterfans-connect> button.
<script type="module" src="https://betterfans.link/connect/v1.js"></script>
<betterfans-connect endpoint="/api/creator-link" display="modal">
Connect your account
</betterfans-connect>
<script>
const button = document.querySelector("betterfans-connect");
button.addEventListener("step", (e) => console.log(e.detail.step));
button.addEventListener("connected", (e) => console.log(e.detail.account));
</script>On a click it sends a POST to endpoint, with your cookies when it is on your own site, and opens the link in the response. Set link to a hosted link URL instead when you already have one. Each event above fires on the element as a DOM event with the event in detail, and error fires with the error when the link cannot open. Style the button with ::part(button), or with the --betterfans-connect-background and --betterfans-connect-color custom properties.
In React
Copy v1.js and v1.d.ts into your app, or import from the URL if your bundler allows it.
import { useState } from "react";
import { connect } from "./betterfans/v1.js";
export function ConnectButton() {
const [step, setStep] = useState("");
const open = () =>
connect({
link: () => fetch("/api/creator-link", { method: "POST" }).then((r) => r.json()),
display: "modal",
onEvent: (event) => event.type === "step" && setStep(event.step),
}).then((result) => setStep(result.status));
return <button onClick={open}>Connect account {step}</button>;
}For agents
An agent does not need a page to link an account. Over MCP, link_account makes the link and returns its url and linkId. The agent sends the url to the creator however your team works, and get_link or the account.connected webhook says when the creator is done. Use the connect script when the link should open inside your own interface.
Keep it in your repo
v1.js changes only in ways that keep working with code written for it. A change that would break it ships as a new file. You can also copy v1.js and v1.d.ts into your repo and serve them yourself.
Or skip the script and use the messages it reads. Add two search parameters to the link's url: embed set to popup or modal, and origin set to your page's origin. The link page then posts each change with postMessage to that exact origin, and to no other.
{ "source": "betterfans-connect", "v": 1, "type": "step", "step": "otp", "platform": "onlyfans" }type is ready, step, connected, failed, expired, invalid or close. Check that event.origin is https://betterfans.link before you read a message. The page only sends; it does nothing with messages sent to it. In modal, the page lets only that origin show it in a frame.
If your page sets security headers
- Cross-Origin-Opener-Policy. With
same-origin, the browser cuts the popup off from your page and the script cannot hear it. Usesame-origin-allow-popups, ordisplay: "modal". The script warns in the console when this happens. - Content-Security-Policy. Allow
https://betterfans.linkinscript-srcwhen you load the script from us, and inframe-srcformodal.