API — Connecting an account
Endpoints that move a platform account from "extension has a session" to "Platform has a bound, syncing account" — token minting, saved proxies, session import, and the browser-profile v2 provisioning chain.
All endpoints on this page are mounted by make_connect_router() at
/api/connect (app/main.py:134-135). They are called by the browser
extension and by Tease Browser during account connection — not by the panel's
own SPA screens.
POST /api/connect/token
Mints a short-lived, single-use connect token the app uses to open an
account's live browser tab. Auth: panel session cookie, and the caller's role
must have the browser tab granted (403 browser_not_granted otherwise).
Response: { "token": string, "ttl": number } (seconds).
This is a real permission boundary
The panel's usual session_scope fence covers /api/admin/*. This router
has a different prefix, so /token checks browser access explicitly —
otherwise a chatter's session could open a live OnlyFans tab it was never
granted.
GET /api/connect/extension/start
Tokenless login for the browser extension: a top-level navigation so the
panel's session cookie rides along. An already-logged-in creator is redirected
straight back to the extension with a fresh connect token in the URL
fragment (never sent to a server, never logged); a logged-out one is
bounced through the panel's own login first. Query param: redirect_uri
(validated — 400 bad redirect_uri if it doesn't match the extension's
expected shape).
GET /api/connect/proxies
Lists the caller's saved proxies, for the extension's proxy picker before
import. Auth: Authorization: Bearer <connect token> — a peek, it does not
consume the token (only /import does). Returns 401 if the token is missing
or invalid; a workspace owner (not a creator) gets {"items": []}.
Response: { "items": [{ id, name, host, port, protocol, has_password, last_status }] }. Never returns the proxy password.
POST /api/connect/proxies/{proxy_id}/lease
Short credential lease for Tease Browser: resolves one saved proxy to
{ server, protocol, username, password, expires_at }, where server is a
bare host:port (no scheme — the shell builds the scheme from protocol
itself; changing this shape breaks the shell's proxy setup). Same bearer-token
auth as /proxies, same non-consuming peek. Errors: 404 (proxy not found for
this owner), 503 (stored password can't be decrypted right now).
POST /api/connect/import
Receives a captured OnlyFans (or Fansly) session from the extension and hands
it to the connection engine — the panel itself never touches OnlyFans here.
Auth: Authorization: Bearer <connect token>, and this call consumes it
(single-use). Rate-limited per owner and per IP (429 on either limit).
- Body: JSON with
platform, and for OnlyFansauth_id,cookies,xbc,user_agent; Fansly takes a different shape (local_storage-carried session, noxbc) and is routed internally to its own handler. - If
saved_proxy_idis present, the account's pod is provisioned on that exit before import — an OF account never imports onto a floating exit. A page already bound to a different saved proxy gets409(changing exit is a separate, explicit action). - Errors:
401(bad/expired/reused token),422(missing session material, unknown platform, or the engine rejected the session),503(engine unavailable),404(the chosen proxy wasn't found for this owner). - Success:
{ "ok": true, "account": "<external id>" }.
WS /api/connect/ws
Bridges the panel to the tunnelled connection engine for the live-view flow.
Query param: token (the same single-use connect token, consumed on
connect). Only one live socket per owner at a time — a second concurrent
connection is refused with an error/busy frame instead of opening. Dead
engine → a clean error/engine_unavailable frame, never a raw disconnect.
Browser profile v2 (additive)
Four more endpoints extend the same import flow with a durable,
pre-provisioned "browser profile" instead of leasing a proxy at import time.
They live behind the browser_profile_v2_api_enabled setting — when it's
off, all four return a plain 404, not a 403 (no capability hint is leaked
while the feature is dark).
| Endpoint | Method | Purpose |
|---|---|---|
/api/connect/browser-profiles/drafts | POST | Allocate a draft profile for a connect_attempt_id + device installation. |
/api/connect/browser-profiles/{id}/prepare | POST | Bind the draft to a platform account and start its sync run. |
/api/connect/browser-profiles/{id}/proxy-lease | POST | Lease the profile's own bound proxy (no substitution). |
/api/connect/browser-profiles/identity-receipt | POST | Activate a profile from a signed engine identity receipt (X-Engine-Receipt-Signature header). |
prepare returns the profile plus platform_account_id, governor_id,
sync_run_id, sync_state, account_created. Errors across this family are
mostly 409 (a conflicting profile/account state) and 503 (the platform
request-limit governor isn't configured) — 422 is not used here, bodies are
strict Pydantic models that FastAPI itself rejects on shape mismatch.
What's next
For what happens after a session lands — sessions, activity, and the sync engine reporting progress — see API — Browser runtime.
Connections
What Account Platform gives to and takes from the other three services, proven in code.
API — Browser runtime
The endpoint family Tease Browser and the desktop app talk to after a device is paired — accounts, workspace state, proxy leases, activity, session lanes, and the panel-side sync feed. All under one prefix, one auth envelope.