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
| Method | Path | Purpose |
|---|---|---|
POST | /accounts/{of_user_id}/exports | Start a job → 202 |
GET | /accounts/{of_user_id}/exports | History, paginated |
GET | /accounts/{of_user_id}/exports/{job_id} | One job's status |
GET | /accounts/{of_user_id}/exports/{job_id}/download | Download the ZIP |
POST | /accounts/{of_user_id}/exports/{job_id}/cancel | Stop 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
}'| Field | Type | Notes |
|---|---|---|
data_types | string[] | Non-empty subset of account, subscribers, transactions, fans, earnings, messages |
since | "YYYY-MM-DD" | null | Lower bound |
until | "YYYY-MM-DD" | null | Upper bound. Both null means all-time |
include_media | boolean | Download 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:
| Type | Files |
|---|---|
subscribers, transactions, fans | .json and .csv |
account, earnings | .json only |
messages | one 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.