ofapisv1
capability

Updated

OnlyFans Statistics API

There is no /statistics route on this API. Nothing mirrors the Statistics tab in the OnlyFans web app, nothing returns a rolled-up period, and no endpoint hands back a time series. Every read is current state.

The solution is to snapshot on a schedule, store what you got, and compute periods yourself. Done properly that beats a vendor report, because you control the definitions and the retention. Done carelessly you get a number that silently disagrees with the creator's dashboard and nobody can say why.

What you can read in one call

GET /me is already a five-counter snapshot for one credit:

Field Meaning
subscribersCount active subscribers
subscribesCount accounts this creator subscribes to
postsCount posts published
photosCount photos published
videosCount videos published
curl -s "https://api.ofapis.com/api/public/v1/accounts/42/me" \
  -H "Authorization: Bearer ofapis_sk_..." | jq '.data | {subscribersCount, postsCount, videosCount}'

One request, one credit, five headline numbers. If all you need are KPI tiles that tick over daily, stop here: a nightly /me is 30 credits a month per account, and the tiles on an analytics dashboard — or a row appended to a Google Sheet — rarely need more.

The one genuinely dated series sits elsewhere. GET /accounts/{id}/scheduled/posts/counters takes a required publishDate and publishDateEnd plus a timeZone, and returns per-day counts shaped like {"2025-07-01":{"message":3,"post":1}}. That measures output cadence, not audience, and it is the only endpoint that buckets anything by date for you — see the scheduling API.

Why counters are not enough

subscribersCount gives the level. It cannot tell you that you gained 40 and lost 38 on a day it moved by 2. Growth, churn and net adds need identities, and identities come from GET /subscribers.

Here is the constraint that decides your whole schema: a subscriber row carries no join date. The row is an identity plus state flags, and the field people assume is a date — subscribedOn — is a boolean. There is no joinedAt and no expiry on the list row, so you cannot ask "who joined last week" in a single call at any price. The only way to know when someone arrived is to have seen the audience without them and then with them.

So the resolution of every growth metric you report equals your snapshot interval. Snapshot nightly and your churn is a daily number forever; no later query recovers hourly detail. Decide the interval up front, because changing it is not retroactive. Paging mechanics for the sweep live on the subscribers API page.

The storage model

Two tables. One holds the counters, one holds membership.

CREATE TABLE of_counters (            -- one row per /me call
  account_id   bigint      NOT NULL,
  taken_at     timestamptz NOT NULL,
  subscribers  integer     NOT NULL,
  posts        integer     NOT NULL,
  PRIMARY KEY (account_id, taken_at)
);

CREATE TABLE of_membership (          -- one row per fan per observation window
  account_id   bigint      NOT NULL,
  fan_id       bigint      NOT NULL,
  first_seen   date        NOT NULL,
  last_seen    date        NOT NULL,  -- advanced on every sweep that still sees them
  PRIMARY KEY (account_id, fan_id, first_seen)
);

Do not store a full subscriber list per day: a 50,000-fan roster snapshotted nightly is 18 million rows a year for data that barely changes. The interval form above is one row per fan per membership spell — the nightly job — a few dozen lines of Python on a cron — advances last_seen for ids it still sees, inserts rows for ids it does not have, and leaves departed fans with a stale last_seen. That is 50,000 rows plus churn, and every metric still falls out of it:

-- net adds, joins and lapses for a given day
SELECT
  count(*) FILTER (WHERE first_seen = :d)                       AS joined,
  count(*) FILTER (WHERE last_seen  = :d - 1)                   AS lapsed,
  count(*) FILTER (WHERE first_seen = :d)
    - count(*) FILTER (WHERE last_seen = :d - 1)                AS net_adds
FROM of_membership WHERE account_id = :acct;

Keep of_counters as the cheap sanity check: if subscribers from /me and count(*) of live membership rows drift apart, a sweep failed partway and that day's churn should not be published.

Two operational notes. A sweep must be atomic from the reporting job's point of view — half a paged sweep looks exactly like a mass exodus, so write the whole pass or none of it. And type=expired on /subscribers gives the lapsed pool directly, a useful cross-check against the last_seen arithmetic and the input to any win-back segment.

Where your numbers will disagree with the dashboard

They will, and you need the answer ready before a creator asks.

The honest framing for a client report is that these are your metrics at a stated interval, not a mirror of OnlyFans' own. Put the interval on the report and the disagreements stop being arguments.

Cost of a real collector

One credit per call, and limit is clamped to 1–100, so a sweep is ceil(fans / 100) calls plus one for /me.

Audience Calls per nightly sweep Credits per 30 days
1,000 11 330
10,000 101 3,030
50,000 501 15,030

A 50,000-fan creator swept nightly eats half of Starter's 30,000 monthly credits. Five of them on Pro is 75,150 of 120,000 — it fits, but it leaves thin headroom for messaging, so have your scheduled job sweep the large rosters weekly and the small ones nightly. Check this against pricing and API cost before you schedule it, not after the first 402.

Failures matter because a sweep is long. 429 is refused before metering and costs nothing — back off on Retry-After and resume. 424 and 5xx are debited then refunded. But 400 and 404 are charged and not refunded, so a loop still requesting a deleted accountId burns a credit per page, every night, forever.

FAQ

Which statistics can I get in a single call?

GET /me returns subscribersCount, subscribesCount, postsCount, photosCount and videosCount for one credit, scoped per account. Those are levels, not trends — where the creator is today, with no history and no breakdown behind them.

Can I get subscriber growth for a date range I did not snapshot?

No. The API returns current state only and a subscriber row has no join date, so there is nothing to query retroactively. Growth exists only for periods your collector was already running, which is the single best argument for starting the nightly job before you need the first report.

Why does my subscriber count differ from the OnlyFans Statistics tab?

Usually day boundaries and gap churn. Your figures are bucketed by your cron's clock, and any fan who joined and lapsed between two sweeps never appears in your data at all. Publish the snapshot interval alongside the numbers so the difference is explainable instead of suspicious.

How much storage does a year of snapshots take?

Very little if you store membership intervals rather than daily lists. One row per fan per membership spell means a 50,000-fan creator sits around 50,000 rows plus churn after a year, against roughly 18 million for the naive one-row-per-fan-per-day approach.

Is there any earnings or revenue figure in these endpoints?

Not in /me or /subscribers — neither carries a price, spend or earnings field. Monetary values exist on chat messages (price in cents, isTip, isOpened), so revenue is a separate aggregation job over message history rather than anything the statistics path gives you.

Get a token free · Full API reference

Related guides

All guides
Start free — 250 credits, no card
Generate a token and make your first call in minutes.
Get started