OnlyFans Notifications API
Three read endpoints wrap the OnlyFans activity feed: the feed itself, a counter, and the release-form mentions list. The counter is the primitive almost every polling loop here should be built around. Base URL https://api.ofapis.com/api/public/v1, one Bearer token, no signing.
The three routes
| Route | Parameters | Returns |
|---|---|---|
GET /notifications |
type, limit |
{hasMore, list[]} |
GET /notifications/count |
none | {newCount} |
GET /mentions |
limit, offset |
{hasMore, list[]} |
Each has an account-scoped twin under /accounts/{accountId}/…. The unscoped form resolves to the first ACTIVE linked account on the token — fine for one creator, a trap the moment a second is linked, since which account answers depends on activation order rather than on your request. Scoped routes also return 404 ACCOUNT_NOT_FOUND for an id outside the token, which beats quietly reading the wrong creator.
Reading the feed
curl "https://api.ofapis.com/api/public/v1/accounts/42/notifications?type=subscribed&limit=100" \
-H "Authorization: Bearer ofapis_sk_..."
{"data":{"hasMore":true,"list":[
{"id":"abc123","type":"subscribed","text":"subscribed to you",
"createdAt":"2025-06-01T10:00:00+00:00"}
]}}
limit defaults to 20 and is clamped server-side to 1–100 — coerced, not rejected, so limit=500 quietly becomes 100 with no error. A 100-item read costs the same one credit as a 20-item read, so send limit=100 on any collector.
type is optional and passes straight through to OnlyFans unvalidated; the documented examples are subscribed and commented. A typo therefore returns 200 with whatever the upstream makes of it, not a 400. Build your filter vocabulary from the type field on items you actually received.
The item payload is forwarded verbatim, so treat those four fields as the stable core and tolerate extra keys rather than deserialising into a strict struct. The upstream request sets skip_users=all, so expect no fan profile riding along with each event — resolve ids through the profile API only for events you act on.
The feed has no offset
This is the constraint that shapes everything above it. /notifications accepts type and limit and nothing else: no offset, no cursor, no before timestamp. hasMore comes back true on any busy account and no parameter acts on it. The deepest available read is the newest 100 events.
- There is no backfill. History from before your first call is unreachable through this route. If you want a dated series, start collecting now and store it the way the snapshot-and-diff model describes.
- Your poll interval is a correctness setting, not a cost setting. If more than 100 events land between two polls, the overflow falls off the end for good. Size the interval against the account's busiest hour, not its daily average.
Mentions are release forms
GET /mentions is the exception: it takes a real zero-based offset alongside limit, so this list walks properly. It is also narrower than the name suggests — it returns release-form mentions, the tagged-collaboration records OnlyFans generates when another creator credits this account, not @-mentions in comments or chat, which arrive in the main feed as ordinary notification types. An empty list is the normal answer for a solo creator.
The counter
GET /notifications/count takes no parameters and returns {"data":{"newCount":3}}. Two properties matter.
It is cheap in bytes but not cheap in credits — one credit, the same as a full 100-item feed read. The pattern still pays, because it lets you skip the feed read on every interval where nothing moved; the saving is one call per quiet interval, not a fractional rate.
And newCount is a level, not a tally. Nothing in the public API marks notifications as read, so you cannot clear the counter yourself, and the creator opening the OnlyFans app drops it to zero underneath you. A decrease means "read somewhere else", not negative activity: store the last value and react only to increases.
Count-first poller
import os, time, requests
BASE = "https://api.ofapis.com/api/public/v1"
S = requests.Session()
S.headers["Authorization"] = f"Bearer {os.environ['OFAPIS_TOKEN']}"
def call(path, **params):
r = S.get(f"{BASE}{path}", params=params, timeout=30)
if r.status_code == 429: # free: refused before metering
time.sleep(int(r.headers.get("Retry-After", 5)))
return call(path, **params)
if r.status_code == 424: # session dead; debited then refunded
raise RuntimeError(r.json()["error"]["code"])
r.raise_for_status()
return r.json()["data"]
def watch(acct, interval=120):
base, last = f"/accounts/{acct}", call(f"/accounts/{acct}/notifications/count")["newCount"]
while True:
time.sleep(interval)
now = call(f"{base}/notifications/count")["newCount"]
if now > last: # only an increase is new activity
yield from call(f"{base}/notifications", limit=100)["list"][: now - last]
last = now # a drop means read in the app
Slicing by the delta assumes newest-first ordering and fewer than 100 new events. For exactness, dedupe on item id against the last batch instead — the append-only ledger page works through the natural key for each source.
Credit arithmetic
One credit per successful call, debited before the handler runs, so the maths is per request, not per useful result.
| Interval | Calls/day/account | Calls per 30 days |
|---|---|---|
| 30s | 2,880 | 86,400 |
| 60s | 1,440 | 43,200 |
| 2 min | 720 | 21,600 |
| 5 min | 288 | 8,640 |
| 15 min | 96 | 2,880 |
Against the plans: Free's 250 credits a month is under three hours of once-a-minute polling — a key for trying the endpoints, not for running a watcher. Starter's 30,000 fits one account at two minutes (21,600) with ~8,400 left for the feed fetches those polls trigger. Pro's 120,000 covers five accounts at five minutes (43,200), or two at sixty seconds (86,400) and little else.
Note what is not binding. Rate limits are 60 requests per minute on Free, 100 on Starter and 500 on Pro, per token, in a 60-second window whose rollover X-RateLimit-Reset gives you. A once-a-minute poll uses one sixtieth of the smallest budget: credits run out long before rate limits do. See pricing.
Failure modes in a long-running poller
- 429 is free. The gateway checks the rate limit before it debits, so a throttled call costs nothing. Sleep for
Retry-After. - 424 is refunded. When the OnlyFans session dies the call is debited then refunded, and the account is flagged ERROR so later calls fast-fail at the resolver without touching OnlyFans. Credits are safe; your data is not. A poller that only checks for 200 sits in a silent 424 loop for days — alert on
OF_SESSION_EXPIRED. - 400 and 404 are charged, not refunded. A malformed or unlinked
accountIdburns a credit every interval; validate ids at startup. - 5xx and 409 are refunded. Retry with backoff; full table on API errors.
When polling is the wrong tool
For inbound DMs, stop polling. message.received is a real webhook event — the only event type that exists — and pushes the message to you at no credit cost per check (setup guide). Keep the counter beside it as a reconciliation backstop. For what webhooks do not cover — tips, subscribes, comments — the feed is the only source, and the subscribers API is where you confirm what a subscribed event did to the audience. Those subscribed counts are also one of the three inputs to a daily revenue estimate, since nothing in the API reports money directly.
FAQ
How far back can I read the notification feed?
A hundred events, the clamped maximum for limit. There is no offset or cursor on /notifications, so you cannot page past the newest page, and history from before your first call is unreachable. Collect forward and store what you receive.
Does reading notifications through the API mark them as read?
No. There is no mark-as-read route in the public API, so your reads leave newCount exactly where it was. The counter drops only when the creator reads them in the OnlyFans app or on the web, which is why a decrease is a resync signal rather than data.
What notification types can I filter on?
The type parameter is forwarded to OnlyFans unvalidated; the documented examples are subscribed and commented. An unrecognised value returns 200 rather than an error, so discover the real vocabulary by reading the type field on items that come back unfiltered.
Is polling the count cheaper than polling the feed?
They cost the same single credit each, so the saving comes from frequency, not price. A count check lets you skip the feed read on quiet intervals: a watcher checking twice an hour and fetching on three of those pays 51 credits a day instead of 96.
Are mentions the same as being tagged in a comment?
No. GET /mentions returns release-form mentions, the tagged-collaboration records between creators, and takes limit and offset. Comment tags and chat mentions appear as ordinary items in the main feed instead, so check both for a complete inbox.