BetterFans Link: the OnlyFans APIBetterFans Link

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.

app/api/creator-link/route.ts
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.

connect.js
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.

displayWhereUse it for
popupA small window over yours. The default.Desktop browsers.
modalA 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.

EventWhenFields
readyThe link page loaded.display
stepThe creator moved to a new screen.step, platform
connectedThe account is linked. The popup or dialog closes by itself a moment later.platform, account with username, name and avatarUrl
failedThe sign-in ended without linking.message, in words for the creator
expiredThe link is past its 24 hours.
invalidThe link cannot be opened, for example because it was cancelled.
closedThe 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.

stepThe creator is
choose_platformPicking OnlyFans or Fansly.
credentialsTyping their sign-in details.
workingWaiting while the platform answers.
captchaAnswering the platform's human check.
choose_methodChoosing how to confirm it is them: a code by app, text or email, or the selfie check.
otpTyping a two-factor code.
faceTaking the OnlyFans selfie check in their phone's browser.
connected, failed, expiredDone.

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.

index.html
<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.

connect-button.tsx
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.

A message from the link page
{ "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. Use same-origin-allow-popups, or display: "modal". The script warns in the console when this happens.
  • Content-Security-Policy. Allow https://betterfans.link in script-src when you load the script from us, and in frame-src for modal.

On this page