Fansly vault & media
The media library, the upload pipeline, who can see what, and how a voice note actually gets sent.
Everything on this page is Fansly only. An OnlyFans account gets 501 platform_not_supported from these routes; its equivalents live on the
/api2/v2/* passthrough.
The one thing that catches everyone
Fansly has two ids for a piece of media, and only one of them can be attached to a message.
| id | what it is | attachable |
|---|---|---|
media_id | the file in the vault | no |
content_id | the file plus a permission gate | yes |
Attaching a media_id returns 200 and delivers a message with nothing in it.
If that message was a PPV, the fan pays for an empty unlock. Every route below
that produces media returns both, and the one you want is content_id.
Browse the vault
Fansly organises the vault into collections (its albums). Three of them are
system buckets — All, Posts, Messages — auto-populated and undeletable; they
come back with system: true.
curl "$BASE/api/crm/$CRM/accounts/$OFUID/vault/collections" \
-H "X-API-Key: $KEY"{ "success": true,
"collections": [
{ "id": "9001", "name": "All", "system": true, "mediaCount": 240 },
{ "id": "9002", "name": "white bra", "system": false, "mediaCount": 24 }
] }Then read one collection's media:
curl "$BASE/api/crm/$CRM/accounts/$OFUID/vault/media?collectionId=9002&limit=50" \
-H "X-API-Key: $KEY"{ "success": true,
"media": [
{ "id": "m1", "kind": "photo", "mimetype": "image/jpeg",
"url": "https://cdn1.fansly.com/…?Policy=…&Signature=…",
"filename": "red.jpg", "width": 1080, "height": 1350,
"durationSec": null, "collectionIds": ["9002"] }
],
"hasMore": true, "nextCursor": "m1" }Those URLs expire, and they are pinned to our IP
url is a signed CDN link with roughly a week of life, scoped to this API's
egress IP address. Fetch or relay it now; storing it in your own database gives
you a dead link, and handing it to a browser on another network gives you a
403. Re-list to get a fresh one.
Paging uses before/after cursors rather than an offset — pass the last row's
id as before for the next page.
Create and delete collections
curl -X POST "$BASE/api/crm/$CRM/accounts/$OFUID/vault/collections" \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"title": "summer set", "description": ""}'
curl -X DELETE "$BASE/api/crm/$CRM/accounts/$OFUID/vault/collections/9002" \
-H "X-API-Key: $KEY"Deleting a collection removes the grouping, not the media inside it.
Moving media between collections is not exposed. Fansly's own request body for that action has never been captured from a live account, and this API does not guess at writes against a creator's real library.
Upload
curl -X POST "$BASE/api/crm/$CRM/accounts/$OFUID/media" \
-H "X-API-Key: $KEY" \
-F "[email protected]" \
-F "price=10" \
-F "require_subscription=true"Or JSON, and we fetch the file server-side:
curl -X POST "$BASE/api/crm/$CRM/accounts/$OFUID/media" \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"source_url": "https://example.com/voice.m4a", "price": 10}'{ "success": true, "platform": "fansly",
"media": { "media_id": "6002", "content_id": "7003", "mimetype": "audio/mp4" } }Behind that single call: reserve the upload on Fansly's media host, PUT the
bytes to a presigned S3 URL, finalize, poll until transcoding finishes, then
wrap the result in a gated accountMedia. You orchestrate none of it.
Fansly transcodes on the way through, so an uploaded WAV comes back as
audio/mp4. Unlike OnlyFans, the file appears in the vault immediately.
Pass wrap=false to upload into the vault without gating it yet — you get a
media_id and a null content_id, and apply the gate later.
Who can see it
Gating happens either inline with the upload (above) or afterwards:
curl -X POST "$BASE/api/crm/$CRM/accounts/$OFUID/media/6002/permissions" \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"price": 10, "require_subscription": true}'| Field | Effect |
|---|---|
| (nothing) | Free — no unlock step |
price | Pay-per-view, in dollars |
require_follow | Fan must follow |
require_subscription | Fan must subscribe (any tier) |
subscription_tier_id | …that specific tier |
whitelist | Named fans get in regardless of the gate |
preview_id | A free preview clip shown before unlocking |
Gates combine, and unlocking is an OR. price plus require_subscription
shows the fan two doors — "pay $10" or "subscribe to a tier" — and either one
opens it. The audience nests: everyone → follow → subscribe → purchase, and a
subscriber is already a follower. So require the lowest gate you actually
want, not all of them.
whitelist is an override, not a gate. Fansly auto-whitelists a DM's recipient,
which is why a fan can always open what you sent them even when the media is
paid.
The response carries the content_id to attach.
Send it
curl -X POST "$BASE/api/crm/$CRM/accounts/$OFUID/chats/$FANID/messages" \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"text": "listen 💕", "content_ids": ["7003"]}'A voice note is nothing special — it is an audio file that went through the three steps above. Upload, gate, attach.
scheduled_for takes a future unix timestamp (0 or omitted sends now), and
reply_to_id makes the send a reply.
Note that a Fansly PPV is a gated attachment, not a price on the send: the
money lives on the media, set when you upload or gate it.
What is deliberately missing
These exist on Fansly but are not exposed here, because their request bodies have never been captured from a live account. Guessing at them means sending a real message to a real audience, or writing to a creator's real library:
- The broadcast send (
GET /accounts/{of_user_id}/broadcasts/scheduledreads the schedule; there is no send) - Adding or removing media from a collection, and renaming a collection
- Message likes and read receipts
Until then, the raw proxy will forward whatever you can construct yourself.