ofapisv1
capability

Updated

OnlyFans Fan Segments API

First, the vocabulary, because it decides what you type. "Segment" is the word for the idea; list is the word in the API. There is no /segments route and never has been. A segment is an OnlyFans list you populate from your own scoring, and everything below lives under /accounts/{accountId}/lists.

If what you want is the list object itself — the built-in Fans and Following collections, their flags and ordering — that is the lists API. This page is about the loop you end up automating: score, populate, address, prune.

Endpoints

GET    /api/public/v1/accounts/{accountId}/lists
POST   /api/public/v1/accounts/{accountId}/lists
DELETE /api/public/v1/accounts/{accountId}/lists/{listId}
GET    /api/public/v1/accounts/{accountId}/lists/{listId}/users
POST   /api/public/v1/accounts/{accountId}/lists/{listId}/users
DELETE /api/public/v1/accounts/{accountId}/lists/{listId}/users/{targetUserId}

Every route is account-scoped; there is no unscoped form, because a list belongs to exactly one creator's session. listId is a string, not an integer — "123", not 123. Treat it as opaque and never parse it.

Parameters and bodies

Route Param / field Default Notes
GET /lists offset 0 zero-based
GET /lists limit 10 clamped to 1–100
GET /lists/{listId}/users offset 0 zero-based
GET /lists/{listId}/users limit 10 clamped to 1–100
POST /lists name — required; blank gives 400 BAD_REQUEST
POST /lists/{listId}/users userIds [] array of integers; empty gives 400 BAD_REQUEST

Note the default limit of 10 on both reads — half what the rest of the API uses. A segment with 400 members is 40 requests unless you raise it.

Create and populate

curl -X POST https://api.ofapis.com/api/public/v1/accounts/42/lists \
  -H "Authorization: Bearer ofapis_sk_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: seg-whales-2026-02" \
  -d '{"name":"Whales Q1"}'
{ "data": { "id": "456", "name": "Whales Q1", "usersCount": 0,
            "canDelete": true, "canManageUsers": true } }

Keep the returned id. Both POSTs honour Idempotency-Key: a repeat replays the stored response with Idempotency-Replayed: true and is not billed again, which is what stops a retried nightly job from creating "Whales Q1" four times.

Adding members takes the whole batch in one call — {"userIds":[98765,11223]} — and returns {"data":{"added":[98765,11223]}}. One call, one credit, however many ids are in it, so batch generously instead of looping one id per request. Removal is the opposite shape: one fan per DELETE, returning the list with its new usersCount.

Reading a segment back

curl "https://api.ofapis.com/api/public/v1/accounts/42/lists/456/users?limit=100&offset=0" \
  -H "Authorization: Bearer ofapis_sk_..."
{ "data": { "list": [ { "id": 98765, "name": "Fan Name", "username": "fan_handle",
                        "subscribedBy": true } ], "hasMore": true, "nextOffset": 10 } }

A member row is considerably richer than a row from the subscribers API: it also carries subscribedByExpireDate, subscribedByAutoprolong, currentSubscribePrice, subscribePrice, lastSeen, canReceiveChatMessage, tipsEnabled, isBlocked, isRestricted and canTrialSend. It also carries listsStates — an array of {id, type, name, hasUser} describing that fan's membership across the account's other lists, which is how you detect a fan sitting in both "Whales" and "Churn risk" without a second read.

GET /lists returns {list, hasMore} with no nextOffset; the members route does return one. Either way, drive paging from your own counter.

Reconciling membership

A list is stored membership, not a saved query. It does not re-evaluate itself when the signals you scored a fan on change, so the job is a diff.

def reconcile(account_id, list_id, want: set[int]):
    """Make the list contain exactly `want`. Returns credits spent."""
    have, offset, spent = set(), 0, 0
    while True:
        r = S.get(f"{BASE}/accounts/{account_id}/lists/{list_id}/users",
                  params={"limit": 100, "offset": offset}, timeout=30)
        r.raise_for_status(); spent += 1
        body = r.json()["data"]
        have |= {u["id"] for u in body["list"]}
        if not body["hasMore"] or not body["list"]:
            break
        offset += 100

    add, drop = want - have, have - want
    if add:                                   # one call for the whole batch
        S.post(f"{BASE}/accounts/{account_id}/lists/{list_id}/users",
               json={"userIds": sorted(add)},
               headers={"Idempotency-Key": f"{list_id}-add-{min(add)}-{len(add)}"})
        spent += 1
    for uid in drop:                          # one call each
        S.delete(f"{BASE}/accounts/{account_id}/lists/{list_id}/users/{uid}")
        spent += 1
    return spent

Cost is ceil(members / 100) + 1 + len(drop) credits. Removals dominate, so a cohort that churns hard is cheaper rebuilt than edited: delete the list, create it, add the batch.

Addressing the cohort

The payoff is sending to the segment as one unit rather than looping over fans.

Preview before you trigger. The count reflects the segment at call time, not at the time you created the mailing — and a reconcile that ran in between will have changed it.

Errors and limits

Status error.code Cause
400 BAD_REQUEST blank name, or an empty userIds array
402 INSUFFICIENT_CREDITS balance below the call's weight
404 bare text body accountId not on this token
409 ACCOUNT_NOT_ACTIVE account linked but disconnected
409 IDEMPOTENCY_CONFLICT same key already in flight
424 OF_SESSION_EXPIRED stored OnlyFans session needs re-linking
429 RATE_LIMIT_EXCEEDED 60/100/500 requests per minute by plan

Every list call is one credit, debited before the handler runs and refunded on 5xx, 424 and 409 — so a reconcile that dies halfway through a drop loop only pays for the deletes that landed. Watch the X-RateLimit-Remaining header during a large rebuild: a 900-member cohort is nine reads plus one add plus however many deletes, which is a real fraction of a Free-plan minute. The per-plan numbers are on rate limits and pricing.

FAQ

Is there a /segments endpoint?

No. Segments are OnlyFans lists, addressed at /accounts/{accountId}/lists, and any documentation showing a /segments path is describing something that does not exist. The marketing word and the API word differ on purpose.

Why is my list only returning ten members?

Because limit defaults to 10 on both list routes, not the 20 used elsewhere in the API. Pass limit=100 explicitly for any sweep; the clamp is 1–100 either way.

Can one fan belong to several segments at once?

Yes, and each member row reports it: the listsStates array names every list on the account with a hasUser flag for that fan. Use it to suppress a fan from a win-back send when they already sit in a VIP cohort.

How do I remove several fans in one call?

You cannot — removal is one DELETE per targetUserId, while adding takes an array. When the drop set is large, deleting the whole list and recreating it from scratch is fewer calls and fewer credits than removing members one at a time.

Does adding a fan who is already in the segment cost extra?

It costs the one credit the call costs, and the response's added array tells you what the operation actually did. Reconciling against a read first is usually cheaper than blind re-adds, because one read page covers a hundred members.

Start free · Pricing · Build a CRM

Related guides

All guides
Start free — 250 credits, no card
Generate a token and make your first call in minutes.
Get started