TEASEDocs
Guides

Content vault

Index, search, and analyze your OnlyFans vault — find any media in a keystroke, see what actually sells, and get cohort-matched content recommendations.

The content vault is a searchable, AI-tagged index of every media item in your OnlyFans vault, plus the analytics that sit on top of it. Once your vault is indexed you can find any clip or set by title or tag in a keystroke, pull a whole folder as a bundle, and — far more valuable — read which content actually drives revenue, what is sitting dormant, where your demand outruns your supply, and which unseen pieces to send a given cohort next.

Every endpoint below lives under https://app.tease.link/api/admin/vault and is authenticated with a Bearer al_live_* key. All reads require the read scope; the scan trigger and auto-tag require write. Every response is JSON and is scoped to the owner that minted the key — you only ever see your own vault.

The vault index is built and refreshed entirely on TEASE's side. You never upload media to us and you never have to maintain the index by hand — you trigger a scan, then read from it.

How it fits together


Scan and index the vault

Indexing is a background job. You trigger it, then poll status until it settles. A scan is idempotent — re-running it refreshes the index rather than duplicating it.

Trigger a scan

curl -X POST "https://app.tease.link/api/admin/vault/scan" \
  -H "Authorization: Bearer $TEASE_API_KEY"

The scan trigger takes no body or query parameters.

{ "started": true, "already_running": false }

If a scan is already in flight the call is a no-op and you get { "started": false, "already_running": true }. If your OnlyFans account isn't connected yet, the call returns 400 with a message telling you to finish the connection first.

Poll status

curl "https://app.tease.link/api/admin/vault/scan/status" \
  -H "Authorization: Bearer $TEASE_API_KEY"
{
  "folders": 84,
  "items": 5120,
  "ai_tagged": 4870,
  "last_indexed_ts": 1751280000,
  "running": false,
  "last_error": ""
}
FieldTypeDescription
foldersintegerNumber of indexed folders/albums.
itemsintegerNumber of indexed media items.
ai_taggedintegerHow many items already carry AI tags.
last_indexed_tsintegerUnix seconds of the last successful index. 0 if never indexed.
runningbooleantrue while a scan is in flight — poll until it flips to false.
last_errorstringEmpty on success; a short message if the last scan failed.

Indexing is a customer feature, not an API you assemble

How the index is built from your OnlyFans account, and how tags are derived, is handled entirely server-side. You are not expected to enumerate media or generate tags yourself — trigger the scan and read the results.


Search the vault

Instant title- and tag-search across the index. Results come back grouped by folder, with any loose (folderless) items separated out, so the panel's content picker can render a tree immediately.

curl "https://app.tease.link/api/admin/vault/search?q=lingerie&scope=all&limit=200" \
  -H "Authorization: Bearer $TEASE_API_KEY"

Query parameters

FieldTypeRequiredDescription
qstringnoSearch text matched against titles and tags. Empty returns everything (up to limit).
scopestringnoall (default), sorted (only items filed into a folder), or unsorted (loose items).
limitintegernoMax items to return, 1–1000. Defaults to 200.

Response

{
  "query": "lingerie",
  "scope": "all",
  "folders": [
    {
      "folder_id": "f_2291",
      "name": "Red lingerie set",
      "items": [
        {
          "media_id": "m_88120",
          "media_type": "photo",
          "folder_id": "f_2291",
          "folder_name": "Red lingerie set",
          "sorted": true,
          "title": "red set — teaser",
          "has_cover": true
        }
      ]
    }
  ],
  "loose": [],
  "counts": { "folders": 1, "items": 1 }
}
FieldTypeDescription
querystringEcho of the search text.
scopestringEcho of the scope filter.
foldersarrayMatched items grouped by their folder. See the item shape below.
loosearrayMatched items that aren't in any folder.
counts.foldersintegerNumber of folders in the result.
counts.itemsintegerTotal matched items.

