TEASEDocs
ProductsClub

Admin endpoints

Every internal admin-panel route Club serves — path, method, what it does, owner-only or not.

This is a reference table of Club's internal admin-panel API — the cookie-authenticated routes the panel itself calls, not the external public API (that's documented separately under API Reference, keyed on al_live_* tokens). "Owner-only" means the route requires strict account-owner identity (a team member, even with broad permissions, gets a 403); everything else is reachable by any authenticated admin/team session unless a specific feature flag or team-role check inside the handler says otherwise. Every row below was generated from the routes actually mounted by the running process and independently verified against the handler code, not written by hand.

AI voice

MethodPathDescriptionOwner-only
GET/ai/voiceReturns the owner's current AI voice config (persona, tone, pilot settings), merging stored overrides onto system defaults.Yes
PUT/ai/voiceDeep-merges the full voice config from the request into the owner's stored config, writes an audit log entry, commits, and returns the saved voice.Yes
POST/ai/voice/autofillRate-limited (3/min): reads the model's own outgoing messages from up to 20 recent local conversations, sends them to an LLM under a daily quota to draft a persona/tone JSON, and returns it unsaved (enabled forced false) without ever contacting OnlyFans.Yes

Automation (campaigns, sequences, templates, PPV, kill-switch)

MethodPathDescriptionOwner-only
GET/automation/campaignsLists all campaign rows for the current owner, newest first, each shaped with status/mode/version/managed_by.No
POST/automation/campaignsCreates a new campaign row (name, kind, targeting/segment/filters, linked template/sequence/ppv/post, schedule) and audits it.No
DELETE/automation/campaigns/{cmp_id}Deletes a campaign by id (404 if not found/not owned), writes an audit row, and returns no content.No
GET/automation/campaigns/{cmp_id}Fetches a single campaign by id and returns its full state dict, or 404 if it doesn't exist for this owner.No
PATCH/automation/campaigns/{cmp_id}Applies a partial update to a campaign after a concurrent-edit conflict check (If-Match/base), re-validates any audience_filters_json, and audits the change.No
GET/automation/campaigns/{cmp_id}/previewRead-only: materializes the campaign's target audience (sample + count) and computed revenue economics without creating any dispatch job or send log.No
POST/automation/campaigns/{cmp_id}/promote-liveFlips a campaign's mode from shadow to live only if all four safety gates (soak passed, global engine-ready, per-account engine-ready, risk_owner ack) pass; otherwise returns 409 listing the unmet gates; never itself sends anything to OnlyFans.Yes
GET/automation/campaigns/{cmp_id}/resultsRead-only rollup of a campaign's actual (or shadow) outcomes: job-status counts, send-result/verb/mode breakdown, and a sample of recent send-log rows, sourced only from DB rows.No
GET/automation/campaigns/{cmp_id}/soakEvaluates and returns a campaign's shadow-run soak metrics (coverage, firewall escapes, pacing, green windows) and pass/fail verdict, stamping soak_passed_at in DB the first time it passes.No
POST/automation/campaigns/{cmp_id}/startEnqueues the campaign by materializing its audience into DispatchJob rows behind opt-in/firewall guards, writing only to our DB and sending nothing to OnlyFans; returns 409 with a reason if a guard refuses.No
GET/automation/campaigns/{cmp_id}/upliftRead-only: splits the campaign's full audience into sub-cohorts by a given dimension (source/rfm/country) and ranks them by projected yield, reusing the same economics math as the preview.No
POST/automation/domain-rotationMoves a burned domain to a fresh spare in one operation (clone config, repoint links/campaigns, re-export); non-platform-admin/home callers are force-scoped to their own domain pool and audited only on a non-dry-run.No
GET/automation/domain-rotation/activeRead-only: lists domain migrations currently in progress for the requesting tenant's rotation pool, scoped server-side (not by request param).No
GET/automation/domain-rotation/candidatesRead-only: lists the requesting tenant's spare domains available for rotation; the docstring explicitly states this door is open to any tenant, not just the owner.No
POST/automation/domain-rotation/rollbackReverts a previously-rotated domain back to its old primary and disables the redirect, with the same per-tenant scoping/ownership checks as the rotation endpoint.No
GET/automation/escape-policyRead-only: returns the current global in-app-webview escape policy (mode/ios/android) shared across all tenants.No
POST/automation/escape-policySets the global cross-tenant in-app-escape policy (mode/ios/android) and audits it; the endpoint has no require_admin dependency, only the owner-only guard.Yes
GET/automation/kill-switchRead-only: returns whether the global cross-tenant automation kill-switch file flag is currently active.No
POST/automation/kill-switchSets the global kill-switch flag (halting automation for every tenant) and audits the change.Yes
GET/automation/optinRead-only: returns (creating with all-off defaults if missing) the current owner's single automation consent/opt-in row.No
POST/automation/optinUpserts the owner's automation consent row (opted_in, kill_switch, verbs, rate limits, risk_owner); engine_ready is never settable via this payload.No
GET/automation/overviewRead-only: rolls up the owner's automation state across all campaigns (sent/shadow/blocked/failed counts) plus opt-in and global kill-switch status.No
GET/automation/postsLists all scheduled-post rows for the current owner, newest first.No
POST/automation/postsCreates a scheduled post, rejecting a nonzero price unless paid posts are allowed (free-page rule), lints it through the content firewall, and audits it.No
DELETE/automation/posts/{post_id}Deletes a scheduled post by id (404 if missing) and audits the deletion.No
GET/automation/posts/{post_id}Fetches a single scheduled post by id, or 404 if not found.No
PATCH/automation/posts/{post_id}Partially updates a scheduled post, refusing edits once it has a send_nonce/of_post_id (already publishing) and restricting manual status changes to draft/canceled only.No
GET/automation/ppv-offersLists all PPV offer rows for the current owner, newest first.No
POST/automation/ppv-offersCreates a PPV offer (name, price, media, teaser body, unlock template), lints the teaser through the firewall, and audits it.No
DELETE/automation/ppv-offers/{ppv_id}Deletes a PPV offer by id (404 if missing) and audits the deletion.No
GET/automation/ppv-offers/{ppv_id}Fetches a single PPV offer by id, or 404 if not found.No
PATCH/automation/ppv-offers/{ppv_id}Partially updates a PPV offer's fields, re-linting the teaser through the firewall whenever teaser_body or media_refs change.No
GET/automation/sequencesLists all message-sequence rows for the current owner, newest first.No
POST/automation/sequencesCreates a message sequence (name, steps, trigger, active flag) and audits it; the firewall verdict is computed on read, not stamped on the row.No
DELETE/automation/sequences/{seq_id}Deletes a message sequence by id (404 if missing) and audits the deletion.No
GET/automation/sequences/{seq_id}Fetches a single message sequence by id, or 404 if not found, returning its steps plus an on-the-fly firewall verdict.No
PATCH/automation/sequences/{seq_id}Partially updates a message sequence's name/trigger/active/steps fields and audits the change.No
GET/automation/templatesLists all message-template rows for the current owner, newest first.No
POST/automation/templatesCreates a message template (name, body, media refs, locale), lints the body through the firewall, and audits it.No
DELETE/automation/templates/{tpl_id}Deletes a message template by id (404 if missing) and audits the deletion.No
GET/automation/templates/{tpl_id}Fetches a single message template by id, or 404 if not found.No
PATCH/automation/templates/{tpl_id}Partially updates a message template's fields, re-linting through the firewall whenever body or media_refs change.No

