ofapisv1
capability

Updated

OnlyFans Creator Profile & User API

Two different questions, three routes. /me answers whose session am I holding and is it alive. The user lookups answer who is this id — the question every webhook, chat row and list membership leaves you with, because chat and message payloads carry bare numeric ids and nothing else.

Endpoints

GET /api/public/v1/me
GET /api/public/v1/users/{targetUserId}
GET /api/public/v1/users/by-username/{username}
GET /api/public/v1/accounts/{accountId}/me
GET /api/public/v1/accounts/{accountId}/users/{targetUserId}
GET /api/public/v1/accounts/{accountId}/users/by-username/{username}

None of these take query parameters. username is the handle without the @. targetUserId is the numeric OnlyFans user id.

One id to keep straight: accountId is the ofapis account id (the integer that identifies a linked session on your token, 42 in every example here). It is not the creator's OnlyFans id returned by /me. Mixing them up produces a 404 ACCOUNT_NOT_FOUND, not a 403.

GET /me

curl https://api.ofapis.com/api/public/v1/accounts/42/me \
  -H "Authorization: Bearer ofapis_sk_..."
{
  "data": {
    "id": 12345678,
    "username": "creator_handle",
    "name": "Creator Name",
    "email": "[email protected]",
    "subscribersCount": 1024,
    "isAuth": true
  }
}

The full object carries more than the example: subscribesCount, postsCount, photosCount, videosCount, avatar and header URLs, the capability flags canChat, canCreateLists, canUploadMedia, canUploadAvatarHeader, canPayInternal, plus isVisibleOnline, watermarkText and watermarkPosition.

The capability flags are worth reading before you act rather than after. canCreateLists: false means a list write will fail upstream; canUploadMedia: false means a scheduled post with mediaFiles has nowhere to get them. Checking the flag costs the same credit as discovering it the hard way, and it turns a failed write into a skipped one.

GET /users/{targetUserId} and /users/by-username/{username}

curl https://api.ofapis.com/api/public/v1/accounts/42/users/by-username/fan_handle \
  -H "Authorization: Bearer ofapis_sk_..."
{
  "data": {
    "id": 98765,
    "username": "fan_handle",
    "name": "Fan Name",
    "isVerified": false,
    "subscribedBy": true,
    "postsCount": 42,
    "photosCount": 30,
    "videosCount": 12
  }
}

This is the richest read in the API, and the only place several fields appear at all:

Field Type What it tells you
subscribedBy boolean this user subscribes to the authenticated creator
subscribedByExpire boolean the subscription is set to lapse
subscribedByExpireDate string ISO-8601 expiry — the churn clock
currentSubscribePrice integer cents, after any promotion
subscribePrice integer cents, list price
subscribedOn boolean the creator subscribes to them
joinDate string ISO-8601, account age
lastSeen string ISO-8601, recency signal
canReceiveChatMessage boolean a DM will actually land
tipsEnabled, tipsMin, tipsMax boolean, cents tipping envelope
about, website, location, notice string profile text
isPaywallRequired, isAdultContent, isPerformer boolean account type
isBlocked, isRestricted, isFriend boolean relationship state

subscribedByExpireDate and currentSubscribePrice are the two that make this endpoint worth a credit: neither appears on the paged rows from the subscribers API, so a renewal reminder or a win-back cohort has to come through here. Which of the reads is cheapest for a given field is worked out row by row on the fan lookup page.

Session health as a free signal

/me is the cheapest honest probe of a linked session. A 200 with isAuth: true means the stored session still works; a 422 OF_AUTH_INVALID means it expired and the account needs re-linking, and a 409 ACCOUNT_NOT_ACTIVE means it is linked but disconnected. Both of those statuses are refunded, so a failing probe costs nothing — only the healthy ones are billed. The curl audit from the shell uses the same request as its first health check.

#!/usr/bin/env bash
# Probe a roster, print the accounts that need re-linking.
for id in 42 43 44; do
  body=$(curl -sS -o - -w '\n%{http_code}' \
    "https://api.ofapis.com/api/public/v1/accounts/$id/me" \
    -H "Authorization: Bearer $OFAPIS_TOKEN")
  code=$(tail -n1 <<<"$body")
  case "$code" in
    200) jq -r '"ok      \(.data.username) subs=\(.data.subscribersCount)"' <<<"$(sed '$d' <<<"$body")" ;;
    422) echo "relink  account $id — OnlyFans session expired" ;;
    409) echo "offline account $id — account not active" ;;
    *)   echo "error   account $id — HTTP $code" ;;
  esac
done

Wire that to the same alert channel as your webhook endpoint health and a dead session surfaces in minutes instead of when a campaign silently stops delivering. Re-linking itself is covered in the authentication guide.

Errors

Status error.code Applies to
401 UNAUTHENTICATED any route — token missing or revoked
402 INSUFFICIENT_CREDITS any route
403 NO_ACTIVE_SUBSCRIPTION any route
404 ACCOUNT_NOT_FOUND scoped routes — accountId not on this token
409 ACCOUNT_NOT_ACTIVE scoped routes — linked but disconnected
422 OF_AUTH_INVALID scoped routes — stored session expired
422 ACCOUNT_NOT_LINKED unscoped routes — no active account at all; charged
429 RATE_LIMIT_EXCEEDED any route — honour Retry-After

The split between 424 and 404 is diagnostic: 424 means the token has no usable account behind it, while 404 means the id you named is not one of yours. A 422 is the only one that needs a human — the creator has to re-authorise.

Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (unix seconds), and echo back any X-Request-Id you send, which is the value to quote in a support thread. Everything here is a single Bearer token with no request signing; the full scheme is on authentication.

Cost

One credit per successful lookup, charged per call with no batch form — resolving 200 ids is 200 credits. Cache aggressively: username, name and joinDate effectively never change, subscribedByExpireDate changes once a billing cycle, and lastSeen is the only field that justifies a re-fetch inside a day. Plan limits are on pricing, and the shared response headers and error codes behave as described in the API reference.

FAQ

Which linked account does the unscoped /me return?

The first account in ACTIVE status on your token. That is deterministic but implicit, so anything managing more than one creator should call /accounts/{accountId}/me and pass the id explicitly rather than relying on ordering.

Can I resolve a handle to a numeric id?

Yes — GET /users/by-username/{username} takes the handle without the @ and returns the full profile, id included. Store that id; it is stable, while a handle can be changed by its owner.

Is there a bulk user lookup?

No. Each id is one request and one credit, so a job that enriches thousands of fans needs a local cache and a pass that only fetches ids it has never seen. Fetch on first sight, refresh on a schedule you choose.

Does a failed probe cost credits?

Not for the statuses that matter here. 422, 409 and any 5xx are refunded after the fact, and 401, 402, 403 and 429 are rejected before the debit happens. A 200 is billed whether or not you liked the answer.

What is the difference between subscribedBy and subscribedOn?

subscribedBy is true when the user subscribes to the authenticated creator — the normal fan relationship. subscribedOn is the reverse: the creator subscribes to them, which is what the /following list is made of.

Start free · See all endpoints · 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