The Only API docs

Cached reads

The routes that cost zero platform requests, and how to keep them fresh.

Every live read reaches OnlyFans or Fansly and counts against their per-account rate limits — the ones that get accounts flagged. The /cached routes serve from our own store instead and cost zero platform requests.

For anything you read repeatedly — dashboards, reports, reconciliation, any loop — use the cached route.

The cached routes

RouteReturns
GET /accounts/{of_user_id}/subscribers/cachedSubscriber snapshot, paginated
GET /accounts/{of_user_id}/subscribers/newNewly seen subscribers
GET /accounts/{of_user_id}/subscribers/statsAggregated counts by granularity (hour/day/week/month)
GET /accounts/{of_user_id}/transactions/cachedTransaction rows, paginated
GET /accounts/{of_user_id}/fans/{fan_id}/transactions/cachedOne fan's transactions
GET /accounts/{of_user_id}/campaigns/{campaign_id}/claimers/cachedCampaign claimers

All of them except /subscribers/stats use offset pagination with a trustworthy total — convention 1 in Pagination. Note the rows come back under a generic list key, not one named after the resource.

/subscribers/stats is not paginated at all: it returns a zero-filled buckets array plus total_in_window.

curl "$BASE/api/crm/$CRM/accounts/$OFUID/subscribers/cached?limit=100&offset=0" \
  -H "X-API-Key: $KEY"

Keeping the cache fresh

Two things update it.

Background polling. Connected accounts are polled on a schedule. This is what generates events — new tips, subscribers, messages, balance changes — and it keeps the caches moving without you asking.

Explicit refresh. When you need current data right now, kick off an async refresh job:

curl -X POST "$BASE/api/crm/$CRM/accounts/$OFUID/subscribers/refresh" \
  -H "X-API-Key: $KEY"
# 202 → poll .../subscribers/refresh/status

Refreshes are delta syncs — they fetch what changed rather than re-walking everything, so they are cheap enough to run on a schedule.

For history that predates the account being connected, use POST /accounts/{of_user_id}/backfill.

A sensible read pattern

# 1. Trigger a refresh only when your data is older than you can tolerate.
if cache_age_seconds > 300:
    session.post(f"{BASE}/api/crm/{CRM}/accounts/{OFUID}/subscribers/refresh",
                 headers={"X-API-Key": KEY})

# 2. Read from the cache regardless — it always answers, and costs nothing
#    against the platform. The refresh will land shortly.
rows = list(walk(session,
                 f"{BASE}/api/crm/{CRM}/accounts/{OFUID}/subscribers/cached",
                 KEY, item_key="list"))

Do not block on the refresh finishing before reading. Read the cache, and let either the next refresh or the background poller catch you up. If you need to know the moment new data lands, subscribe to events instead of polling status.

Polling settings are per account

GET and PATCH /accounts/{of_user_id}/polling control what we poll for an account, and how often. The same object holds allow_of_write_actions, the gate on OnlyFans write operations — see Connect an account.

When to read live instead

Reach for the live route when staleness is genuinely unacceptable — confirming a balance right before a payout, or checking whether a specific message landed. Live routes are GET /accounts/{of_user_id}/subscribers, /balances, /earnings, /purchases, /chats, and the whole /api2/v2/* passthrough.

Even then, do it once, not in a loop.

On this page