The Only API docs

Bulk account import

Paste many accounts at once — preview, run, and answer 2FA prompts as they park.

Connecting accounts one at a time is fine for a handful. For an agency onboarding dozens, paste them all at once: the importer parses the paste, tells you exactly what will happen before touching anything, then works through the rows in the background — parking any that need a 2FA code until you supply one.

All eight routes are panel-scoped and authenticated like every other CRM route — see the Bulk Import reference for full request and response schemas.

The flow

Preview — no side effects

curl -X POST "$BASE/api/crm/$CRM/import/preview" \
  -H "X-API-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "text": "[email protected],hunter2,http://user:pass@host:port\[email protected],hunter3",
        "default_platform": "onlyfans"
      }'

Nothing is written and no login is attempted. You get back what the importer would do: the detected format, which rows are valid, which lane each takes, and precisely why row 47 is bad.

{
  "success": true,
  "format": "bare",
  "delimiter": ",",
  "has_header": false,
  "columns": ["email", "password", "proxy"],
  "total": 2,
  "valid_count": 2,
  "invalid_count": 0,
  "rows": [
    {
      "row_index": 0,
      "email": "[email protected]",
      "platform": "onlyfans",
      "lane": "password",
      "proxy": "host:port",
      "has_password": true,
      "has_totp_secret": false,
      "cookie_fields": [],
      "valid": true,
      "errors": []
    }
  ]
}

Preview never echoes a secret

Passwords and TOTP secrets come back as booleans (has_password, has_totp_secret), cookies as a list of field names, and the proxy as host:port only — the credentials are stripped. The paste already crossed the network once; reflecting it back would double the exposure for no benefit.

Always preview first. It is free, it costs no platform requests, and it is the only chance to catch a malformed paste before logins start.

Create the job

Same body, plus an optional source:

curl -X POST "$BASE/api/crm/$CRM/import/jobs" \
  -H "X-API-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "…", "default_platform": "onlyfans", "source": "paste"}'

Returns 202 with the job, and dispatches a background worker. Up to 1,000 rows per job by default.

Watch it

Progress arrives on the event stream as import.progress (coalesced to at most one per second) and finishes with import.complete:

curl -N "$BASE/api/crm/$CRM/events/stream" -H "X-API-Key: $KEY"

Or poll:

curl "$BASE/api/crm/$CRM/import/jobs/$JOB_ID" -H "X-API-Key: $KEY"   # one job
curl "$BASE/api/crm/$CRM/import/jobs" -H "X-API-Key: $KEY"           # history

Answer the 2FA prompts

Rows whose platform demands a code park rather than fail, with status needs_2fa. Supply the code and that row finishes:

curl -X POST "$BASE/api/crm/$CRM/import/jobs/$JOB_ID/rows/$ROW_ID/otp" \
  -H "X-API-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{"code": "123456"}'

This call is synchronous — one platform call, and the response carries the real outcome rather than "queued", because someone is waiting on it.

Parked rows outlive the job

curl "$BASE/api/crm/$CRM/import/pending-2fa?limit=200" -H "X-API-Key: $KEY"

Panel-wide, across every job. This is what lets a dashboard say "3 accounts need a 2FA code" long after whoever started the import has closed the tab — a per-job query could not answer that.

Retry and cancel

# Retry one failed row
curl -X POST "$BASE/api/crm/$CRM/import/jobs/$JOB_ID/rows/$ROW_ID/retry" \
  -H "X-API-Key: $KEY"

# Stop the whole job
curl -X POST "$BASE/api/crm/$CRM/import/jobs/$JOB_ID/cancel" -H "X-API-Key: $KEY"

Lanes

Every valid row is routed down one of two lanes, decided by what you supplied:

LaneTriggered byBehaviour
cookieSession cookies or a Fansly auth tokenSession paste — no login attempt, most reliable
passwordAn email/username and passwordFull login flow, may park on 2FA

Prefer cookie rows where you have them, for the same reasons given in Connect an account.

Row statuses

StatusMeaning
pendingParsed and accepted, not yet claimed
runningLogin in flight
needs_2faParked — waiting for you to post a code
needs_2fa_expiredThe 2FA window closed and the credential was destroyed; retry restarts the login
successAccount connected
failedGave up; see the row's error
invalidFailed parse validation — never attempted
skippedThat email is already connected to this panel
slot_exhaustedPlan account limit reached
canceledJob canceled before this row ran

There is no queued and no complete — success is success. The authoritative list comes back as row_states on GET /import/jobs/{job_id}.

Endpoints

EndpointWhat it does
POST /import/previewParse + validate, no side effects
POST /import/jobsCreate a job → 202
GET /import/jobsJob history
GET /import/jobs/{job_id}One job, with rows
POST /import/jobs/{job_id}/cancelStop a running job
POST /import/jobs/{job_id}/rows/{row_id}/otpSupply a 2FA code (synchronous)
POST /import/jobs/{job_id}/rows/{row_id}/retryRetry one row
GET /import/pending-2faPanel-wide parked rows

All writes are on the 100/minute sensitive-route limit.

One slot per imported account — and the cap can be overshot

Each connected account consumes a slot. A row that hits the cap ends as slot_exhausted (not failed) with Account limit reached (N). Buy a slot to add another account. and code: "SLOT_LIMIT".

The check is a read, not a reservation, so up to the lane concurrency (10 cookie / 6 password) rows can all observe the same last free slot and proceed — a panel can end up slightly over its cap. Check GET /api/crm/{crm_id}/usage first.

On this page