Broadcasts & segments

MethodPathDescriptionOwner-only
GET/broadcastsLists all broadcast campaigns for the tenant and returns them alongside the account-wide send-lane mode (whether live sending is currently enabled).No
POST/broadcastsCreates a new broadcast campaign from the submitted title/mode/audience/message/schedule, requiring an extra team capability grant if the mechanism is native mass-send; returns the new campaign id.No
GET/broadcasts/automessagesLists the owner's auto-message campaigns (one per kind: welcome, expiring, expired, online) with rendered text, PPV price, trigger knobs and shadow mode.No
POST/broadcasts/automessagesUpserts the single campaign for a given kind: sets template text/media, PPV price, trigger knobs and enabled status, halting queued jobs if disabled.No
DELETE/broadcasts/automessages/{cmp_id}Halts not-yet-sent dispatch jobs, then deletes the auto-message campaign and its associated template/PPV offer rows, returning halted count.No
PATCH/broadcasts/automessages/{cmp_id}Partially updates an existing auto-message (text, media, PPV price, segment, trigger knobs, enabled), halting queued jobs only when it's turned off.No
GET/broadcasts/automessages/{cmp_id}/shadow-logReturns the most recent shadow-mode SendLog rows for the campaign (fan id, rendered body, result) — who it would have gone to, nothing actually sent.No
GET/broadcasts/automessages/{cmp_id}/would-fireDry-runs eligibility using the real trigger/segment logic to count fans who'd qualify now, plus opt-out/kill-switch/firewall gating flags, without sending anything.No
POST/broadcasts/previewResolves an audience spec into a preview (without creating a campaign), returning a 400 with a human-readable error if the audience is unsupported, too large, or references a missing segment.No
GET/broadcasts/segmentsLists this owner's non-deleted saved fan segments, each with its filters, a human-readable description, and a live member count.No
POST/broadcasts/segmentsValidates the submitted filters and creates a new saved segment for the current owner, rejecting the write once 100 live segments already exist.No
POST/broadcasts/segments/previewValidates the submitted filters (not yet saved) and returns how many fans currently match, for live-updating the segment builder UI.No
DELETE/broadcasts/segments/{seg_id}Soft-deletes an owned segment by setting deleted_at, refusing with 409 if any draft/scheduled/running/paused broadcast campaign still references it.No
PATCH/broadcasts/segments/{seg_id}Updates an owned segment's name and/or filters (validating any new filters) and commits the change.No
GET/broadcasts/segments/{seg_id}/membersReturns a paginated, sortable list of fans matching an owned segment's filters, with spend, purchase, and subscription details per fan.No
GET/broadcasts/segments/{seg_id}/sync-planComputes and returns a preview (add/remove counts) of syncing the segment to OnlyFans Collections, read-only and never actually writing to OF.No
GET/broadcasts/statsRefreshes the mass-message stats mirror from the engine feed for the current tenant, then returns the last 100 broadcasts (tenant-scoped) with computed sent/viewed/replied/purchased rates and a connecting/stale/ok freshness state derived from how old the last engine snapshot is.No
GET/broadcasts/stats/{mm_id}/buyersLooks up the broadcast by mm_id scoped to the current tenant (404 if not owned), reconciles/returns a cached async engine job result for its OnlyFans buyer list (re-queuing in the background once per hour, else queuing a fresh job), never blocking on the OnlyFans call itself.No
DELETE/broadcasts/{campaign_id}Hard-deletes a campaign and its expansion rows for this tenant, returning 404 if it doesn't exist (or belongs to another tenant) and 204 on success.No
GET/broadcasts/{campaign_id}Returns computed stats for a single campaign, or 404 if the campaign id doesn't resolve.No
PATCH/broadcasts/{campaign_id}Updates only the fields explicitly present in the request body (title/audience/message/schedule) on an existing campaign, 404 if not found.No
POST/broadcasts/{campaign_id}/cancelCancels a campaign, returning 404 if it doesn't exist and {ok:true} on success.No
POST/broadcasts/{campaign_id}/pausePauses an in-progress campaign's sending, returning 404 if it doesn't exist and {ok:true} on success.No
POST/broadcasts/{campaign_id}/resumeResumes a paused campaign's sending, returning 404 if it doesn't exist and {ok:true} on success.No
POST/broadcasts/{campaign_id}/retry-failedRequeues only the failed sends of a campaign (gated by the same 'broadcasts' team capability as sending), 404 if unknown/foreign, 409 on an invalid campaign state.No
POST/broadcasts/{campaign_id}/scheduleExpands a draft campaign into a paced 1:1 send plan and schedules it, returning the result plus current send-lane mode; 404 if not found, 409 on a scheduling error.No

Campaign source assignment

MethodPathDescriptionOwner-only
POST/campaigns/assign-source-bulkBinds many campaigns to source domains in one transaction (one shared target or per-campaign targets), writes/updates SourceLink rules, marks them known, and runs one batched targeted re-attribution per page; unknown ids/domains are reported per-item without failing the whole batch.No
POST/campaigns/{provider_id}/assign-sourceBinds a single campaign to a validated domain source via a SourceLink rule, marks the campaign known, and runs targeted DB-only re-attribution (reresolve + establish) for that campaign's fans.No
PATCH/campaigns/{provider_id}/hiddenHides or unhides a campaign in the source layout view, snapshotting its current lifetime revenue on hide so it resurfaces once new money arrives, without touching its source/rule data.No
POST/campaigns/{provider_id}/unassign-sourceReverts a campaign to pending by deactivating its SourceLink rule (kept for audit) and clearing domain/source/country, then re-runs targeted DB-only re-attribution so its fans honestly downgrade.No

Chat scripts

MethodPathDescriptionOwner-only
GET/scriptsLists canned-response scripts, either full-text searching by q or filtering by folder with a sort order, gated by the 'scripts_use' team capability.No
POST/scriptsValidates the optional PPV price against OF's $3-$200 bounds, then creates a new canned-response script (with optional media/price preset) and returns its id.No
DELETE/scripts/{script_id}Deletes a canned-response script by id, returning 404 if it doesn't exist.No
PATCH/scripts/{script_id}Validates any updated PPV price, then patches only the fields sent in the body on the given script, 404 if not found.No
POST/scripts/{script_id}/usedRecords that a script was used (updates its use-tracking/timestamp), returning 404 if the script doesn't exist.No

Chatters (team, shifts, KPIs)