Each item carries: media_id, media_type (photo, video, gif, …), folder_id, folder_name, sorted (whether it's filed), title, and has_cover (whether a thumbnail is available — see covers).

Search returns media_ids, never media bytes. Use the media_ids to compose a PPV in the panel, or fetch a thumbnail from the cover endpoint.

Open a folder

Pull every item in one folder — the "select the whole pack" action. The response includes a flat media_ids array ready to drop straight into a message composer.

curl "https://app.tease.link/api/admin/vault/folder/f_2291" \
  -H "Authorization: Bearer $TEASE_API_KEY"
{
  "folder_id": "f_2291",
  "name": "Red lingerie set",
  "media_ids": ["m_88120", "m_88121", "m_88122"],
  "items": [
    { "media_id": "m_88120", "media_type": "photo", "title": "red set — teaser", "has_cover": true }
  ]
}
FieldTypeDescription
folder_idstringThe requested folder id.
namestringFolder name, or empty if unknown.
media_idsarrayEvery media id in the folder, in order.
itemsarrayThe full item record for each id (same shape as search).

Pull a drop

A folder can also be read as a drop — its items grouped by role (video, photoset, bts, other) with a ready-ordered bundle_media_ids for a one-tap "send the whole drop". This is the natural unit a chatter sells.

curl "https://app.tease.link/api/admin/vault/drop/f_2291" \
  -H "Authorization: Bearer $TEASE_API_KEY"
{
  "folder_id": "f_2291",
  "name": "Red lingerie set",
  "cover_media_id": "m_88120",
  "roles": {
    "video":    { "count": 1, "media_ids": ["m_88130"] },
    "photoset": { "count": 8, "media_ids": ["m_88120", "m_88121"] },
    "bts":      { "count": 2, "media_ids": ["m_88140", "m_88141"] },
    "other":    { "count": 0, "media_ids": [] }
  },
  "bundle_media_ids": ["m_88130", "m_88120", "m_88121", "m_88140", "m_88141"],
  "counts": { "total": 11, "video": 1, "photoset": 8, "bts": 2, "other": 0 },
  "is_drop": true
}
FieldTypeDescription
cover_media_idstringSuggested cover for the drop.
rolesobjectItems split into video, photoset, bts, other, each with a count and ids.
bundle_media_idsarrayThe whole drop in send-order — drop straight into a composer.
countsobjecttotal plus a per-role count.
is_dropbooleanAn opaque flag TEASE sets when the folder qualifies as a drop.

Covers

Fetch a thumbnail image for any media id. The endpoint returns a thumbnail image (image/jpeg), so point an <img> at it directly — only items where has_cover is true have one.

curl "https://app.tease.link/api/admin/vault/cover/m_88120" \
  -H "Authorization: Bearer $TEASE_API_KEY" \
  -o cover.jpg

Returns 404 when the media id has no cover. The endpoint only ever serves a thumbnail for a media id in your own vault.


Auto-tag the backlog

auto-tag runs AI tagging over your untagged items so they become searchable and start flowing into the performance, gaps, and recommendation analytics. AI tagging runs server-side. It is an explicit action — it never sweeps your whole vault on its own — and it is cost-capped and idempotent: already-tagged items are skipped and never re-billed, and limit is clamped to a hard server ceiling rather than rejected.

curl -X POST "https://app.tease.link/api/admin/vault/auto-tag" \
  -H "Authorization: Bearer $TEASE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "all_untagged": true, "limit": 100 }'
const res = await fetch('https://app.tease.link/api/admin/vault/auto-tag', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.TEASE_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ all_untagged: true, limit: 100 }),
});
const result = await res.json();
res = requests.post(
    "https://app.tease.link/api/admin/vault/auto-tag",
    headers={"Authorization": f"Bearer {os.environ['TEASE_API_KEY']}"},
    json={"all_untagged": True, "limit": 100},
)
result = res.json()

Body parameters

FieldTypeRequiredDescription
media_idsstring[]noExplicit ids to tag. Omit to use the backlog instead.
all_untaggedbooleannoWhen true, tag the untagged backlog up to limit. Defaults to false.
limitintegernoMax items to tag in this call. Clamped to a hard server cap, so an oversized value is capped, not rejected.

Response

