OnlyFans Tips API
Everyone searching for an onlyfans tips api wants GET /tips. It does not exist, and inventing it just produces 404s in your code. What does exist is enough signal across two reads to notice a tip within a minute, pin it to a fan and a conversation, and record a cents figure. This page is that mechanism, and the list of things it gets wrong. Everything below is a plain Authorization: Bearer GET against https://api.ofapis.com/api/public/v1 — no signing, no second credential.
A tip is a message property, not a resource
Tips reach the API as ordinary DMs. Each message object from GET /chats/{chatId}/messages carries a boolean isTip alongside price, fromUser, createdAt and a numeric id. There is nothing to list or sort separately — every tip is attached to a conversation you already read through the chat API. The DM conversation is the only message surface ofapis exposes, so a tip that never becomes a DM message is invisible here entirely.
The cheapest detector is the chat list
The obvious move is to poll the notifications feed. It is the wrong first call. Each entry in GET /chats already carries hasUnreadTips — documented as whether there are unread tips in that conversation — plus withUser, unreadMessagesCount and a lastMessage summary exposing isTip and fromUser. One call with limit=100 covers a hundred conversations and says which to open.
import os, requests
BASE = "https://api.ofapis.com/api/public/v1"
H = {"Authorization": "Bearer " + os.environ["OFAPIS_TOKEN"]}
def chats_with_tips():
offset, flagged = 0, []
while True:
r = requests.get(f"{BASE}/chats", params={"limit": 100, "offset": offset},
headers=H, timeout=30)
r.raise_for_status()
page = r.json()["data"]
flagged += [c["withUser"]["id"] for c in page["list"] if c.get("hasUnreadTips")]
if not page.get("hasMore"):
return flagged
offset = page["nextOffset"]
limit is clamped server-side to 1–100, offset is zero-based, and the response hands back nextOffset — use it rather than incrementing by your own page size. A 450-conversation roster costs five credits.
The catch is the word unread. hasUnreadTips is an edge trigger tied to read state: once the conversation is opened — by your integration, by a chatter in the web app, by the creator on their phone — the flag clears and that tip is never flagged again. It means "something happened here recently", not "here is a backlog to drain".
Amount and attribution
The chat list gives you the conversation; the amount only lives on the message.
def tips_in_chat(chat_id, limit=20):
r = requests.get(f"{BASE}/chats/{chat_id}/messages",
params={"limit": limit}, headers=H, timeout=30)
r.raise_for_status()
return [{"message_id": m["id"],
"fan_id": (m.get("fromUser") or {}).get("id"),
"cents": m.get("price", 0),
"at": m.get("createdAt")}
for m in r.json()["data"]["list"] if m.get("isTip")]
Attribution is free because of a quirk worth knowing: chatId is the fan's OnlyFans user id, so the path parameter you just used is already the key for a CRM rollup and for whatever fan record you keep alongside it. fromUser.id on a tip should equal it; if it does not, you are looking at a message the creator sent.
Verify one mapping before trusting it: the schema documents price as cents for PPV messages, and tips reuse the field. Send a small test tip and confirm the integer you read back is cents.
Count-then-fetch beats polling the feed
If you also want the notification feed, poll GET /notifications/count and only pull GET /notifications when the counter moves. Both cost one credit, so the saving is not in billing — it is in what the feed can actually do for you, which is less than you would expect. GET /notifications accepts exactly two parameters: type and limit (clamped 1–100). No offset, no cursor, no before/after. The response includes hasMore, but nothing lets you act on it. The feed is a newest-first window of at most 100 items, full stop.
The type value is forwarded to OnlyFans verbatim; the reference documents subscribed and commented as examples and fixes no vocabulary. Do not guess the tip string — learn your account's:
curl -s "https://api.ofapis.com/api/public/v1/notifications?limit=100" \
-H "Authorization: Bearer $OFAPIS_TOKEN" \
| jq '[.data.list[].type] | group_by(.) | map({type: .[0], count: length})'
The upstream request is issued with user objects stripped, and the documented item shape is {id, type, text, createdAt} — an id, a label, a sentence of prose. No amount, no structured fan reference. The feed says that something happened, not how much or to whom.
Cadence, credits, latency
Detection lag equals your poll interval; credits are linear in frequency:
| Interval | Count calls/day | Credits/month | Worst-case lag |
|---|---|---|---|
| 30 s | 2,880 | ~86,400 | 30 s |
| 60 s | 1,440 | ~43,200 | 1 min |
| 5 min | 288 | ~8,640 | 5 min |
| 15 min | 96 | ~2,880 | 15 min |
Fetches go on top: each interval where the counter moved costs a /chats sweep plus one message read per flagged conversation. Against monthly allowances of 250 on Free, 30,000 on Starter and 120,000 on Pro, a 60-second loop only fits Pro, while a 5-minute loop leaves Starter two thirds of its budget for real work. Requests-per-minute — 60, 100 and 500 — is a separate ceiling no tip poller comes near, so rate limits will not stop you; credits will.
Webhooks look like the fix but are not: message.received is the only event type, and its payload carries {id, text, fromUser, toUser, createdAt} with no price and no isTip. A webhook can wake your poller early; it cannot report a tip.
What this cannot tell you
- Nothing here reconciles to a payout. Every figure is a gross amount on a message; the platform cut, refunds, chargebacks and payout timing appear nowhere. This is operational analytics, not accounting, and the daily earnings estimate page is explicit about how far it can be pushed.
isOpeneddoes not mean paid. It is documented as whether a message has been opened or read — read state, not purchase state. An earlier version of this page implied otherwise and was wrong.- No backfill beyond the window. With no offset on the feed, the earliest tip it can show you is whatever sat in the most recent 100 items at your first poll.
- Read state is shared. Anyone using the OnlyFans app clears the flags your poller depends on. Multi-chatter teams lose detections this way, silently.
- No totals. No per-fan or per-period tip sum exists in the API. Every aggregate you display is one you computed and own, out of a ledger you keep yourself.
Running the loop for real
Two codes end long-running pollers. A 424 with OF_SESSION_EXPIRED or OF_AUTH_INVALID means the linked session is dead and every later call fails identically — alert on the first rather than retrying into a wall. A 429 carries Retry-After in seconds; honour it. Both arrive as {"error":{"code","message"}} with X-Request-Id echoed back; see the error reference and the API docs. Charging is not uniform either: a 424 or 5xx is debited then refunded, but a 400 or 404 — a malformed limit, a chatId that is not a conversation — is charged and kept.
FAQ
Is there a GET /tips endpoint?
No. There is no tips collection, no tip object and no tip-amount field you can query directly. Tips appear as DM messages with isTip set true, and as a hasUnreadTips flag on the conversation summary in GET /chats. That is the entire surface.
Can I send a tip through the API?
No. Tipping is a fan-to-creator action and no endpoint initiates one. The API only reads tips that have arrived. You can reply automatically with a free or priced DM through the messaging API.
How quickly will I see a tip?
As quickly as your poll interval, since nothing pushes tip data to you. A 5-minute loop costs roughly 8,640 credits a month before fetches and gives worst-case 5-minute lag; 30 seconds costs around 86,400 and only fits a Pro budget.
Why did tip detection stop firing for an active creator?
Almost always because hasUnreadTips cleared before your poller ran — a chatter opening the conversation removes the signal. Back the flag up with a periodic message sweep of recently active conversations instead of relying on it alone.
Does the tip amount match what I get paid?
No, and the gap is not small. The cents figure is gross, before OnlyFans' platform cut, and nothing in the API reflects refunds, chargebacks or payout timing. Use it to rank fans and measure campaigns, never to close books.