MethodPathDescriptionOwner-only
GET/chattersReturns the team roster ordered by id; a self-scoped chatter sees only their own operator row.No
POST/chattersCreates a new operator record in the roster; requires the roster_manage team capability and is blocked for self-scoped chatters.No
POST/chatters/attributeManually pins (or clears) which operator a sale is credited to, overriding automatic attribution; requires the attribution_edit capability.No
GET/chatters/audit/messagesReturns filtered, paginated OutboundMessage audit rows (sender, recipient, timing, input method, price, reply latency, purged flag); auto-scopes chatters with a team-access grant to their own operator_id and masks price_cents when the caller lacks the leaderboard_money cap.No
POST/chatters/clock-inOpens a clock-in shift for the operator (start_ts=now, end_ts=null), idempotently returning the already-open shift on a repeat call.No
POST/chatters/clock-outCloses the operator's currently open clock-in shift, setting end_ts=now and returning worked seconds.No
GET/chatters/clock-statusReports whether the operator currently has an open clock-in shift, including its shift id and start time.No
GET/chatters/fan-kpisComputes account-level fan economics (ARPPU/APC/APV/ARPNU, new-vs-paying) for the window from of_transactions and of_fans.No
GET/chatters/fan-pricingReturns a suggested PPV price and spend tier for one fan, computed from that fan's transaction history.No
GET/chatters/forecastReturns a run-rate revenue projection for the next 30 days plus subscriber health metrics.No
GET/chatters/goalsReturns each chatter's actual net revenue vs target for the rolling period with pct progress; self-scoped chatters see only their own goal.No
POST/chatters/goalsSets an operator's net revenue target for a period; self-scoped chatters may set only their own goal, others need roster_manage.No
GET/chatters/leaderboardComputes per-operator sales-attribution KPIs and reconciliation for the window, filtering to self row when self-scoped and masking money unless leaderboard_money cap is granted.No
GET/chatters/payouts.csvStreams a per-chatter payout CSV (role, net, hours, payout) for the window; requires payouts_export cap and is blocked for self-scoped chatters.No
GET/chatters/qualityReturns quality KPIs (response time, PPV unlock rate, messages per sale) for the account or one operator over the window.No
GET/chatters/shiftsLists shifts overlapping the given window (default all time); self-scoped chatters see only their own shifts.No
POST/chatters/shiftsCreates a shift record for an operator after validating end_ts>=start_ts and operator existence; self-scoped chatters may only create their own.No
DELETE/chatters/shifts/{shift_id}Hard-deletes a shift by id; a shift belonging to another operator is 404 (not 403) for self-scoped chatters, else requires roster_manage.No
GET/chatters/waitingLists fans currently waiting for a reply, longest wait first, prioritizing high-value fans.No
GET/chatters/{operator_id}Returns one operator's KPI drill-down plus their shifts for the window; 404 if a self-scoped caller requests a teammate's id, and masks money on others' cards without the leaderboard_money cap.No
PATCH/chatters/{operator_id}Updates fields on an existing operator record; requires roster_manage cap and is blocked for self-scoped chatters, 404 if the operator doesn't exist.No

Content library

MethodPathDescriptionOwner-only
GET/contentLists top vault media ranked by buyers count, tips, or period sales from content_sale, with total count.No
GET/content/bundlesAggregates content_sale rows into recurring media bundles, ranked by revenue then sales, with cover ids.No
GET/content/cover/{content_id}Streams the locally re-hosted cover image for one content item from disk, or 404 if none exists.No
GET/content/fan/{fan_id}/statusBuilds a fan's bought/sent/free media badge map from live chat + content_sale, writing results back to MediaSend.No
GET/content/for-paymentResolves which vault media a fan bought for a content-less PPV via live-chat price/time match, saved to content_sale.No
GET/content/media-coverServes one cover image by its allowlisted OF CDN URL, reading bytes the engine already cached to disk, or 404 on a miss.No
GET/content/purchased-by/{fan_id}Returns a fan's purchased content from the content_sale ledger, optionally refreshing from live chat + re-hosting covers.No
GET/content/searchSearches content_sale for media sold near a given price/time, ranked by closeness, to manually link an unmatched PPV.No
GET/content/statsReturns batch sales stats (times sold, avg solo price, unlock rate) for up to 100 media ids from ContentSale/MediaSend.No
GET/content/{content_id}Returns one content item's detail: purchase rank, captured sale history with fan usernames, and bundles it's sold in.No
GET/content/{content_id}/cohortsGroups this content's buyers by cohort (source/RFM/spend tier) and computes a lookalike non-buyer audience to retarget.No
GET/content/{content_id}/suggested-priceSuggests a PPV price for this content from its realized sale prices and unlock-rate guardrail (raise/lower/ok/insufficient_data).No
POST/content/{media_id}/captionLooks up the vault item by media_id (404 if missing), builds a text signal from its title/folder/tags, and calls the key-scoped LLM (Claude/DeepSeek via core.translate) under a daily quota to generate a short sales caption, returning {media_id, caption} on success or a soft {caption:null, error:"llm_key_not_set"} on no key/LLM failure/empty output — never a 500.No
GET/content/{media_id}/sendsAggregates our outbound sends containing this media joined to purchase status: sent, opened, purchased, revenue, buyers.No
POST/content/{media_id}/tagsAdds a normalized manual tag to a vault media (idempotent), folding it into the vault item's search_text.No
DELETE/content/{media_id}/tags/{tag}Deletes a manual tag from a vault media by exact normalized match, leaving AI/folder tags untouched; 404s if not found.No

Content requests

MethodPathDescriptionOwner-only
GET/requestsLists content requests, optionally filtered by status and/or fan_id, returned under an "items" key.No
POST/requestsCreates a new proof/custom content request for a fan (chatter-initiated) and returns its id and status.No
POST/requests/{request_id}/cancelCancels an open content request; returns 409 if the request is not in an open state.No
POST/requests/{request_id}/deliverMarks a fulfilled content request as delivered (proof was sent to the fan); returns 409 if not in 'fulfilled' state.No
POST/requests/{request_id}/respondApplies the model's action (start/fulfill-with-media/decline-with-reason) to a request; 404 if missing or invalid transition.No

Dashboard (blocks, live, metrics)

MethodPathDescriptionOwner-only
GET/dashboard/blocksLists all custom dashboard blocks (all tabs, tab filtering left to the frontend) belonging to the caller's workspace, tenant-scoped so other tenants' rows are invisible.No
POST/dashboard/blocksValidates a tab/name/config body, enforces a per-tenant MAX_BLOCKS cap on custom (non-system) blocks, then inserts, audit-logs, and returns the new block.No
POST/dashboard/blocks/fansly/presetsIdempotently creates any missing built-in Fansly system dashboard blocks for the tenant, audit-logs new creations, and returns the full preset set or 409 if incomplete.No
DELETE/dashboard/blocks/{block_id}Deletes a caller-owned, non-protected custom block and atomically prunes all persisted layout references to it, audit-logging the deletion.No
PUT/dashboard/blocks/{block_id}Partially updates a caller-owned, non-protected block's name and/or config (only fields present in the body change; tab is immutable), audit-logging changed fields.No
GET/dashboard/liveKicks off an async revenue refresh, then returns a domain-scoped live feed of recent clicks, subscriptions, and payments.No
GET/dashboard/metricsComputes a batch of registry metrics (by CSV keys, optional :dim) for a resolved account scope, masking money for 'hide' team members and returning per-key errors instead of failing the whole batch.No
GET/dashboard/metrics/libraryReturns static metric-registry passports (key/name/hint/unit/dims/platform_sources) for the block picker, filtered to keys the caller is permitted to see; computes nothing.No

Export & PDF reports