{
  "tagged": [{ "media_id": "m_88120", "tags": ["lingerie", "solo", "teaser"] }],
  "tagged_count": 1,
  "skipped": [{ "media_id": "m_88121", "reason": "already_tagged" }],
  "skipped_count": 1,
  "errors": [],
  "capped": false,
  "requested": 2
}
FieldTypeDescription
taggedarrayItems that got new tags, each with its media_id and the tags applied.
tagged_countintegerCount of newly tagged items.
skippedarrayItems skipped, each with a reason (e.g. already tagged, nothing to tag).
skipped_countintegerCount of skipped items.
errorsarrayPer-item failures. A bad item degrades to an error here — it never 500s the whole call.
cappedbooleantrue if requested exceeded the cap and the run was trimmed.
requestedintegerHow many items the call attempted before capping.

Tagging is degrade-safe per item: a single item that can't be tagged comes back in errors or skipped with a reason; the rest of the batch still succeeds. Re-running is always safe — tagged items are skipped, so you're never charged twice for the same media.


What sells — performance

performance ranks your vault by revenue: which individual media and which tags actually drive purchases, each with an unlock rate (how often a fan who received it bought it). This is the read behind "what's working" in the panel.

curl "https://app.tease.link/api/admin/vault/performance?limit=20" \
  -H "Authorization: Bearer $TEASE_API_KEY"

Query parameters

FieldTypeRequiredDescription
from_tsintegernoWindow start, Unix seconds. Defaults to 30 days ago.
to_tsintegernoWindow end, Unix seconds. Defaults to now.
limitintegernoMax rows per list (media and tags), 1–200. Defaults to 20.

Response

{
  "range": { "from_ts": 1748688000, "to_ts": 1751280000 },
  "media": [
    {
      "media_id": "m_88130",
      "title": "red set — full video",
      "media_type": "video",
      "folder_name": "Red lingerie set",
      "has_cover": true,
      "tags": ["lingerie", "solo"],
      "sent": 247,
      "opened": 181,
      "purchased": 89,
      "revenue_cents": 142800,
      "unlock_rate": 0.34
    }
  ],
  "tags": [
    { "tag": "lingerie", "sent": 988, "opened": 731, "purchased": 301, "revenue_cents": 461500, "unlock_rate": 0.29 }
  ],
  "ppv_sends": 1320
}
FieldTypeDescription
rangeobjectThe resolved from_ts/to_ts window.
mediaarrayTop media by revenue. See the row fields below.
tagsarrayTop tags by revenue, with the same sent/opened/purchased/revenue_cents/unlock_rate.
ppv_sendsintegerTotal PPV sends counted in the window.

Each media row carries media_id, title, media_type, folder_name, has_cover, tags, and the performance numbers:

FieldTypeDescription
sentintegerTimes this media was sent in the window.
openedintegerTimes it was opened/viewed.
purchasedintegerTimes it was unlocked/bought.
revenue_centsintegerNet revenue it drove, in cents.
unlock_ratenumber | nullShare of recipients who unlocked it. null when too few sends to be meaningful.

unlock_rate is reported as an outcome, already suppressed when the sample is too thin to trust — you don't have to compute it or decide when it's reliable.


What to shoot next — gaps

gaps answers "what should I shoot?" by surfacing tags where demand outruns supply — themes that sell well but where you have few items to send — alongside how much of your vault is still unsorted or untagged.

curl "https://app.tease.link/api/admin/vault/gaps?limit=20" \
  -H "Authorization: Bearer $TEASE_API_KEY"

Query parameters

FieldTypeRequiredDescription
from_tsintegernoWindow start, Unix seconds. Defaults to 30 days ago.
to_tsintegernoWindow end, Unix seconds. Defaults to now.
limitintegernoMax shoot_more rows, 1–200. Defaults to 20.

Response

{
  "range": { "from_ts": 1748688000, "to_ts": 1751280000 },
  "shoot_more": [
    {
      "tag": "shower",
      "revenue_cents": 88200,
      "purchased": 61,
      "sent": 140,
      "rank": 1
    }
  ],
  "total_items": 5120,
  "unsorted": 380,
  "untagged": 250
}
FieldTypeDescription
rangeobjectThe resolved window.
shoot_morearrayTags ranked as under-supplied for their demand — your shoot list. See the rows below.
total_itemsintegerTotal indexed items.
unsortedintegerItems not yet filed into a folder.
untaggedintegerItems with no AI tags yet — candidates for auto-tag.

