Release
September 22, 2026
Twelve new endpoints opening up the fan surface: the filterable spender list, per-fan lifetime stats, a filterable transaction feed with a companion counts endpoint, and the Fan Info sidebar records — custom name, metadata, note, claimed links and chat summary. All FansMetric-backed and badged FM. Separately, every photo and video link now expires after two hours, with Get Vault Media added so you can ask for a fresh one.
Current plan
The fan endpoints are entirely additive. The media change is not: photo and video links are FansMetric addresses now, and they expire two hours after you receive them. If you save media links anywhere, save the media id instead — start with Media URLs.
Photo and video links now expire
OnlyFans does not let anyone else open its photo and video addresses, so one copied out of a response never worked for you anyway. We now swap those addresses out before you see them. What arrives instead is a FansMetric link that fetches the file for you — an ordinary web address you can open or show on a page, with no API key needed.
The trade is that a link which opens without a password cannot live long, so each one expires two hours after it is created. That makes a link not worth saving: save the item's id and ask for the file again when you next need to show it. The full rules live on Media URLs.
- Which files. Swapped: photos, videos, GIFs and audio from posts, the vault, chats, stories and mass messages. Untouched: profile pictures and cover photos, Giphy results, and the upload addresses you get back when uploading.
- Getting a fresh link. Get Vault Media returns one vault item with new links, so you can refresh a single file instead of paging the whole vault. Add
refresh=1for a brand-new two-hour window. It uses the same permission as List Vault Media, so nothing needs granting. For anything outside the vault, ask the same endpoint for it again. - If a link fails, ask for the item again — that fixes all four cases.
403expired or altered,404the account was disconnected or removed,429that account loaded too many files this minute,502OnlyFans refused. - Limits. Each connected account can load up to 300 files a minute. Videos play and skip around normally, and showing the same file again within the hour is free. Do not edit a link — changing a single character breaks it.
- Treat a link like the file. Anyone holding one can see that media until it expires, so do not post them anywhere public.
What this opens up
Until now every /fans route relayed OnlyFans. None of the fan data FansMetric computes — lifetime spend aggregates, the per-account breakdown, the transaction history, or anything in the Fan Info sidebar — was reachable through the API. These twelve endpoints close that gap.
Because they read our own database rather than proxying, they keep answering when a connection's OnlyFans session has lapsed. That makes them the reliable half of the fan surface: the relays can fail on an expired session, these cannot.
New endpoints
Twelve, in two groups split by what the data actually is.
- The spender list. List Fans is the filterable fan table — lifetime spend, purchase count, average purchase, average weekly spend, days active and last purchase — with four
{min, max}range filters, alast_purchase_within_dayswindow, and an allowlisted sort. Filtering happens in the database, so narrowing to "spent over $100 and bought this month" does not mean paging the whole set. - Per-fan analytics. Get Fan Stats breaks lifetime spend down per account — the detail the list deliberately sums away — returning a row for every account you name, so one they never bought from comes back at zero rather than missing. Get Fan Transactions pages their transactions newest-first, each row carrying
amount,fee,vat_amount,netandcurrency, with optionalcategories,statusesandfrom/tofilters; Get Fan Transaction Counts takes the identical body and returns counts instead of rows. - Fan Info sidebar records. Your organisation's private annotations, each keyed to one account: custom name (write), metadata (write) for timezone / birthday / city, note (write), the links the fan claimed, and the AI chat summary.
- New permissions:
api_fans_list,api_fans_read_stats,api_fans_read_transactions, andapi_fm_fans_{read,update}_name,api_fm_fans_{read,update}_metadata,api_fm_fans_{read,update}_note,api_fm_fans_read_links,api_fm_fans_read_chat_summary. A role missing one gets 403, so grant them before switching traffic across.
The note and name collision — read this one
FansMetric keeps its own note and its own display name for a fan, and OnlyFans keeps its own of each. All four are now reachable, at URLs a character apart. Editing one does not affect the other.
- Notes.
PATCH /v1/{account_id}/fm/fans/{fan_id}/notewrites FansMetric's note.PUT /v1/{account_id}/fans/{user_id}/noteswrites OnlyFans' subscriber notice. The differences are the/fm/segment and the singularnote. - Names.
PATCH …/fm/fans/{fan_id}/namesets your private label.PUT …/fans/{user_id}/display-namesets the name OnlyFans shows. Get Fan Details relays the OnlyFans one; the/fm/route returns yours. Both being populated with different values is normal, not a bug. - If you are migrating from the web app's behaviour: the sidebar's note and name fields are the FansMetric ones, i.e. the
/fm/paths.
Filtering the transaction feed
All four filters are optional and compose: omit them and the endpoint returns every transaction over all time, chargebacks included.
- AND across fields, OR within one.
{"categories":["message","tip"],"statuses":["undo"]}means "refunded message-or-tip purchases". The category vocabulary istip,message,recurring_subscription,new_subscription,unlock,unknown; statuses aredone,undo,pending_return,loading. - Unknown values are a 400, never an empty result. Sending
"categories":["boost"]returns an error namingboostand listing the six valid values. A typo that silently returned zero rows would be the worst possible failure here, so it cannot happen. An empty array is not a typo and is treated as no filter. from/toaccept the same vocabulary as the rest of the API —2026-01-01,2026-01-01 15:04:05, RFC 3339,now, and relative shorthand like-30days. Both bounds are inclusive; either may be omitted. A date bound excludes rows with a null timestamp, which is correct but worth knowing if your totals shift when you add a window.- Re-send the filters when paging.
_pagination.next_pageis a bare offset, so page two needs the same body with a newoffset— filters included. Dropping them pages a different set. - Counts follow the filter. Get Fan Transaction Counts counts the filtered set, not the whole history: filter to
undoandallequals the chargeback count while every other status reads0. Both breakdowns always sum toall.
Quirks worth knowing before you integrate
Five places where the obvious assumption is wrong.
- Transaction rows carry both gross and net, and only one reconciles.
amountis what the fan paid, and it is the figure List Fans and Get Fan Stats total — so summing it reproducestotal_spend.netis what the creator kept oncefeeandvat_amountcame out, and summing that will not. All four are nullable: an older imported row can reportnetwith no breakdown behind it. - Every id on the wire is an OnlyFans id. A transaction's
idis OnlyFans' own transaction id, andaccount_idon both the feed and the stats is the numeric account id you sent in — not FansMetric's internal UUIDs. Inputs and outputs speak the same vocabulary, so a row can be traced straight back to what you see in OnlyFans. - The transaction feed includes refunds; nothing else does.
status: "undo"rows are filtered out of every aggregate but are rows in Get Fan Transactions, so a fan with a chargeback returns more transaction rows than theirpurchases_count. That is deliberate — a reversal is only legible next to the charge it reverses. Pass"statuses":["done"]if you want the aggregate-matching view, or["undo"]for chargebacks alone. - Blank versus unset differs by field. On the custom name and the note,
""round-trips as"". On metadata'stimezoneandcityit reads back asnull, because historic records were seeded with empty strings and "unset" has to mean one thing. Send an explicitnullwhen you intend to clear a metadata field. average_weekly_spendis 0 for fans active a week or less. Not missing data: projecting a weekly rate from a two-day window produces a number nobody should act on, so it is suppressed rather than extrapolated.
Scoping
Two scopes, and the URL shape tells you which.
POST /v1/fans…takesaccount_idsin the body and aggregates across them. That is why the account set is an input rather than part of the path: the figures are sums across the accounts you name, not per-account results you merge yourself. Naming an account you do not own returns 404.GET /v1/{account_id}/fm/fans/{fan_id}/…serves records belonging to exactly one account. The same fan carries independent notes, names and metadata under each of your creators, and one creator's records are not reachable through another'saccount_id— including inside the same organisation.- Reads never create. A fan you have never annotated returns nulls and writes nothing;
PATCHcreates the record on first write.
Docs improvements
Two fixes to the reference itself. The API did not change — the site did.
- Code spans inside bold copy render as code. A phrase written as
average_weekly_spendis 0 for fans active a week or less used to print its backticks literally, on this page and a dozen others going back to the August release. Bold and code were matched as alternatives, so whichever opened first swallowed the rest of the run; bold now parses the text inside it. - Parameters can declare their wire type. The Try it panel used to guess a type from the parameter name, which landed on
stringfor anything unusual — so List Fans'{min, max}range filters were posted as raw text instead of JSON and came back400. Each one now declares its type and ships a worked example, so Send works straight off the page.