BetterFans Link: the OnlyFans APIBetterFans Link

API quickstart

Create a test key, make your first call, read the response envelope and page through a list.

This takes about five minutes. You need a BetterFans Link workspace and a terminal. Nothing here touches a real OnlyFans account: a test key reads the sandbox creators.

Create a test key

  1. Sign up or sign in and open your workspace.
  2. Turn on Test mode with the switch in the header.
  3. Open Developers, then API keys, and create a key with the read scope. Add write too if you want to try actions later.
  4. Copy the key. It starts with bfl_test_ and is shown once.

Keep the key in an environment variable, never in code:

export BFL_KEY="paste-your-bfl_test_-key-here"

Make your first call

GET /v1/me tells you which workspace and key you are using.

curl https://app.betterfans.link/v1/me \
  -H "Authorization: Bearer $BFL_KEY"

The response names your workspace, and key.mode is test. See Who am I for every field.

List the sandbox creators

Every account route starts with an account id. List the accounts your key can use:

curl https://app.betterfans.link/v1/accounts \
  -H "Authorization: Bearer $BFL_KEY"

With a test key this returns the sandbox creators. Pick one and copy its id. An account id is the creator's OnlyFans user id, as a string.

export ACCOUNT_ID="paste-an-id-from-the-list"

Read the envelope

Ask for the account's top fans by spend:

curl "https://app.betterfans.link/v1/accounts/$ACCOUNT_ID/fans?sort=spend&limit=3" \
  -H "Authorization: Bearer $BFL_KEY"

Every successful response has the same shape. A list looks like this, trimmed:

{
  "data": [
    { "id": "38291045", "username": "mike_travels", "spend": { "total": { "amount": 184250, "currency": "USD" } } }
  ],
  "hasMore": true,
  "nextCursor": "q8ZtR2vN5xWcL7mK4pBd",
  "meta": { "source": "sandbox", "asOf": "2026-09-28T13:58:40Z", "requestId": "req_7Hq2LmX9pRt4VbN8cKe3WzYa" }
}
  • data holds the items. A single object route returns one object in data and no hasMore.
  • meta.source says where the data came from: sandbox in test mode, synced for synced data, live for a read straight from OnlyFans.
  • meta.asOf is when the data was last synced. See synced and live reads.
  • Money is integer cents. 184250 is $1,842.50. See money.

Page through a list

When hasMore is true, pass nextCursor back as cursor and keep every other parameter the same. limit is 25 by default. A larger limit than 100 is treated as 100.

let cursor: string | null = null;
do {
  const url = new URL(`https://app.betterfans.link/v1/accounts/${process.env.ACCOUNT_ID}/fans`);
  url.searchParams.set("limit", "100");
  if (cursor) url.searchParams.set("cursor", cursor);
  const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.BFL_KEY}` } });
  const body = await res.json();
  if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
  for (const fan of body.data) console.log(fan.username, fan.spend.total.amount);
  cursor = body.hasMore ? body.nextCursor : null;
} while (cursor);

Handle errors

A failed call returns an HTTP status and an error object. Branch on error.code, which is stable, and show error.message to people:

curl -i https://app.betterfans.link/v1/accounts/1/fans -H "Authorization: Bearer $BFL_KEY"
{
  "error": {
    "type": "not_found",
    "code": "account_not_found",
    "message": "No account with this id is linked to your workspace.",
    "hint": "List your accounts with GET /v1/accounts.",
    "docsUrl": "https://app.betterfans.link/docs/errors#account-not-found"
  },
  "requestId": "req_7Hq2LmX9pRt4VbN8cKe3WzYa"
}

Every code has an entry on the errors page, and error.docsUrl links straight to it.

Next steps

On this page