Each shoot_more row reports the tag's revenue_cents, purchased, sent, and its rank — its position in the shoot list, where 1 is the most under-supplied for its demand. The list arrives already ordered; you don't need to derive the ranking.

unsorted and untagged are a backlog signal: items there can't surface in search or analytics until they're filed or tagged. Run auto-tag to clear the untagged count.


What's gone cold — dormant

dormant returns vault items that have had zero send activity in a window — content that's just sitting there and could be re-circulated. It's a set-difference between your vault and what actually sold or was sent recently; nothing about your OnlyFans account net is touched.

curl "https://app.tease.link/api/admin/vault/dormant?window_days=30" \
  -H "Authorization: Bearer $TEASE_API_KEY"

Query parameters

FieldTypeRequiredDescription
window_daysintegernoLook-back window, 1–3650 days. Defaults to 30.

Response

{
  "items": [
    {
      "media_id": "m_70011",
      "title": "beach throwback",
      "tags": ["outdoor", "bikini"],
      "cover": true,
      "last_sent": 1741000000
    }
  ],
  "count": 1,
  "window_days": 30
}
FieldTypeDescription
itemsarrayDormant media. Each has media_id, title, tags, cover, and last_sent.
countintegerNumber of dormant items.
window_daysintegerEcho of the window used.
last_sentinteger | nullWhen the item was last sent (Unix seconds), or null if never.

Cohort recommendations

recommend is the vault's most valuable read: given a cohort of fans, it returns unseen content — items that cohort has not already been sent — ranked for how well they fit. Point it at content (recommend for the people who bought a given piece) or describe a cohort with the same audience filters the broadcast composer uses.

curl "https://app.tease.link/api/admin/vault/recommend?source=tiktok&rfm_tag=whale&limit=20" \
  -H "Authorization: Bearer $TEASE_API_KEY"

Query parameters

Provide either content_id (recommend for the cohort that bought that content) or one or more audience filters. All parameters are optional and combine as an AND filter.

FieldTypeRequiredDescription
content_idintegernoBuild the cohort from the buyers of this content.
sourcestringnoFilter the cohort by acquisition source (e.g. tiktok).
countrystringnoFilter by country (ISO code).
rfm_tagstringnoFilter by RFM/lifecycle tag (e.g. whale, dormant).
spend_minintegernoMinimum lifetime spend, in cents.
spend_maxintegernoMaximum lifetime spend, in cents.
inactive_daysintegernoOnly fans inactive for at least this many days.
bought_ppvbooleannoRestrict to fans who have (or haven't) bought PPV.
new_vs_returningstringnonew or returning.
is_trialbooleannoRestrict to trial (or non-trial) fans.
limitintegernoMax recommendations, 1–200. Defaults to 20.

Response

{
  "items": [
    {
      "media_id": "m_88130",
      "title": "red set — full video",
      "tags": ["lingerie", "solo"],
      "cover": true,
      "unlock_rate": 0.4,
      "fit_reason": "Recommended for this cohort"
    }
  ],
  "cohort_summary": {
    "filters": { "source": "tiktok", "rfm_tag": "whale" },
    "source": "tiktok",
    "fan_count": 312
  },
  "thin": false
}
FieldTypeDescription
itemsarrayRecommended unseen content, best fit first. See the row fields below.
cohort_summaryobjectThe resolved cohort: the filters applied, dominant source, and fan_count. When built from content_id it also carries from_content_id and buyer_count.
thinbooleantrue when the cohort has little buying signal, so the ranking is less certain. Treat the order as a weaker suggestion.

Each recommendation row carries media_id, title, tags, cover, unlock_rate, and a short, human-readable fit_reason you can show next to the suggestion.

The recommendation is an outcome, not a recipe

TEASE picks and ranks the unseen content for you — which signals feed the ranking, and how they're weighed, is internal. You give it a cohort; it gives you an ordered list of what that cohort hasn't seen and is most likely to buy. The thin flag tells you when to trust it less.


Next steps

On this page