Build an OnlyFans Analytics Dashboard
This page is for whoever has to build the collector behind the screen. Start with what does not exist, because it decides the product: there is no statistics endpoint, no earnings endpoint and no transactions endpoint on this API, and every read returns current state rather than a period. An OnlyFans analytics dashboard on ofapis therefore has three sources: counters and list pages pulled on a schedule (GET /accounts/{id}/me, /subscribers, /chats), the GET /accounts/{id}/notifications feed, and the log of calls your own worker made. Anything with a time axis on it is computed in your database, from rows you stored, by code you wrote.
That has one consequence worth putting in the product itself, not in a footnote: nothing here reconciles to an OnlyFans payout. The platform's own financial figures are not exposed over an API, so any money tile you render is a reconstruction from message prices and event counts. Label it as one. The narrower money build is covered on revenue analytics; this page is about the dashboard as a whole.
Tiles that compute, and tiles that do not
| Tile | Input | Computable |
|---|---|---|
| Account counters: subscribers, posts, photos, videos | /accounts/{id}/me |
Yes — five numbers for one credit |
| Active subscriber count | /subscribers?type=active, paged to exhaustion |
Yes — you count the pages yourself |
| Daily net growth | Two consecutive snapshots | Yes, from the day your collector starts |
| Churned fans (who left) | Set-diff of subscriber ids between snapshots |
Yes |
| New-subscriber events | /notifications?type=subscribed |
Yes, within the feed window |
| Unread activity badge | /notifications/count |
Yes — the cheapest tile in the product |
| Reply latency | Your send log joined to /chats/{chatId}/messages createdAt |
Yes — you own both timestamps |
| Collab mentions | /mentions (release-form mentions) |
Yes, paginated |
| Earnings this month | — | No endpoint exists |
| Per-fan lifetime spend | — | Not from the API. A subscriber record is {id, username, name, avatar, subscribedOn} and carries no spend field; a per-fan figure only exists if you accumulate message rows yourself |
| Subscriber cohorts by signup date | — | Not from the API. subscribedOn is a boolean, not a date; cohorts accrue only from events you store |
| Post impressions, views, link clicks | — | No endpoint exists |
The cohort row catches teams out. Because subscribedOn is a flag rather than a timestamp, you cannot ask the API when someone subscribed. The only signal is the subscribed event in the notifications feed, so cohort history accrues forward from your first collector run and can never be backfilled. The spend row has the same shape in a different place: nothing per fan is cumulative, which is why it pays to know what a per-fan read does and does not return before designing a tile around it.
A subscriber snapshot is a page walk, not a number
The response is {"data":{"list":[...],"hasMore":true,"nextOffset":20}}. There is no total. limit defaults to 20 and the server clamps it to 1–100, so counting 4,000 active fans costs forty calls at forty credits. Walk it with limit=100 or you quadruple the bill for nothing.
import os, requests
BASE = "https://api.ofapis.com/api/public/v1"
H = {"Authorization": f"Bearer {os.environ['OFAPIS_KEY']}"}
def snapshot_subscribers(account_id, sub_type="active"):
ids, offset = set(), 0
while True:
r = requests.get(f"{BASE}/accounts/{account_id}/subscribers", headers=H,
params={"type": sub_type, "limit": 100, "offset": offset})
r.raise_for_status()
page = r.json()["data"]
ids.update(u["id"] for u in page["list"])
if not page["hasMore"]:
return ids
offset = page["nextOffset"]
Store the id set, not just its cardinality. The count gives you the growth line; the set makes churn answerable — yesterday's ids minus today's ids is the list of fans who left, with usernames attached. Field detail is on the subscribers API reference. It is also the same snapshot table a fan CRM keeps, so if both are on the roadmap, collect once and read twice.
The notifications feed is a head window
This is the sharpest constraint in the build. GET /notifications accepts type and limit (clamped 1–100) and nothing else — no offset, no cursor, no since parameter. You cannot page backwards through it. Every call returns the newest items, and anything that scrolled past the hundredth position is gone from the API's point of view.
So the collector's job is to run often enough that fewer than 100 events accumulate between runs. A creator generating 500 events a day averages about 21 an hour, which fills a 100-item window in under five hours; an hourly pull leaves roughly five times headroom, a four-hourly one leaves none. Size the interval from the busiest creator on the roster, not the median.
The payload is forwarded verbatim from OnlyFans, so read fields defensively. Documented items look like {"id":"abc123","type":"subscribed","text":"subscribed to you","createdAt":"..."} alongside a hasMore flag. Consecutive pulls overlap by design, so upsert on id and let the primary key absorb the duplicates. The notifications API page covers the type filters.
Count-first refresh
Do not poll the feed itself. Poll GET /notifications/count, whose documented shape is {"newCount": 3}, and pull the full feed only when that number moves. Both calls cost one credit, but the counter is a single request while the feed pull is a request plus whatever your fan-out does afterwards.
The cadence is a budget decision. On Pro — $159, 120,000 credits a month, five linked accounts, 500 requests per minute per token — a five-minute counter poll is 288 calls per creator per day, 8,640 a month, 43,200 across five creators: 36% of the plan's credits spent on a badge. At fifteen minutes the same five cost 14,400. Give each account the interval it needs rather than polling the whole roster at the fastest rate.
For events you genuinely want on screen within seconds, do not poll at all. Webhooks push message.received — the only event type that exists today — and cost no credits to receive. Route multi-creator deliveries on data.message.toUser.id, because the body carries no account id.
A schema that stays cheap at five creators
Three tables carry the whole dashboard. Keep raw rows and compute on read; store pre-aggregated metrics instead and every definition change becomes a backfill you cannot run, because the source data is gone.
CREATE TABLE snapshot ( -- one row per account per collector run
account_id BIGINT NOT NULL,
captured_at TIMESTAMPTZ NOT NULL,
active_count INT NOT NULL,
PRIMARY KEY (account_id, captured_at)
);
CREATE TABLE subscriber_state ( -- current membership; churn is a diff of last_seen
account_id BIGINT NOT NULL,
fan_id BIGINT NOT NULL,
username TEXT,
first_seen TIMESTAMPTZ NOT NULL,
last_seen TIMESTAMPTZ NOT NULL,
PRIMARY KEY (account_id, fan_id)
);
CREATE TABLE activity_event ( -- notifications, deduped by the feed's own id
account_id BIGINT NOT NULL,
notif_id TEXT NOT NULL,
type TEXT NOT NULL,
occurred_at TIMESTAMPTZ NOT NULL,
payload JSONB NOT NULL,
PRIMARY KEY (account_id, notif_id)
);
account_id is the first column of every key because the roster view is the common case, not the exception. One token addresses every linked account through the /accounts/{accountId}/... prefix, so the collector is one function called in a loop — the same shape described under agency automation and on the statistics API page.
Failures that cost money
Credits are debited before the handler runs, and only some failures give them back. Refunded: any 5xx, a 424 expired OnlyFans session, and the 409 raised when the account is not active. Free: 401, 402, 403 and 429, which the gateway refuses before it meters anything. Charged and not refunded: 400, 404, 413, 415 and 422. That last one matters most to a collector, because 422 is what you get when the account is not linked or its OnlyFans authorization is invalid — a job pointed at a half-configured account pays full price for every poll it makes.
Two guards handle it. Treat a 400 as a code bug and a 422 as a configuration alarm: stop retrying either one, because neither will start succeeding on its own. And read X-RateLimit-Remaining off every response, not just the 429s, so the worker slows down before it hits the per-minute ceiling: 60 on Free, 100 on Starter, 500 on Pro. See rate limits and pricing.
FAQ
Can the dashboard show historical data from before I started collecting?
No. The API exposes current state and a short notifications window, so there is no backfill path for growth, churn or cohorts. History begins at your first collector run, which is the single strongest argument for deploying a minimal collector early and building the charts afterwards.
Why does my subscriber count differ from the OnlyFans web UI?
Because you are counting a paginated list at a moment in time while the UI reads the platform's own aggregate, and subscriptions expire continuously between your pages. Treat the snapshot as accurate to within a page, pass type=active consistently, and compare day-over-day deltas rather than absolute parity with the web figure.
How often should the collector run?
Run the subscriber page walk once a day — it is the expensive job and growth is a daily metric anyway. Run the /notifications/count check on a timer short enough that your busiest account never accumulates 100 unread events between pulls, and pull the full feed only when the counter changes.
Does one dashboard cover a whole roster?
Yes, and it is the same code. Every read has an /accounts/{accountId}/ variant under a single Bearer token, so the collector takes an account id as an argument and the roster view is a GROUP BY account_id. The account ceiling is the plan's, not the API's: one on Free and Starter, five on Pro. Where a roster view sits in the rest of the stack — who reads it, and which numbers a chatter shift is actually managed against — is worked through on running an OnlyFans agency.
Can I put a revenue tile on this dashboard?
Only an estimate, clearly labelled. There is no earnings or transactions endpoint, so the number has to be assembled from message price fields and event counts, and it will not match a payout statement after platform fees, refunds and chargebacks. Read what a revenue tile can honestly say before you ship one, then get a token and run a single subscriber walk — the page count it returns is the real size of your collector's daily bill.