MethodPathDescriptionOwner-only
GET/export/sharesLists up to 50 non-draft PDF export share rows (title, url, views/downloads, expiry, revoked flag, finance/sections) from PdfExportShare.No
POST/export/shares/{share_id}/revokeSets revoked=1 on a non-draft PdfExportShare row by ID (404 if missing) so its link 404s immediately; the row itself is kept for history.No
POST/export/tg-channelValidates the export payload, rate-limits per owner, renders the Telegram-channel PDF via a draft share, discards the draft, and streams the PDF as a download.No
GET/export/tg-channel/manifestReturns the static report section catalog, expiry-day choices, max password length, and delivery options that drive the export dialog UI.No
POST/export/tg-channel/shareValidates the payload, renders a public (non-draft) PDF share of the channel report, and per delivery also Telegram-sends or emails the link/file.No

Fan detail

MethodPathDescriptionOwner-only
GET/fan/{fan_id}Aggregates one fan's of_transactions into a spender-detail card (net/gross totals, per-day chart, tx list, source/origin, RFM profile); 404 if the fan has no transactions.No
GET/fan/{fan_id}/transactionsReturns a paginated, newest-first list of one fan's transactions (all kinds including refunds) as shared TransactionLogRow cards via build_transaction_rows, plus totals and a separate all-time refund count/net computed directly from OfTransaction with status='undo', tenant-scoped.No

Fans (attribution, segments, export)

MethodPathDescriptionOwner-only
GET/fans/attributionsReturns one server-ranked, filtered and paginated page of this owner's scanned fans (optionally across multiple linked accounts via scope/id) with money/tenure enrichment, filter facets, and scan-coverage counters; pure DB read, empty list on DB error.No
GET/fans/chargebacks/recentReads recently detected refunds/chargebacks from the money-flow event log (no OnlyFans calls), returning up to 200 rows within the requested day window plus their summed net_cents.No
POST/fans/establish-nowSynchronously runs the DB-only known-campaign attribution job (establish_known_campaign_fans) for the current owner, rate-limited to 6/min, no OnlyFans calls.No
GET/fans/exportStreams a ZIP of up to limit (≤5000) of this owner's scanned fans (CSV summary, per-fan JSON with info/portrait/conversation, methodology and prompt docs), gated by the export DLP quota/2FA step-up.No
GET/fans/export/segment/{seg_id}.csvStreams a CSV export of a segment's members after checking the shared fan-export DLP/quota gate, so the row limit can't be bypassed via segments.No
POST/fans/harvest/startStarts (superseding any prior job) a fire-once DM-harvest sweep for up to target top-spending fans, creating a DmHarvestJob and enqueueing its first batch via the engine gateway provider.No
GET/fans/harvest/statusReturns the current/last harvest job's live counters (harvested/portraited/total/engine state) for the Fans tab's polling dashboard; pure read, idle status on DB error.No
POST/fans/harvest/stopMarks the owner's running harvest job(s) as stopped, halting further batch enqueuing without tearing down in-flight work; idempotent no-op if none is running.No
POST/fans/scan-attribution/backfillScans a bounded batch (≤50) of not-yet-attributed fans, or stale ones in rescan mode, top-spenders first, computing LLM attributions in parallel then persisting them serially, returning processed/remaining counts.No
GET/fans/scan/overviewReturns the aggregate numbers for the first-scan progress screen by delegating to fan_scan_progress.build_scan_overview; this handler only supplies auth/gate/session.No
GET/fans/{fan_id}/attributionReturns the current FanAttribution row for one fan, owner-scoped, or null when the fan was never scanned or the row lookup fails.No
GET/fans/{fan_id}/fullReturns the full per-fan record (info + portrait + complete conversation) as JSON for one owner-scoped fan, the same object shape used in the ZIP export.No
POST/fans/{fan_id}/scan-attributionBuilds the fan's local conversation feed then runs the LLM attribution service for that one fan, writes/commits the FanAttribution row, and returns it; rate-limited to 30/min.No
GET/fans/{fan_id}/segmentsChecks membership of a given fan against each of the owner's saved segments and returns the list of segments (id, name) the fan belongs to.No

Fansly

MethodPathDescriptionOwner-only
GET/fansly/accounts/{platform_account_id}/fbuddy-parity/runsResolves the caller's Fansly account, then lists that account's FBuddy metric-parity runs (optionally filtered by draft/finalized state, capped at limit, default 50 max 100).Yes
POST/fansly/accounts/{platform_account_id}/fbuddy-parity/runsResolves the account, derives an idempotency key from the account id and body digest, and creates a new draft FBuddy parity run for that account (fixed benchmark key/protocol version, caller-supplied timezone), committing it.Yes
GET/fansly/accounts/{platform_account_id}/fbuddy-parity/runs/{run_id}Resolves the account and run (404 if either is missing/not owned) and returns the run plus its recorded comparisons and current draft manifest.Yes
POST/fansly/accounts/{platform_account_id}/fbuddy-parity/runs/{run_id}/comparisonsValidates a manually-entered FBuddy vs Tease metric comparison for the given run, appends it to the run and commits, returning the stored comparison (409 on an evidence gap).Yes
POST/fansly/accounts/{platform_account_id}/fbuddy-parity/runs/{run_id}/finalizeFinalizes the draft run after checking the caller-supplied expected comparison count and draft digest against the stored draft, commits, and returns the finalized run with its comparisons and manifest.Yes
GET/fansly/accounts/{platform_account_id}/fbuddy-parity/runs/{run_id}/manifestResolves the account and run and returns the run's draft manifest (comparison count, metric count, window roles, draft digest) without the individual comparison rows.Yes
GET/fansly/attributionResolves the caller's scoped Fansly account, computes source-attribution links for the given period via compute_fansly_attribution, and returns them with scope/freshness/stale_seconds metadata after fan-privacy filtering.No
GET/fansly/contentResolves the account/period from query params, then returns Fansly content analytics (rows, media-type breakdown, evidence-based advice) via screen_content.build, or empty rows/advice with a gap entry when no account resolves.No
GET/fansly/dashboardValidates window/tz/scope and resolves the Fansly account by principal scope, then assembles and returns dashboard data (freshness, money, top supporters, content winners, plans, tracking links, FYP revenue/views, best-time-to-post) purely from platform_* projections, with no live network call to Fansly.No
GET/fansly/discoverResolves the account/period, then returns Fansly discovery/visibility metrics (FYP, search, suggestions, posts, tags, media, hours) via screen_discover.build, or all-null placeholders with a gap entry when no account resolves.No
GET/fansly/fan/{fan_platform_id}Builds a Fansly fan card (spend totals, recent purchases, revenue-by-day, timeline) purely from platform_transactions scoped to the caller's accounts; returns 404 if the fan has no transactions there, and leaves subscription/tags/lists/notes/source/portrait null/empty since those data sources aren't built yet.No
PUT/fansly/fan/{fan_platform_id}/listsValidates account/fan scope and write-lock state via the shared _gate() helper, then unconditionally errors (403 if locks closed, else 409 no-engine) - never actually updates the fan's list membership.No
POST/fansly/fan/{fan_platform_id}/notesValidates account/fan scope and write-lock state via the shared _gate() helper, then unconditionally errors (403 if locks closed, else 409 no-engine) - never actually saves the note.No
PUT/fansly/fan/{fan_platform_id}/sourceValidates account/fan scope and write-lock state via the shared _gate() helper, then unconditionally errors (403 if locks closed, else 409 no-engine) - never actually sets the canonical source.No
PUT/fansly/fan/{fan_platform_id}/tagsValidates account/fan scope and write-lock state via the shared _gate() helper, then unconditionally errors (403 if locks closed, else 409 no-engine) - never actually saves the tags.No
GET/fansly/fansReturns one server-paginated page of the Fansly fan roster (from the platform_fans projection) merged with top-supporters spend data from platform_transactions, plus facets, freshness info, and gaps[] noting missing data (e.g. no account selected, no portrait job yet).No
GET/fansly/fans/exportBuilds and streams a CSV (up to 50,000 rows) of the same filtered/sorted fan roster as the list endpoint, always returning a file (with comment lines for gaps/truncation) instead of a 404 on empty results.No
GET/fansly/fyp-score-readinessValidates the period/timezone query params, resolves the caller's Fansly platform account, then loads relational metric snapshots and returns FYP score-readiness (completeness, per-component status, gaps) with no actual score.No
GET/fansly/media/{kind}/{external_id}Serves a locally cached Fansly avatar/cover image: looks up the account-scoped DB row for the given kind/id, hashes its stored (unsigned) CDN URL to a file path, and streams that file from disk with sniffed content-type, or 404s if the row, URL, or fetched file is missing.No
GET/fansly/plansResolves the account/period, then returns Fansly subscription plans and promo offers via screen_commerce.plans, or empty plans/offers with a gap entry when no account resolves.No
GET/fansly/tracking-linksResolves the account/period, then returns Fansly tracking-link stats (clicks, claims, free/paid fans, net/gross revenue cents) via screen_commerce.tracking, or null totals with a gap entry when no account resolves.No

