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
| Method | Path | Description | Owner-only |
|---|---|---|---|
| GET | /ai/voice | Returns the owner's current AI voice config (persona, tone, pilot settings), merging stored overrides onto system defaults. | Yes |
| PUT | /ai/voice | Deep-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/autofill | Rate-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)
| Method | Path | Description | Owner-only |
|---|---|---|---|
| GET | /automation/campaigns | Lists all campaign rows for the current owner, newest first, each shaped with status/mode/version/managed_by. | No |
| POST | /automation/campaigns | Creates 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}/preview | Read-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-live | Flips 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}/results | Read-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}/soak | Evaluates 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}/start | Enqueues 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}/uplift | Read-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-rotation | Moves 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/active | Read-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/candidates | Read-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/rollback | Reverts 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-policy | Read-only: returns the current global in-app-webview escape policy (mode/ios/android) shared across all tenants. | No |
| POST | /automation/escape-policy | Sets 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-switch | Read-only: returns whether the global cross-tenant automation kill-switch file flag is currently active. | No |
| POST | /automation/kill-switch | Sets the global kill-switch flag (halting automation for every tenant) and audits the change. | Yes |
| GET | /automation/optin | Read-only: returns (creating with all-off defaults if missing) the current owner's single automation consent/opt-in row. | No |
| POST | /automation/optin | Upserts 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/overview | Read-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/posts | Lists all scheduled-post rows for the current owner, newest first. | No |
| POST | /automation/posts | Creates 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-offers | Lists all PPV offer rows for the current owner, newest first. | No |
| POST | /automation/ppv-offers | Creates 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/sequences | Lists all message-sequence rows for the current owner, newest first. | No |
| POST | /automation/sequences | Creates 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/templates | Lists all message-template rows for the current owner, newest first. | No |
| POST | /automation/templates | Creates 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
| Method | Path | Description | Owner-only |
|---|---|---|---|
| GET | /broadcasts | Lists all broadcast campaigns for the tenant and returns them alongside the account-wide send-lane mode (whether live sending is currently enabled). | No |
| POST | /broadcasts | Creates 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/automessages | Lists 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/automessages | Upserts 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-log | Returns 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-fire | Dry-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/preview | Resolves 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/segments | Lists this owner's non-deleted saved fan segments, each with its filters, a human-readable description, and a live member count. | No |
| POST | /broadcasts/segments | Validates 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/preview | Validates 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}/members | Returns 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-plan | Computes 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/stats | Refreshes 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}/buyers | Looks 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}/cancel | Cancels a campaign, returning 404 if it doesn't exist and {ok:true} on success. | No |
| POST | /broadcasts/{campaign_id}/pause | Pauses an in-progress campaign's sending, returning 404 if it doesn't exist and {ok:true} on success. | No |
| POST | /broadcasts/{campaign_id}/resume | Resumes a paused campaign's sending, returning 404 if it doesn't exist and {ok:true} on success. | No |
| POST | /broadcasts/{campaign_id}/retry-failed | Requeues 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}/schedule | Expands 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
| Method | Path | Description | Owner-only |
|---|---|---|---|
| POST | /campaigns/assign-source-bulk | Binds 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-source | Binds 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}/hidden | Hides 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-source | Reverts 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
| Method | Path | Description | Owner-only |
|---|---|---|---|
| GET | /scripts | Lists 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 | /scripts | Validates 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}/used | Records that a script was used (updates its use-tracking/timestamp), returning 404 if the script doesn't exist. | No |
Chatters (team, shifts, KPIs)
| Method | Path | Description | Owner-only |
|---|---|---|---|
| GET | /chatters | Returns the team roster ordered by id; a self-scoped chatter sees only their own operator row. | No |
| POST | /chatters | Creates a new operator record in the roster; requires the roster_manage team capability and is blocked for self-scoped chatters. | No |
| POST | /chatters/attribute | Manually pins (or clears) which operator a sale is credited to, overriding automatic attribution; requires the attribution_edit capability. | No |
| GET | /chatters/audit/messages | Returns 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-in | Opens 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-out | Closes the operator's currently open clock-in shift, setting end_ts=now and returning worked seconds. | No |
| GET | /chatters/clock-status | Reports whether the operator currently has an open clock-in shift, including its shift id and start time. | No |
| GET | /chatters/fan-kpis | Computes account-level fan economics (ARPPU/APC/APV/ARPNU, new-vs-paying) for the window from of_transactions and of_fans. | No |
| GET | /chatters/fan-pricing | Returns a suggested PPV price and spend tier for one fan, computed from that fan's transaction history. | No |
| GET | /chatters/forecast | Returns a run-rate revenue projection for the next 30 days plus subscriber health metrics. | No |
| GET | /chatters/goals | Returns 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/goals | Sets 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/leaderboard | Computes 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.csv | Streams 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/quality | Returns quality KPIs (response time, PPV unlock rate, messages per sale) for the account or one operator over the window. | No |
| GET | /chatters/shifts | Lists shifts overlapping the given window (default all time); self-scoped chatters see only their own shifts. | No |
| POST | /chatters/shifts | Creates 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/waiting | Lists 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
| Method | Path | Description | Owner-only |
|---|---|---|---|
| GET | /content | Lists top vault media ranked by buyers count, tips, or period sales from content_sale, with total count. | No |
| GET | /content/bundles | Aggregates 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}/status | Builds a fan's bought/sent/free media badge map from live chat + content_sale, writing results back to MediaSend. | No |
| GET | /content/for-payment | Resolves 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-cover | Serves 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/search | Searches content_sale for media sold near a given price/time, ranked by closeness, to manually link an unmatched PPV. | No |
| GET | /content/stats | Returns 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}/cohorts | Groups 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-price | Suggests 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}/caption | Looks 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}/sends | Aggregates our outbound sends containing this media joined to purchase status: sent, opened, purchased, revenue, buyers. | No |
| POST | /content/{media_id}/tags | Adds 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
| Method | Path | Description | Owner-only |
|---|---|---|---|
| GET | /requests | Lists content requests, optionally filtered by status and/or fan_id, returned under an "items" key. | No |
| POST | /requests | Creates a new proof/custom content request for a fan (chatter-initiated) and returns its id and status. | No |
| POST | /requests/{request_id}/cancel | Cancels an open content request; returns 409 if the request is not in an open state. | No |
| POST | /requests/{request_id}/deliver | Marks a fulfilled content request as delivered (proof was sent to the fan); returns 409 if not in 'fulfilled' state. | No |
| POST | /requests/{request_id}/respond | Applies 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)
| Method | Path | Description | Owner-only |
|---|---|---|---|
| GET | /dashboard/blocks | Lists 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/blocks | Validates 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/presets | Idempotently 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/live | Kicks off an async revenue refresh, then returns a domain-scoped live feed of recent clicks, subscriptions, and payments. | No |
| GET | /dashboard/metrics | Computes 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/library | Returns 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
| Method | Path | Description | Owner-only |
|---|---|---|---|
| GET | /export/shares | Lists 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}/revoke | Sets 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-channel | Validates 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/manifest | Returns 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/share | Validates 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
| Method | Path | Description | Owner-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}/transactions | Returns 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)
| Method | Path | Description | Owner-only |
|---|---|---|---|
| GET | /fans/attributions | Returns 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/recent | Reads 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-now | Synchronously 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/export | Streams 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}.csv | Streams 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/start | Starts (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/status | Returns 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/stop | Marks 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/backfill | Scans 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/overview | Returns 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}/attribution | Returns 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}/full | Returns 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-attribution | Builds 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}/segments | Checks 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
| Method | Path | Description | Owner-only |
|---|---|---|---|
| GET | /fansly/accounts/{platform_account_id}/fbuddy-parity/runs | Resolves 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/runs | Resolves 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}/comparisons | Validates 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}/finalize | Finalizes 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}/manifest | Resolves 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/attribution | Resolves 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/content | Resolves 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/dashboard | Validates 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/discover | Resolves 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}/lists | Validates 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}/notes | Validates 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}/source | Validates 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}/tags | Validates 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/fans | Returns 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/export | Builds 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-readiness | Validates 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/plans | Resolves 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-links | Resolves 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)
| Method | Path | Description | Owner-only |
|---|---|---|---|
| GET | /free-pool | Returns 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/dress | Dresses 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-batch | Auto-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-reco | Returns 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/reclaim | Pulls 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/suggest | Dry-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-split | Returns 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/undo | Collapses 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
| Method | Path | Description | Owner-only |
|---|---|---|---|
| GET | /inbox/conversations | Returns 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}/messages | Live-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}/state | Applies 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/enabled | Returns 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/tags | Scans 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}/actions | Settles 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}/discount | Requires 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}/economics | Reads 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}/label | Sets 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}/unsend | Requires 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}/notes | Fetches 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}/notes | Creates 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-lists | Returns 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-suggest | Computes 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}/profile | Builds 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}/restrict | Requires 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}/retry | Retries 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-reply | Reads 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-note | Requires 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}/timeline | Returns 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/cover | Serves 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/refresh | Touches 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/send | Sends 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/stream | Opens 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-event | Engine-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-pulse | Engine-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 |
Landing share links
| Method | Path | Description | Owner-only |
|---|---|---|---|
| GET | /landing-share | Validates 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-share | Cleans 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}/refresh | Looks 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
| Method | Path | Description | Owner-only |
|---|---|---|---|
| GET | /logs | Returns 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
| Method | Path | Description | Owner-only |
|---|---|---|---|
| GET | /money/breakdown | Reads 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)
| Method | Path | Description | Owner-only |
|---|---|---|---|
| GET | /posting/capabilities | Returns 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/list | Lists 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/preview | Renders 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/schedule | Creates 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}/cancel | Cancels 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)
| Method | Path | Description | Owner-only |
|---|---|---|---|
| GET | /promo/bundles | Refreshes 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/bundles | Validates 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/campaigns | Resolves 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/campaigns | Validates 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}/stop | Queues a stop_promo engine job so the promo stops handing out new claim slots. | No |
| GET | /promo/impact | Computes 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
| Method | Path | Description | Owner-only |
|---|---|---|---|
| GET | /shift/requests | Lists 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/start | Clocks 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/state | Returns 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/stop | Closes 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
| Method | Path | Description | Owner-only |
|---|---|---|---|
| POST | /source-domains | Creates, 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
| Method | Path | Description | Owner-only |
|---|---|---|---|
| GET | /source-layout | Returns 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-sources | Lists 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/assign | Assigns 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-unique | Finds 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/undo | Reverts 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-assign | Bulk-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-pending | Lists 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/rescan | Immediately 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-status | Returns the current progress/state of the engine's requested campaign walk and whether that deep walk is even available for this owner. | No |
Source links
| Method | Path | Description | Owner-only |
|---|---|---|---|
| GET | /source-links | Lists all source-link mappings newest-first, filtering out rows whose domain_source is outside the caller's team scope. | No |
| POST | /source-links | Creates 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)
| Method | Path | Description | Owner-only |
|---|---|---|---|
| GET | /sources/{domain}/spenders | Lists 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
| Method | Path | Description | Owner-only |
|---|---|---|---|
| GET | /spenders | Returns 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
| Method | Path | Description | Owner-only |
|---|---|---|---|
| GET | /stats | Aggregates 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-duel | Resolves 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)
| Method | Path | Description | Owner-only |
|---|---|---|---|
| GET | /tg/fans | Returns 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/gate | Returns 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/gate | Validates 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/avatar | Streams 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/connect | Validates 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/disconnect | Best-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/profile | Validates 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/sync | Pulls 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/refresh | Re-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
| Method | Path | Description | Owner-only |
|---|---|---|---|
| GET | /tg-attribution | Computes owner-scoped Telegram paying-fan attribution for a period (cached by owner/period), then applies telegram-attribution privacy redaction before returning. | No |
Telegram channel
| Method | Path | Description | Owner-only |
|---|---|---|---|
| GET | /tg-channel | Returns 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/invite | Mints 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/posts | Returns 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/sync | Synchronously 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-link | Creates 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
| Method | Path | Description | Owner-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 |
Telegram invite links
| Method | Path | Description | Owner-only |
|---|---|---|---|
| GET | /tg-links | Requires 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-links | Validates 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/sync | Requires 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)
| Method | Path | Description | Owner-only |
|---|---|---|---|
| GET | /tg-tribute | Returns 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/connect | Stores 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/disconnect | Clears 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
| Method | Path | Description | Owner-only |
|---|---|---|---|
| GET | /traffer-payouts | Lists 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-payouts | Creates 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 |
Unassigned links
| Method | Path | Description | Owner-only |
|---|---|---|---|
| GET | /unassigned-links | Read-only listing of unmapped links (traffic seen but no source binding yet), ordered by most recently active. | No |
Vault
| Method | Path | Description | Owner-only |
|---|---|---|---|
| POST | /vault/auto-tag | AI-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/collections | Lists the tenant's saved vault filter collections (VaultCollection rows), newest first. | No |
| POST | /vault/collections | Creates 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/dormant | Returns 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/folders | Validates 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}/media | Requires 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}/media | Cleans/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/gaps | Returns high-demand/low-supply content tags plus the unsorted/untagged backlog to suggest what to shoot next. | No |
| GET | /vault/ops/enabled | Returns 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}/retry | Re-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/performance | Returns top vault media and tags ranked by revenue with unlock rate over a time window. | No |
| GET | /vault/recommend | Recommends 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/scan | Starts 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/status | Returns indexed folder/item/tagged counts, last-indexed time, and whether a scan is currently running. | No |
| GET | /vault/search | Searches 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/upload | Validates 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/enabled | Returns 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}/abort | Looks 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}/complete | Validates 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/uploads | Quietly 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}/block | Clears the local 'do not send' flag on a vault item. | No |
| POST | /vault/{media_id}/block | Sets 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}/favorite | Clears the local favorite flag on a vault item. | No |
| POST | /vault/{media_id}/favorite | Sets the local favorite flag on a vault item. | No |
| PUT | /vault/{media_id}/note | Sets the free-text local note on a vault item, or clears it when the text is empty. | No |
| GET | /vault/{media_id}/scripts | Finds 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
| Method | Path | Description | Owner-only |
|---|---|---|---|
| GET | /wall-posts/analytics | Lists 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/insights | Returns 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 |