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
- Sign up or sign in and open your workspace.
- Turn on Test mode with the switch in the header.
- Open Developers, then API keys, and create a key with the
readscope. Addwritetoo if you want to try actions later. - 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" }
}dataholds the items. A single object route returns one object indataand nohasMore.meta.sourcesays where the data came from:sandboxin test mode,syncedfor synced data,livefor a read straight from OnlyFans.meta.asOfis when the data was last synced. See synced and live reads.- Money is integer cents.
184250is $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
- Try a write in test mode with writes and approvals.
- Get events pushed to you with webhooks.
- Link a real creator when you are ready with going live.