The Only API docs

Data exports

Export messages, tips, subscribers, fans and earnings as a ZIP.

An async, per-account export. You start a job, it walks the selected data — cheap cached tables first, expensive live platform walks last — packages CSV + JSON into a ZIP, and reports progress live over SSE. The ZIP stays downloadable for 7 days.

Endpoints

MethodPathPurpose
POST/accounts/{of_user_id}/exportsStart a job → 202
GET/accounts/{of_user_id}/exportsHistory, paginated
GET/accounts/{of_user_id}/exports/{job_id}One job's status
GET/accounts/{of_user_id}/exports/{job_id}/downloadDownload the ZIP
POST/accounts/{of_user_id}/exports/{job_id}/cancelStop a running job
DELETE/accounts/{of_user_id}/exports/{job_id}Delete the job and its ZIP

Start an export

curl -X POST "$BASE/api/crm/$CRM/accounts/$OFUID/exports" \
  -H "X-API-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "data_types": ["account","subscribers","transactions","fans","earnings","messages"],
        "since": "2026-06-01",
        "until": null,
        "include_media": false
      }'
FieldTypeNotes
data_typesstring[]Non-empty subset of account, subscribers, transactions, fans, earnings, messages
since"YYYY-MM-DD" | nullLower bound
until"YYYY-MM-DD" | nullUpper bound. Both null means all-time
include_mediabooleanDownload message media. Required: messages must be in data_types, or this is silently ignored. OnlyFans only — a Fansly export records a warning and lists attachment URLs in the message JSON instead

The 202 response includes a warning worth heeding:

{
  "success": true,
  "job": { "job_id": "f01152d3…", "status": "queued",  },
  "warning": "Messages and media are fetched live from the platform — this can take several minutes and counts against your API quota."
}

Cost is not uniform across data types

account, subscribers, fans and (on OnlyFans) transactions are cached reads and nearly free. earnings costs 1–2 live calls. Fansly transactions falls back to a live wallet walk of up to 20 pages when its cache is cold. messages — and especially include_media — is the expensive one. Export messages when you need them, not by default.

One export at a time

A second request while one is in flight returns the existing job rather than an error:

{ "already_running": true, "job": {  } }

Unless that job is stale — no terminal status for over 30 minutes — in which case it is marked failed and yours starts fresh.

Track progress

Poll:

curl "$BASE/api/crm/$CRM/accounts/$OFUID/exports/$JOB_ID" -H "X-API-Key: $KEY"

Or stream, which is what the dashboard does:

curl -N "$BASE/api/crm/$CRM/events/stream" -H "X-API-Key: $KEY"
event: export.progress
data: {"job_id":"…","status":"running","phase":"messages","phase_index":6,
       "phase_total":7,"counts":{…},"updated_at":"…"}

event: export.complete
data: {"job_id":"…","status":"complete",…}

phase_index / phase_total give you a real progress bar; phase names what is running. Because messages come last, a job can sit at high progress for a long time — that is the live walk, not a stall.

Download

curl -L -o export.zip \
  "$BASE/api/crm/$CRM/accounts/$OFUID/exports/$JOB_ID/download" \
  -H "X-API-Key: $KEY"

Responds application/zip. What each type produces differs:

TypeFiles
subscribers, transactions, fans.json and .csv
account, earnings.json only
messagesone JSON per conversation, plus messages.csv and index.json

Available for 7 days, then cleaned up. DELETE the job to remove it sooner.

Statuses are queued, running, complete, failed, canceled, expired — the terminal success value is complete, not completed. A poll loop waiting for "completed" never fires.

Cancelling

curl -X POST "$BASE/api/crm/$CRM/accounts/$OFUID/exports/$JOB_ID/cancel" \
  -H "X-API-Key: $KEY"

Worth doing if you started a media export by accident — it will otherwise keep consuming quota until it finishes.

There is a UI for this

Dashboard → Export drives exactly these endpoints. If a human needs a one-off download rather than an integration, send them there.

On this page