Free pool (trial slot dressing)

MethodPathDescriptionOwner-only
GET/free-poolReturns the owner's dressable spare-link ('sleep link') inventory plus free paid/ftl counts, served through a per-owner read cache with a background refresher.No
POST/free-pool/dressDresses one spare link onto a (domain, country, arm) slot via the free_pool engine, then writes an audit row, commits, and busts the links cache.No
POST/free-pool/dress-batchAuto-dresses a whole pack of countries for a domain/arm from the pool in one call with no per-link confirmation, then audits, commits, and busts the cache.No
GET/free-pool/geo-recoReturns raw facts (arm clicks, already-dressed countries, traffic-without-slot countries, free spares) for the frontend's geo-dress recommendation, deliberately without a threshold.No
POST/free-pool/reclaimPulls a dressed code back into the pool (e.g. on country delete/hide) via the engine, then writes an audit row, commits, and busts the links cache.No
POST/free-pool/suggestDry-run only: returns which spare and auto-name the engine would pick for a dress request, without writing anything to the database.No
GET/free-pool/trial-splitReturns the owner's trial-split config (enabled, min clicks/day, max targets, excluded slots) plus the current list of split candidate slots.No
POST/free-pool/trial-split/undoCollapses a trial slot's split back to a single target and records the owner's refusal, then writes an audit row, commits, and busts the links cache.No

Inbox

MethodPathDescriptionOwner-only
GET/inbox/conversationsReturns the owner-scoped, filterable (status/tab/assigned operator) list of chat threads from the Conversation table, ordered waiting-first, plus per-tab counts.No
GET/inbox/conversations/{fan_id}/messagesLive-reads a fan's chat messages from the OF vendor via the owner's read client, normalizes/orders them, merges unsend marks and send-status, and best-effort kicks an engine refresh if the captured body is missing or stale; on any vendor/credential error it returns an empty {messages: [], error: true} instead of failing.No
PATCH/inbox/conversations/{fan_id}/stateApplies a triage action (snooze/reopen/close/mark_unread/mark_read) to one Conversation row by fan_id, updating status/snooze_until/unread_count, returning 404 if not found, 422 for unknown action, and {ok:false} instead of 500 on a commit failure.No
GET/inbox/fan/actions/enabledReturns which fan actions (discount/restrict/name/note) are currently enabled via env flags, behind team_fence only (no capability check needed to view state).No
GET/inbox/fan/tagsScans all of the owner's fan notes, extracts and case-insensitively de-dupes their tags in Python, and returns them sorted (degrades to an empty list on a DB error instead of a 500).No
GET/inbox/fan/{fan_id}/actionsSettles any pending queued fan-action log rows against the engine job status, then returns the fan's local label, enabled-actions map, and last 20 action log entries.No
POST/inbox/fan/{fan_id}/discountRequires the fan_discount team capability, validates the fan id is numeric, and queues (or shadow-logs, if the discount flag is off) an OnlyFans discount job with percent/months.No
GET/inbox/fan/{fan_id}/economicsReads the fan's own of_transactions and message_stats rows (no vendor call) to return lifetime/30-day net spend, a PPV send/unlock funnel, a PPV/tips/subscription money breakdown, last purchase timing, and the 10 most recent transactions, degrading to a zeroed {known:false} shape on any read error.No
PUT/inbox/fan/{fan_id}/labelSets or clears our own local display name for the fan in the FanLabel table, and if OF actions are enabled and the caller holds fan_of_actions, also queues a push_name job to OnlyFans.No
POST/inbox/fan/{fan_id}/messages/{msg_id}/unsendRequires the send team capability, snapshots the outgoing message's text/time/price before queueing an OnlyFans unsend job, and writes an audit log entry with who pressed it.No
GET/inbox/fan/{fan_id}/notesFetches all notes for the given fan_id ordered newest-first and returns them with a count, returning an empty list instead of a 500 if the query fails.No
POST/inbox/fan/{fan_id}/notesCreates a new FanNote for the fan with a trimmed body and normalized tags, stamping author (operator id or 0 for owner) and timestamps, committing or rolling back with {ok: false} on failure.No
DELETE/inbox/fan/{fan_id}/notes/{note_id}Looks up the note scoped to that fan_id (404 if not found/foreign), deletes it, and returns {ok, deleted_id}, rolling back and returning {ok: false} on a commit error.No
PATCH/inbox/fan/{fan_id}/notes/{note_id}Loads the note scoped to that fan_id (404 if missing), updates body and/or tags only for fields supplied (rejecting an empty body with 422), re-stamps updated_at, and saves.No
GET/inbox/fan/{fan_id}/of-listsReturns the OnlyFans native Collections/lists a fan belongs to, read from the local OfListDef/OfListMember tables (gated by the view_fan_spend team capability).No
GET/inbox/fan/{fan_id}/ppv-suggestComputes an advisory PPV price for one fan from their lifetime spend and resolved country's purchasing-power parity, using only local DB rows (no OF call).No
GET/inbox/fan/{fan_id}/profileBuilds and returns one fan's RFM/lifecycle/value-tier/chargeback profile for the inbox side panel, computed purely from local transaction rows (gated by the view_fan_spend team capability).No
POST/inbox/fan/{fan_id}/restrictRequires the fan_of_actions team capability and queues an OnlyFans restrict-or-unrestrict job for the fan depending on the request body's on flag.No
POST/inbox/fan/{fan_id}/retryRetries one previously failed inbox send by marking the old OutboundMessage row as retried and re-invoking the send pipeline with a fresh nonce, refusing if the row isn't in 'failed' state or was already retried.No
POST/inbox/fan/{fan_id}/suggest-replyReads the fan's chat thread, dossier, spend and PPV-pricing signals, then calls the owner's configured LLM (under a daily quota reservation) to return several reply-variant drafts for a human to review and send manually — it never sends anything itself.No
POST/inbox/fan/{fan_id}/sync-noteRequires fan_of_actions capability; queues a clear_note job if clear is set, otherwise pushes our concatenated local notes as the OnlyFans native note, or 400s if there's nothing to send.No
GET/inbox/fan/{fan_id}/timelineReturns a paginated timeline of a fan's purchases, refunds, subscription/PPV events and notes plus membership info, built entirely from local DB rows (gated by the view_fan_spend team capability).No
GET/inbox/media/coverServes a locally cached JPEG cover for a chat photo whose OF signed CDN URL expired, resolved by stable media path from the engine's downloaded covers directory; 404 if no cached copy exists.No
POST/inbox/refreshTouches the engine's chats-refresh heartbeat file and syncs the freshest engine feed into the Conversation/MessageStat tables; no-ops for BYO-OFAPI accounts or on any error.No
POST/inbox/sendSends a 1-on-1 DM by first running it through the content firewall and a global kill-switch, then through the gated automation executor (currently forced into shadow mode, so nothing is actually delivered to OnlyFans), logging the outcome and updating conversation state.No
GET/inbox/streamOpens an admin-authenticated SSE stream that forwards this owner's real-time inbox pub/sub events (new messages, typing, counts) to the open chat panel, with periodic keepalives.No
POST/inbox/ws-eventEngine-to-backend intake with no user auth (only a shared static X-Inbox-Event-Token header, disabled/404 if unconfigured): upserts one new OF message into the conversation tables idempotently and publishes an SSE nudge to open panels.No
POST/inbox/ws-pulseEngine-to-backend intake with no user auth (only a shared static X-Inbox-Event-Token header, disabled/404 if unconfigured): forwards ephemeral typing/unread-tip/cache-invalidation signals to SSE subscribers without writing anything to the DB.No
MethodPathDescriptionOwner-only
GET/landing-shareValidates the domain, then lists non-revoked, non-expired landing-share links for that domain (URL, day window, generated_at, expiry, hit count).No
POST/landing-shareCleans up expired shares, freezes a stats snapshot for the domain/day window, creates a new tokenized PdfExportShare row, writes the snapshot JSON to disk, logs an audit event, and returns the new share.No
DELETE/landing-share/{share_id}Looks up the share by id, 404s if missing or wrong kind, marks it revoked=1 (row kept for audit trail), logs an audit event, and returns 204.No
POST/landing-share/{share_id}/refreshLooks up the share by id (404 if missing/revoked/wrong kind, 400 if no domain stored), rebuilds the stats snapshot for the same day window, overwrites the on-disk view file for the existing token, updates config_json, logs an audit event, and returns the refreshed share (URL unchanged).No

