Make your first OnlyFans API call
Create a test key, make your first call, read the response envelope and page through a list.
Product page: OnlyFans agency API
This takes about five minutes. You need a BetterFans Link workspace and a terminal. Nothing here touches a real OnlyFans or Fansly 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. Read only is enough for this page. Pick Read and write with approval 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://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://betterfans.link/v1/accounts \
-H "Authorization: Bearer $BFL_KEY"With a test key this returns the sandbox creators. Pick one and copy its id. Its platform says whether it is on OnlyFans or Fansly. An account id is the creator's user id on that platform, 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://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", "platform": "onlyfans" }
}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 the platform.meta.platformnames the account's platform.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://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://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://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.