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=1 for 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. 403 expired or altered, 404 the account was disconnected or removed, 429 that account loaded too many files this minute, 502 OnlyFans 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, a last_purchase_within_days window, 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, net and currency, with optional categories, statuses and from/to filters; 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, and api_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.

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 is tip, message, recurring_subscription, new_subscription, unlock, unknown; statuses are done, undo, pending_return, loading.
  • Unknown values are a 400, never an empty result. Sending "categories":["boost"] returns an error naming boost and 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/to accept 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_page is a bare offset, so page two needs the same body with a new offset — 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 undo and all equals the chargeback count while every other status reads 0. Both breakdowns always sum to all.

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. amount is what the fan paid, and it is the figure List Fans and Get Fan Stats total — so summing it reproduces total_spend. net is what the creator kept once fee and vat_amount came out, and summing that will not. All four are nullable: an older imported row can report net with no breakdown behind it.
  • Every id on the wire is an OnlyFans id. A transaction's id is OnlyFans' own transaction id, and account_id on 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 their purchases_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's timezone and city it reads back as null, because historic records were seeded with empty strings and "unset" has to mean one thing. Send an explicit null when you intend to clear a metadata field.
  • average_weekly_spend is 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… takes account_ids in 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's account_id — including inside the same organisation.
  • Reads never create. A fan you have never annotated returns nulls and writes nothing; PATCH creates 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_spend is 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 string for anything unusual — so List Fans' {min, max} range filters were posted as raw text instead of JSON and came back 400. Each one now declares its type and ships a worked example, so Send works straight off the page.