Logs

MethodPathDescriptionOwner-only
GET/logsReturns the most recent transactions (payments, $0 FTL trials, and social 'pulse' events) with fan, amount, source persona/country, new/returning flags, and matched PPV content.No

Money breakdown

MethodPathDescriptionOwner-only
GET/money/breakdownReads and returns one paginated page of TransactionLogRow entries for a chosen money kind (ppv/tips/subscription/rebill/other/refund) plus row count and net/gross sums, scoped to a team-confined member's allowed fan sources, or merged across an account-switcher's resolved owner_ids, or the default owner/tenant context — read-only, no writes.No

Posting (wall)

MethodPathDescriptionOwner-only
GET/posting/capabilitiesReturns whether paid posts are allowed for the account and whether story publishing is enabled, so the UI can hide price fields and gray out the story button.No
GET/posting/listLists scheduled/sent posts from the DB, optionally filtered by status and kind, serializing each row (caption, media ids, price, timing, OF post id, last error, firewall state).No
POST/posting/previewRenders and returns the exact post body that would be sent to OnlyFans (via render_post_body) without writing anything or contacting OnlyFans.No
POST/posting/scheduleCreates a scheduled post row in the DB for the given caption/media/price/timing, returning 400 on validation or paid-post-not-allowed errors.No
POST/posting/{post_id}/cancelCancels a scheduled post by id, treating the home owner as the top role for permission purposes, returning 403/404 on permission or not-found errors.No

Promo (bundles, campaigns)

