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
| Route | Returns |
|---|---|
GET /accounts/{of_user_id}/subscribers/cached | Subscriber snapshot, paginated |
GET /accounts/{of_user_id}/subscribers/new | Newly seen subscribers |
GET /accounts/{of_user_id}/subscribers/stats | Aggregated counts by granularity (hour/day/week/month) |
GET /accounts/{of_user_id}/transactions/cached | Transaction rows, paginated |
GET /accounts/{of_user_id}/fans/{fan_id}/transactions/cached | One fan's transactions |
GET /accounts/{of_user_id}/campaigns/{campaign_id}/claimers/cached | Campaign 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/statusRefreshes 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.