# The Only API — agent brief

REST API for OnlyFans and Fansly. 529 endpoints. Docs: https://docs.theonlyapi.com

## Auth

Every request: `X-API-Key: <key>` — header only, never a query parameter.
Base URL: `https://theonlyapi.com/api/crm/{crm_id}`

Seven routes are unscoped and live at the bare origin (no key needed):
`GET /health`, `GET /api/whoami`, `POST /api/auth/register`,
`POST /api/auth/login`, `POST /api/auth/start-email-verification`,
`POST /api/auth/verify-email`, `POST /api/crm/register`.

`GET /api/whoami` resolves a key to its `crm_id`.

## Two surfaces — pick deliberately

1. **CRM layer** (95 endpoints, OnlyFans + Fansly). Normalized. Data is a
   sibling key: `{ success, subscribers: [...], total, limit, offset, hasMore }`.
   Not universal — some routes return a bare object or array.

2. **Passthrough** `/api2/v2/*` (434 endpoints, **OnlyFans only**). Always
   `{ success, status_code, data }` where `data` is OnlyFans' own payload
   including its own `list`/`hasMore`. Requires a `user-id: <of_user_id>`
   header naming the connected account. A Fansly `user-id` here is rejected.

Default to the CRM layer. `of_user_id` identifies an account on either platform.

## Rules that are easy to get wrong

**Earnings — never `SUM(amount)`.** Chargebacks are not negative rows; the
original positive row's `status` flips to `undo`. A plain sum overstates
earnings by twice the chargeback. Sign by status: `done`/`loading` → +amount,
`undo` → −amount, anything else → 0 (ignore, do not assume positive).

**Pagination — three conventions.**
- `limit`/`offset` with a real `total` (the `/cached` routes).
- `limit`/`offset` with only `hasMore` (live reads).
- Cursor: `marker` for `/purchases`, `id` for `/messages`.

Offsets are **per item**: advance `offset += len(page)`, never `+= limit`.
`GET /chats` ignores `limit` and returns 10–14 regardless. A short page from
`/messages` is **not** the end of the conversation. Terminate on `hasMore`
(or a genuinely empty page), never on `len(page) < limit`.

Note `hasMore` is camelCase; everything else is snake_case.

**Writes are gated.** OnlyFans writes (DMs, mass messages, payout requests)
return `403 Write actions disabled for this account` until
`allow_of_write_actions` is enabled via
`PATCH /accounts/{of_user_id}/polling`.

**Mass DM dry-runs by default.** `POST /messages/mass` has
`dry_run: true` unless explicitly set false — it previews recipient count and a
sample without sending. Always preview first; a send cannot be recalled.
OnlyFans only (Fansly returns 501).

**Async jobs return 202.** Refresh, backfill and export routes return `202` and
run in the background; poll the matching `/refresh/status` route or listen on
`GET /events/stream`. A `409` (or `already_running: true`) means one is
already running — treat that as success, do not retry.

**Unscoped routes use a different host.** `GET /health`, `GET /api/whoami`,
`POST /api/crm/register` and `POST /api/auth/*` are served by
`https://api.theonlyapi.com` — on theonlyapi.com they 404 or are intercepted.

**Errors have no single shape.** Read `error` as the message; treat
`success`, `code` and `correlation_id` as optional. Do not require
`success` to be present.

**`_notice` in a response means the key was not sent.** Unauthenticated JSON is
rewritten by honeypot middleware (arrays become `{_notice, data: [...]}`). Also:
there is no User-Agent check — a decoy response means you are on a decoy
**path** (e.g. `/api/health`), not that your client looked wrong.

**Money scale differs by platform.** CRM routes normalize it; raw passthrough
responses do not. Do not mix the two in one calculation.

## Rate limits

1000/min default, 100/min for writes and key management, 20/min for login routes.
(These are the deployed values; the spec still publishes looser fallbacks.)
`GET /health` and `GET /events/stream` are exempt. Monthly quota is separate:
free is 1,000 calls, paid is unlimited (`api_calls_limit: -1`). Check
`GET /usage`.

Platform-side limits are **not** enforced for you: bursts ~1 req/s per account are
fine, sustained >5 req/s risks the account being flagged. Prefer `/cached`
routes, which cost zero platform requests.

## Realtime

Nine event types: `new_subscriber`, `renewed_subscriber`,
`expired_subscriber`, `new_tip`, `new_message`, `new_purchase`,
`balance_increased`, `polling_paused` (`payout_completed` is reserved and not
emitted). `*` is a webhook subscription wildcard only.

Webhook signature: `sha256=HMAC_SHA256(secret, "{timestamp}." + raw_body)`.
Sign the **raw bytes** — re-serializing parsed JSON breaks it. Timestamp is in
`X-OnlyAPI-Timestamp`, signature in `X-OnlyAPI-Signature`. Retries:
5s, 30s, 5m, 30m, 2h; five consecutive failures deactivates the webhook until
re-enabled.

SSE (`GET /events/stream`) has no replay — backfill gaps from
`GET /events?since=<last occurred_at>`.

## Resources

- Guides, full text: https://docs.theonlyapi.com/llms-guides.txt
- CRM reference: https://docs.theonlyapi.com/llms-crm.txt
- Passthrough reference: https://docs.theonlyapi.com/llms-of.txt
- Any page as Markdown: append `.md` to its URL
- OpenAPI 3.1 spec: https://theonlyapi.com/api/openapi.json
- Docs MCP (read these docs, no auth): https://docs.theonlyapi.com/mcp
- API MCP (call the API, 59 tools): https://theonlyapi.com/mcp