MethodPathDescriptionOwner-only
GET/promo/bundlesRefreshes the local promo mirror for the current page's owner, reads its OfBundle rows (3/6/12-month slots), and returns them with freshness/ops-enabled flags.No
POST/promo/bundlesValidates duration (3/6/12) and discount (0-50 step 5), then queues a create_bundle engine job and returns pending status with a nonce (or staged:false if the op gate is off).No
DELETE/promo/bundles/{bundle_id}Queues a delete_bundle engine job for the given bundle_id via the shared submit flow; does not write to OnlyFans directly.No
PUT/promo/bundles/{bundle_id}Validates the new duration/discount then queues an update_bundle engine job carrying bundle_id and the new values.No
GET/promo/campaignsResolves the requested scope of pages, refreshes each page's promo mirror, and returns their OfPromo rows (audience/kind/value/claims/expiry) tagged with page_owner_id.No
POST/promo/campaignsValidates audience/kind/value against OnlyFans limits, then queues a create_promo engine job deduped by audience:kind:value.No
DELETE/promo/campaigns/{promo_id}Queues a delete_promo engine job for the given promo_id (irreversible on OnlyFans' side once applied).No
POST/promo/campaigns/{promo_id}/stopQueues a stop_promo engine job so the promo stops handing out new claim slots.No
GET/promo/impactComputes what breaks if a trial/link/promo/bundle is deleted (which domains/countries lose their link) plus best-effort counts; 422 on bad kind, 404 if not found.No
GET/promo/ops/{nonce}Looks up the EngineJob for a queued promo operation by nonce and returns its status/attempts/error/timestamps; 404 if no such job.No

Shift requests

MethodPathDescriptionOwner-only
GET/shift/requestsLists shift requests for the caller's workspace (default status=pending, max 200); a member without the roster_manage capability sees only their own requests.No
POST/shift/requests/{request_id}Requires the roster_manage team capability; approves a pending shift request (opening an off-schedule shift for its requested minutes) or marks it denied.No
POST/shift/startClocks the caller's own operator in as a normal shift, or — if not gated that way — opens a time-boxed off-schedule shift (self-approved) or files a pending approval request, per the workspace's shift_self_start setting.No
GET/shift/stateReturns the caller's own shift status: on/off state, current shift window, clock-in flag, next scheduled start, and any pending off-schedule request, for the panel to render.No
POST/shift/stopCloses the caller's currently open shift (clock-in or off-schedule) by setting its end timestamp to now, returning clocked_in=false if none was open.No

Source domains

MethodPathDescriptionOwner-only
POST/source-domainsCreates, or idempotently returns, an owner-scoped 'source-only' Domain row for a given hostname, auto-namespacing the hostname if it collides with another owner's global row.No

Source layout

MethodPathDescriptionOwner-only
GET/source-layoutReturns the owner's (or selected account's) scanned OnlyFans campaigns with a fresh proposed source/domain per unassigned campaign, cached and sorted with pending-with-traffic work first.No
GET/source-layout/fan-sourcesLists the fan-source review queue (pending or manually-set rows) for the resolved page/creator/tenant scope, paginated by cursor and filtered by platform.No
POST/source-layout/fan-sources/assignAssigns a source key to each listed fan (after checking the caller is allowed on that page), writes an audit entry, commits, and invalidates links/fans/stats caches if anything changed.No
POST/source-layout/fan-sources/backfill-uniqueFinds fans with a single unambiguous candidate source and either previews them (dry run) or auto-assigns them, auditing and invalidating caches when applied.No
POST/source-layout/fan-sources/undoReverts a previous source assignment for each listed fan/override id (after page-access checks), writes an audit entry, commits, and invalidates caches if anything changed.No
POST/source-layout/llm-assignBulk-reassigns a validated domain source onto the given fan_ids that are currently in 'llm'-kind source rows, writes an audit log entry, and commits.No
GET/source-layout/llm-pendingLists fans whose LLM-guessed source is ambiguous (matches a generic channel and agrees with the current source), grouped by source with fan names and net revenue totals.No
POST/source-layout/rescanImmediately re-syncs of_campaigns from the engine's local feed and drops a control file requesting a deeper engine-side per-campaign fan walk; makes zero live OnlyFans requests itself.No
GET/source-layout/rescan-statusReturns the current progress/state of the engine's requested campaign walk and whether that deep walk is even available for this owner.No
MethodPathDescriptionOwner-only
GET/source-linksLists all source-link mappings newest-first, filtering out rows whose domain_source is outside the caller's team scope.No
POST/source-linksCreates a new active source-link binding (match type/value to a domain), rejecting sentinel or unknown domains and out-of-scope hosts, then writes an audit log entry.No
DELETE/source-links/{link_id}Deletes a source-link row by id (404 if missing or its domain is outside the caller's scope) and writes an audit log entry.No
PATCH/source-links/{link_id}Updates a source-link's domain/channel/active/notes fields (validating any new target domain and scope), then writes an audit log entry.No

Sources (fan spend by source)

MethodPathDescriptionOwner-only
GET/sources/{domain}/spendersLists paying fans attributed to one source domain via the seven-stage resolver, with range/lifetime and domain-chain (self/path) options; 404 if the domain is unknown, 403 if the caller lacks the spenders_view team capability or the source is outside their scope.No

Spenders

MethodPathDescriptionOwner-only
GET/spendersReturns the account-wide list of every fan who ever paid (from of_transactions, revenue-status filtered), with net/purchases/first-last dates, optionally range-limited; 403 for source-scoped team members.No

Stats

MethodPathDescriptionOwner-only
GET/statsAggregates click/impression traffic, CTR and revenue stats over a date range for the dashboard, with RBAC-filtered page/creator/tenant scope rollup and SWR caching on the default single-owner path.No
GET/stats/ab-duelResolves a time window from a named range or explicit from/to timestamps, then returns compute_ab_duel's A/B comparison stats (daily series, mobile, new-vs-old fans) for the given domain and tier.No

Telegram (fans, gate)

MethodPathDescriptionOwner-only
GET/tg/fansReturns the current tenant's Telegram fan list (search/country/paid/blocked/source filters, sort, cursor pagination) from local tables; rejects multi-account scope_kind/platform_account_id requests with 400.No
GET/tg/gateReturns the Telegram gate panel payload for the caller's own tenant: connection state, masked bot card, and /start stats bucketed by the requested period.Yes
PATCH/tg/gateValidates and partially updates the gate's settings (greeting/button/split/typing knobs plus the destinations/default_dest/rules trio), auto-migrating an env-only home-owner row to a DB row first, else 409 if no bot is connected.Yes
GET/tg/gate/avatarStreams the owner's stored Telegram bot avatar file from disk with private/short-lived caching, returning 404 if the owner has no connected bot row, no synced avatar, or the file is missing.Yes
POST/tg/gate/connectValidates a BotFather token via getMe, upserts the owner's bot row (preserving existing settings), registers the Telegram webhook with the row's secret, and best-effort syncs the bot profile.Yes
POST/tg/gate/disconnectBest-effort deletes the Telegram webhook then tombstones the owner's bot row (clears the token, disables it) while keeping settings/stats for a later reconnect.Yes
POST/tg/gate/profileValidates provided name/bio/short_bio against Telegram's length caps (422 if exceeded), pushes only the given fields via setMyName/setMyDescription/setMyShortDescription (502 on Telegram failure or rate-limit), then re-syncs and commits the row and returns the current profile shape.Yes
POST/tg/gate/syncPulls the bot's live Telegram profile (getMe, description, short description, and profile photo download-to-disk) into the DB row, commits, invalidates the turnstile cache, and returns the profile shape plus a synced flag and any per-step errors.Yes
POST/tg/gate/webhook/refreshRe-registers the Telegram webhook for the owner's already-connected bot using its stored secret, returning 502 if Telegram rejects or is unreachable.Yes

Telegram attribution

MethodPathDescriptionOwner-only
GET/tg-attributionComputes owner-scoped Telegram paying-fan attribution for a period (cached by owner/period), then applies telegram-attribution privacy redaction before returning.No

Telegram channel

MethodPathDescriptionOwner-only
GET/tg-channelReturns the owner's cached Telegram channel media-kit payload for a bucketed period: subscriber KPI/growth curve plus broadcast view/share/reaction scalars.No
POST/tg-channel/inviteMints or reuses a named invite link per traffic source via the Telegram user-session (the docstring says 'via @teaseappbot' but the code actually uses user_session.create_invite_link, not the bot, so join counts stay readable); 503 if no user-session, 422 on bad source, 502 on Telegram errors.Yes
GET/tg-channel/postsReturns the cached list of the newest 100 synced channel posts (views/forwards/replies/reactions, preview thumb, permalink), empty list if no channel/sync yet.No
POST/tg-channel/posts/syncSynchronously pulls the newest ~50 channel posts via the MTProto user-session, commits them, and invalidates the posts cache; 503 if no user-session configured, 502 on any Telegram failure.Yes
GET/tg-channel/posts/{msg_id}Returns one cached channel post with its per-day view-count series and engagement-rate percent (0 unless both views and subscriber count are known); 404 if not found.No
POST/tg-channel/posts/{msg_id}/track-linkCreates or repoints the owner's /t/<code> tracking link for one post (same code, new target_url on repeat calls) using the owner's first registered domain; 404 if no channel/post, 400 if no domain.Yes

Telegram fan detail

MethodPathDescriptionOwner-only
GET/tg-fan/{user_id}Fetches detail for one Telegram fan (by user_id) scoped to the current owner, raising 404 if that fan isn't found for the owner.No
MethodPathDescriptionOwner-only
GET/tg-linksRequires only require_admin; returns the per-link table (joins, attributed net revenue, spenders, cost, ROI via cached compute_tg_links) for the tenant plus channel-connected and user-session-configured flags.No
POST/tg-linksValidates kind/label/cost, requires a configured Telegram user-session, then mints a real named invite link on the connected channel via mint_tracked_link and returns the new row (422/404/503/502 on failure).Yes
POST/tg-links/syncRequires a configured Telegram user-session and calls sync_link_joins to pull each tracked link's importer/join list immediately, returning the count of newly synced joins.Yes
PATCH/tg-links/{link_id}Looks up the tenant-scoped link by id and updates label/donor/cost fields and active flag with validation, without touching the actual Telegram invite link itself.Yes

Telegram tribute (turnstile payments)

MethodPathDescriptionOwner-only
GET/tg-tributeReturns this owner's Tribute ("Приватка") KPI summary (net revenue, active subscribers, recent events) plus the connection state (connected/has_key/webhook_url), scoped by tenant and cached per period.No
POST/tg-tribute/connectStores the caller's own Tribute API key (400 if empty), mints a webhook token if missing, enables the connection, and returns {connected, has_key, webhook_url} without ever echoing the key.Yes
POST/tg-tribute/disconnectClears the caller's stored Tribute API key and disables the connection (keeping the webhook token for later reconnect), idempotently returning {connected: false}.Yes

Traffer payouts

MethodPathDescriptionOwner-only
GET/traffer-payoutsLists all traffer-payout arrangements (ordered newest first) plus the available domain sources, filtered to the caller's team scope if one applies.No
POST/traffer-payoutsCreates a traffer payout for a domain source after checking team scope, rejecting sentinel/unknown sources and duplicate arrangements (409), then audit-logs the create.No
DELETE/traffer-payouts/{payout_id}Deletes a traffer-payout row by id (404 if missing, scope-checked against its source) and writes an audit log entry.No
PATCH/traffer-payouts/{payout_id}Partially updates a traffer-payout row's fields (contractor, kind, amounts, ad spend, active, note), clearing the irrelevant amount on a kind switch, and audit-logs the change.No
MethodPathDescriptionOwner-only
GET/unassigned-linksRead-only listing of unmapped links (traffic seen but no source binding yet), ordered by most recently active.No

Vault

MethodPathDescriptionOwner-only
POST/vault/auto-tagAI-tags untagged vault items via DeepSeek text (title/folder/caption), writes ContentTag(source='ai') and flips ai_tagged, rate-limited and daily-capped per owner.No
GET/vault/collectionsLists the tenant's saved vault filter collections (VaultCollection rows), newest first.No
POST/vault/collectionsCreates a new named VaultCollection storing the current filter as JSON; 400 if the name is blank.No
DELETE/vault/collections/{collection_id}Deletes a saved VaultCollection by id (404 if not found); only removes the saved filter, not any files.No
PATCH/vault/collections/{collection_id}Renames a VaultCollection and/or replaces its stored filter JSON.No
GET/vault/cover/{media_id}Serves the locally re-hosted vault thumbnail (OFAPI cover_path, else the gateway engine's per-owner covers file) or 404; never fetches the remote CDN.No
GET/vault/dormantReturns vault items with zero send/sale activity in the given window, computed as a read-only set-diff over content performance.No
GET/vault/drop/{folder_id}Returns a folder's items classified by role (video/photoset/BTS) plus the ordered media-id bundle for taking the whole drop.No
GET/vault/folder/{folder_id}Returns every media id in one folder/album; always passes through the DLP export gate first since it's an unbounded full-folder dump.No
POST/vault/foldersValidates a non-empty folder name and submits a create_list vault_op job to the engine queue, returning {status:'pending', nonce} or a disabled/staged response if the vault-ops engine lane isn't live.No
DELETE/vault/folders/{list_id}Requires an explicit confirm flag, then submits a delete_list vault_op job (irreversible OnlyFans folder deletion) via the shared submit/audit pipeline.No
PUT/vault/folders/{list_id}Validates a non-empty new name and submits a rename_list vault_op job for the given folder id, returning pending status or disabled/staged.No
DELETE/vault/folders/{list_id}/mediaRequires confirm=true, cleans/dedupes the submitted numeric media ids (max 100), and submits a remove_media vault_op job for that folder.No
POST/vault/folders/{list_id}/mediaCleans/dedupes the submitted numeric media ids (max 100) and submits an add_media vault_op job to place those files into the folder.No
GET/vault/gapsReturns high-demand/low-supply content tags plus the unsorted/untagged backlog to suggest what to shoot next.No
GET/vault/ops/enabledReturns which of the five vault folder operations are currently live, based only on the engine's freshness manifest (fail-closed to all-false if stale/missing).No
GET/vault/ops/{nonce}Looks up the EngineJob by nonce and job_type='vault_op' and returns its status, error text, and attempt count, or 404 if no such job exists.No
POST/vault/ops/{nonce}/retryRe-submits a failed/stale vault_op job under the SAME nonce (to avoid OnlyFans creating a duplicate folder on retry), blocked with 409 if it's still queued/inflight or the op is currently disabled.No
GET/vault/performanceReturns top vault media and tags ranked by revenue with unlock rate over a time window.No
GET/vault/recommendRecommends unseen vault content for a cohort (from content_id or audience filters), ranked by tag fit times unlock rate, excluding already-sent items.No
POST/vault/scanStarts a background vault re-index from the gateway feed, or (deep=1, gateway-only) requests a full engine crawl and still kicks the re-index; 400 if unavailable.No
GET/vault/scan/statusReturns indexed folder/item/tagged counts, last-indexed time, and whether a scan is currently running.No
GET/vault/searchSearches the vault index by query/scope/uploader and returns media grouped by folder plus loose items; export=true additionally passes the DLP export gate.No
POST/vault/uploadValidates a media file (type/size) with the vault upload service, asks the OnlyFans engine for a put URL via require_admin_principal auth + content/media_upload team capability, writes an audit row, and returns the upload id/state (bytes never pass through the backend).No
GET/vault/upload/enabledReturns whether vault uploads are currently enabled and the current size/type limits by reading the engine's readiness gate file, guarded by require_admin plus team_fence (no DB access).No
GET/vault/upload/{upload_id}Quietly settles any stale in-flight uploads, then looks up the upload row by id and returns its serialized status (queued/ready/finishing/done/failed/shadow), including a fresh PUT target when state is 'ready'; 404 if not found.No
POST/vault/upload/{upload_id}/abortLooks up the upload row (404 if missing), asks the engine to abort it and clean up any partial multipart bytes in the OnlyFans bucket, writes an audit entry, and commits.No
POST/vault/upload/{upload_id}/completeValidates the upload is in 'ready' state (409 otherwise), submits the browser-reported ETags/parts to the engine to finalize the multipart upload, writes an audit row on success, and returns the result (including offline/busy states).No
GET/vault/uploadsQuietly settles stale in-flight uploads, then returns the most recent uploads (default limit 20) serialized along with the current enabled/limits gate info.No
DELETE/vault/{media_id}/blockClears the local 'do not send' flag on a vault item.No
POST/vault/{media_id}/blockSets a local 'do not send' flag on a vault item, which the send firewall then enforces across DM, chain, campaign, and feed.No
DELETE/vault/{media_id}/favoriteClears the local favorite flag on a vault item.No
POST/vault/{media_id}/favoriteSets the local favorite flag on a vault item.No
PUT/vault/{media_id}/noteSets the free-text local note on a vault item, or clears it when the text is empty.No
GET/vault/{media_id}/scriptsFinds scripts that reference this media id (via Script.media_ids_json), returning an empty/unavailable result if that column doesn't exist yet.No

Wall posts analytics

MethodPathDescriptionOwner-only
GET/wall-posts/analyticsLists the owner's wall posts with stats, enriches rows with revenue-window/inflow data, and returns connected:false with an empty list instead of a 500 if the query fails.No
GET/wall-posts/insightsReturns an AI-tag breakdown and a 24-hour best-posting-time histogram for the owner's posts, each independently falling back to safe empty defaults on failure.No
GET/wall-posts/{of_post_id}Fetches one tenant-scoped post by of_post_id, returns found:false if missing, else its fields plus a per-snapshot stat series, revenue window, and new-fans-after-post count.No

On this page