OnlyFans API error codes
OnlyFans API error codes from BetterFans Link: what each code and HTTP status means, which errors to retry and how to fix the rest.
Product page: OnlyFans and Fansly API coverage
Errors come back with an HTTP status and a JSON body. Branch on error.code: it is stable. error.message is for people and can be more specific than the default shown here, so never match on it.
The error body
| Field | Type | Description |
|---|---|---|
error | object | The error. |
error.type | enum | Error category. It decides the HTTP status, except for method_not_allowed, which is 405. One of invalid_request, authentication, billing, permission, not_found, conflict, account_unavailable, rate_limited, upstream or internal. |
error.code | enum | Stable code to branch on. One of the 58 codes under all codes. |
error.message | string | A sentence for people. It can be more specific than the default, so never match on it. |
error.hint | string | Optional. What to do next. |
error.param | string | Optional. The parameter that failed validation. |
error.permission | string | Optional. The permission that was missing. |
error.accountStatus | string | Optional. The account's status, sent with account_unavailable. |
error.limit | object | Optional. Which plan limit refused the request, sent with quota_exceeded, plan_limit_reached and rate_limited when the plan's rate refused it. See plans and limits. |
error.limit.name | enum | The limit: accounts, liveCalls, requestsPerMinute, seats or webhookEndpoints. |
error.limit.allowed | integer | How much the plan allows. |
error.limit.used | integer | How much the workspace has used. |
error.limit.resetsAt | string | Optional. When the limit resets, for live calls and the rate. |
error.docsUrl | string | Link to this code on the errors page. |
requestId | string | The request id. Send it to support when you ask about a request. |
{
"error": {
"type": "account_unavailable",
"code": "account_unavailable",
"message": "The platform is not accepting calls for this account right now.",
"hint": "See error.accountStatus. Reads without fresh=true still work.",
"accountStatus": "needs_relink",
"docsUrl": "https://betterfans.link/docs/errors#account-unavailable"
},
"requestId": "req_7Hq2LmX9pRt4VbN8cKe3WzYa"
}Retrying
| Error | What to do |
|---|---|
429 rate_limited | Wait the number of seconds in Retry-After, then retry. |
502 onlyfans_error and onlyfans_timeout | Retry after a short wait, or read synced data without fresh=true. |
502 fansly_error and fansly_timeout | Retry after a short wait, or read synced data without fresh=true. |
500 or 503 internal_error | Retry with a growing delay. |
409 account_unavailable | Do not retry until the account status changes. |
402 quota_exceeded | Live calls resume at error.limit.resetsAt. Read synced data without fresh=true until then. |
403 workspace_suspended | Do not retry until support lifts the suspension. |
Any other 4xx | Fix the request first. Sending it again unchanged fails the same way. |
| Any error on an action | Retry with the same Idempotency-Key, so the action is created once. |
All codes
Codes marked dashboard come from the dashboard and the hosted link page, never from the REST API or MCP.
| Code | Status | Type |
|---|---|---|
invalid_parameter | 400 | invalid_request |
invalid_cursor | 400 | invalid_request |
idempotency_key_required | 400 | invalid_request |
method_not_allowed | 405 | invalid_request |
not_supported_on_platform | 400 | invalid_request |
missing_api_key | 401 | authentication |
invalid_api_key | 401 | authentication |
revoked_api_key | 401 | authentication |
expired_api_key | 401 | authentication |
not_signed_in (dashboard) | 401 | authentication |
invalid_access_token (dashboard) | 401 | authentication |
missing_scope | 403 | permission |
writes_disabled | 403 | permission |
missing_permission | 403 | permission |
dashboard_key_only | 403 | permission |
session_only (dashboard) | 403 | permission |
reauthentication_required (dashboard) | 403 | permission |
email_not_verified (dashboard) | 403 | permission |
workspace_suspended | 403 | permission |
staff_only (dashboard) | 403 | permission |
staff_protected (dashboard) | 403 | permission |
impersonation_read_only (dashboard) | 403 | permission |
quota_exceeded | 402 | billing |
plan_limit_reached | 402 | billing |
account_not_found | 404 | not_found |
fan_not_found | 404 | not_found |
chat_not_found | 404 | not_found |
action_not_found | 404 | not_found |
link_not_found | 404 | not_found |
route_not_found | 404 | not_found |
workspace_not_found (dashboard) | 404 | not_found |
resource_not_found | 404 | not_found |
user_not_found (dashboard) | 404 | not_found |
idempotency_conflict | 409 | conflict |
action_not_pending | 409 | conflict |
action_expired | 409 | conflict |
link_not_ready (dashboard) | 409 | conflict |
last_owner (dashboard) | 409 | conflict |
owns_workspaces (dashboard) | 409 | conflict |
two_factor_enabled (dashboard) | 409 | conflict |
two_factor_not_enabled (dashboard) | 409 | conflict |
subscription_exists (dashboard) | 409 | conflict |
no_subscription (dashboard) | 409 | conflict |
subscription_active (dashboard) | 409 | conflict |
checkout_unavailable (dashboard) | 409 | conflict |
plan_not_available (dashboard) | 409 | conflict |
last_admin (dashboard) | 409 | conflict |
user_banned (dashboard) | 409 | conflict |
email_unverified (dashboard) | 409 | conflict |
account_in_use (dashboard) | 409 | conflict |
account_unavailable | 409 | account_unavailable |
rate_limited | 429 | rate_limited |
onlyfans_error | 502 | upstream |
onlyfans_timeout | 502 | upstream |
fansly_error | 502 | upstream |
fansly_timeout | 502 | upstream |
billing_provider_error (dashboard) | 502 | upstream |
internal_error | 500 | internal |
Invalid request
invalid_parameter
| Status | Type | Message | Hint |
|---|---|---|---|
| 400 | invalid_request | A parameter is missing or not valid. | Check error.param and the endpoint reference. |
A query parameter has a value the route does not accept, a required one is missing, or the body does not match. error.param names the parameter. Fix the value and send the request again. Unknown query parameters are ignored, so a misspelled name has no effect instead of failing.
invalid_cursor
| Status | Type | Message | Hint |
|---|---|---|---|
| 400 | invalid_request | The cursor is not valid for this list. | Pass nextCursor exactly as the previous page returned it. |
A cursor belongs to one list with one set of filters. Pass nextCursor back unchanged with the same parameters, or leave cursor out to start again from the first page.
idempotency_key_required
| Status | Type | Message | Hint |
|---|---|---|---|
| 400 | invalid_request | Writes need an Idempotency-Key header. | Send a unique value per intended action, for example a UUID. |
Every POST /v1/accounts/{accountId}/actions needs an Idempotency-Key header. Generate a UUID for each action you intend and send the same value when you retry that action.
method_not_allowed
| Status | Type | Message | Hint |
|---|---|---|---|
| 405 | invalid_request | This endpoint only accepts GET. | Use POST /v1/accounts/{accountId}/actions to change anything on the account. |
Call OnlyFans and Call Fansly only read. To send, take back or change anything, create an action instead. It waits for a person to approve it, unless the key has auto-approve on.
not_supported_on_platform
| Status | Type | Message | Hint |
|---|---|---|---|
| 400 | invalid_request | This account's platform does not support this. | Check the platforms each endpoint lists in the API reference. GET /v1/accounts shows each account's platform. |
The account's platform does not support this route, action type or tool. Each route in the API reference lists its platforms, and GET /v1/accounts shows each account's platform. For an action, error.param is type. Sending the same request again fails the same way.
Authentication
missing_api_key
| Status | Type | Message | Hint |
|---|---|---|---|
| 401 | authentication | No API key was sent. | Send Authorization: Bearer <your key>. |
Send the key as Authorization: Bearer <key> or in the x-api-key header.
invalid_api_key
| Status | Type | Message | Hint |
|---|---|---|---|
| 401 | authentication | This API key is not valid. | Check for typos or create a new key in the dashboard. |
No key matches. Check that the whole key was copied and that nothing was added around it. Keys are shown once, so a lost key has to be replaced with a new one.
revoked_api_key
| Status | Type | Message | Hint |
|---|---|---|---|
| 401 | authentication | This API key was revoked. | Create a new key in the dashboard. |
Someone revoked this key in the dashboard, or it was rolled and the old key stopped working. Create a new key.
expired_api_key
| Status | Type | Message | Hint |
|---|---|---|---|
| 401 | authentication | This API key has expired. | Create a new key in the dashboard. |
The key passed the expiry date set when it was created. Create a new key.
not_signed_in
| Status | Type | Message | Hint |
|---|---|---|---|
| 401 | authentication | You are not signed in. | Sign in again. |
Dashboard only. The dashboard session ended. Sign in again.
invalid_access_token
| Status | Type | Message | Hint |
|---|---|---|---|
| 401 | authentication | This access token is not valid, was revoked or has expired. | Create a new token under Account, Access tokens. |
Dashboard only. The account access token or OAuth token is not one we issued, someone revoked it, it passed its expiry date, or the person it belongs to is banned. Create a new token on the Account page and replace the old one wherever it is stored. A banned person gets this answer until the ban is lifted. API keys work on /v1, not on the dashboard procedures.
Billing
quota_exceeded
| Status | Type | Message | Hint |
|---|---|---|---|
| 402 | billing | This workspace has used its live calls for this period. | Synced reads and test mode keep working. Live calls resume when the period resets (error.limit.resetsAt), or upgrade or allow overage in Billing. |
The workspace used every live call its plan includes for this period. error.limit says how many were allowed, how many were used and when the period resets in resetsAt. Synced reads and test mode keep working. Live calls resume at the reset, or at once when an owner upgrades the plan or allows overage on the Billing page. See plans and limits.
plan_limit_reached
| Status | Type | Message | Hint |
|---|---|---|---|
| 402 | billing | The workspace's plan does not allow more of this. | Upgrade in Billing, or remove one first. error.limit names the limit. |
The plan does not allow one more of something: an account, a teammate or a webhook endpoint. error.limit.name names the limit, with allowed and used. Nothing you already have is removed. Remove one first, or ask an owner to choose a larger plan on the Billing page. See plans and limits.
Permission
missing_scope
| Status | Type | Message | Hint |
|---|---|---|---|
| 403 | permission | This key does not have the scope this call needs. | Create a key with the write scope. |
The key has only the read scope and the call needs write. Create a key with the write scope for code that asks for writes.
writes_disabled
| Status | Type | Message | Hint |
|---|---|---|---|
| 403 | permission | Writes are turned off for this account. | An admin can turn writes on in the account's settings. |
Writes are switched off for this account. An owner or admin can switch them on in the account's settings in the dashboard. Reads keep working.
missing_permission
| Status | Type | Message | Hint |
|---|---|---|---|
| 403 | permission | Your role does not allow this. | Ask a workspace admin. |
In the dashboard: your workspace role does not allow this. error.permission names the permission. Ask an owner or admin. Over MCP, search_api and describe_endpoint return it to a test key or to a workspace with no linked account: connect with a live key and link an account first.
dashboard_key_only
| Status | Type | Message | Hint |
|---|---|---|---|
| 403 | permission | Only the dashboard can run this. | Approve the action in the dashboard. |
Only the dashboard runs a pending action, after a person approves it. API and OAuth keys create actions and read their status. An action from a key with auto-approve on runs when the key creates it.
session_only
| Status | Type | Message | Hint |
|---|---|---|---|
| 403 | permission | Access tokens cannot do this. | Sign in at betterfans.link and do it in the dashboard. |
Dashboard only. An account access token can do what you can do in a workspace, but not account security, invitations, OAuth consent or managing tokens. Sign in at betterfans.link and do it in the dashboard.
reauthentication_required
| Status | Type | Message | Hint |
|---|---|---|---|
| 403 | permission | Confirm it is you before doing this. | Sign in again with your password or an email code, then retry within 10 minutes. |
Dashboard only. Some changes need a sign in from the last 10 minutes: deleting a workspace or your account, changing your email, setting a first password and turning two-step verification on or off. Sign in again with your password or an email code, then retry.
email_not_verified
| Status | Type | Message | Hint |
|---|---|---|---|
| 403 | permission | Confirm your email address first. | Enter the code we email you, then try again. |
Dashboard only. A new dashboard account confirms its email before it opens a workspace. Enter the code emailed at sign up, or send a new one.
workspace_suspended
| Status | Type | Message | Hint |
|---|---|---|---|
| 403 | permission | This workspace is suspended, so it cannot make live calls. | Synced data and test mode still work. Contact support to lift it. |
BetterFans Link suspended live calls for this workspace. Synced reads, test mode and the dashboard keep working, so nothing you stored is lost. Write to hello@betterfans.link to find out why and to have it lifted. Retrying does not help until it is lifted.
staff_only
| Status | Type | Message | Hint |
|---|---|---|---|
| 403 | permission | Only Link staff can do this. | None |
Dashboard only. Only BetterFans Link staff can open the admin console. A workspace role, owner included, does not reach it.
staff_protected
| Status | Type | Message | Hint |
|---|---|---|---|
| 403 | permission | Staff accounts and your own cannot be banned or viewed as. | Remove their staff role first. |
Dashboard only. Staff accounts, and your own, cannot be banned or viewed as. Remove their staff role first.
impersonation_read_only
| Status | Type | Message | Hint |
|---|---|---|---|
| 403 | permission | You are viewing as this person, so nothing can be changed. | Stop viewing to make changes as yourself. |
Dashboard only. BetterFans Link staff are viewing the dashboard as this person to help with a support request. That view is read only, so every change is refused until they stop viewing.
Not found
account_not_found
| Status | Type | Message | Hint |
|---|---|---|---|
| 404 | not_found | No account with this id is linked to your workspace. | List your accounts with GET /v1/accounts. |
The account id is not linked to your workspace, your key is limited to other accounts, or a test key asked for an account that is not a sandbox creator. BetterFans Link answers 404, never 403, so it never reveals whether an account exists. List the accounts your key can use with GET /v1/accounts.
fan_not_found
| Status | Type | Message | Hint |
|---|---|---|---|
| 404 | not_found | No fan with this id on this account. | Search with GET /v1/accounts/{accountId}/fans?search=. |
Fan ids are user ids on the account's platform. Find the fan with GET /v1/accounts/{accountId}/fans?search= and use the id it returns.
chat_not_found
| Status | Type | Message | Hint |
|---|---|---|---|
| 404 | not_found | No chat with this fan. | A chat id is the fan's id. |
A chat id is the fan's id. Use a fan id from List fans or List chats.
action_not_found
| Status | Type | Message | Hint |
|---|---|---|---|
| 404 | not_found | No action with this id. | List actions with GET /v1/actions. |
Check the id, or list actions with GET /v1/actions.
link_not_found
| Status | Type | Message | Hint |
|---|---|---|---|
| 404 | not_found | No link with this id. | Create one with POST /v1/links. |
Check the id, or create a new link with POST /v1/links.
route_not_found
| Status | Type | Message | Hint |
|---|---|---|---|
| 404 | not_found | This endpoint does not exist. | See the API reference. |
The method and path match no route. Paths start with /v1. Compare yours with the API reference.
workspace_not_found
| Status | Type | Message | Hint |
|---|---|---|---|
| 404 | not_found | Workspace not found. | Pick a workspace you belong to. |
Dashboard only. The workspace does not exist or you are not a member. Pick a workspace you belong to.
resource_not_found
| Status | Type | Message | Hint |
|---|---|---|---|
| 404 | not_found | Not found. | None |
Something the request refers to does not exist. Check every id in the path.
user_not_found
| Status | Type | Message | Hint |
|---|---|---|---|
| 404 | not_found | No person with this id or email has a Link account. | None |
Dashboard only. No person with this id or email has a BetterFans Link account. Check the address, or ask them to sign up first.
Conflict
idempotency_conflict
| Status | Type | Message | Hint |
|---|---|---|---|
| 409 | conflict | This Idempotency-Key was already used with a different request. | Use a new key for a different action. |
You reused an Idempotency-Key with a different body. One key stands for one intended action: use a new key for a new action. The same key with the same body returns the original action, so retries are safe.
action_not_pending
| Status | Type | Message | Hint |
|---|---|---|---|
| 409 | conflict | This action was already decided. | Check its status with GET /v1/actions/{actionId}. |
The action was already approved, rejected, run or it failed. Read its status with GET /v1/actions/{actionId}.
action_expired
| Status | Type | Message | Hint |
|---|---|---|---|
| 409 | conflict | This action expired before anyone approved it. | Create it again. |
Pending actions expire after 24 hours without a decision. Create the action again with a new Idempotency-Key.
link_not_ready
| Status | Type | Message | Hint |
|---|---|---|---|
| 409 | conflict | This link is not waiting for that step. | None |
Dashboard only. The link is not at the step this request is for, for example a 2FA code sent before the platform asked for one, or a sign-in before the creator picked a platform. Reload the link's state and follow the step it shows.
last_owner
| Status | Type | Message | Hint |
|---|---|---|---|
| 409 | conflict | A workspace needs at least one owner. | Make someone else an owner first. |
Dashboard only. A workspace always keeps at least one owner. Make another member an owner before you remove or demote this one.
owns_workspaces
| Status | Type | Message | Hint |
|---|---|---|---|
| 409 | conflict | You are the only owner of a workspace. | Make someone else an owner of each workspace you own alone, or delete those workspaces, then try again. |
Dashboard only. Deleting your account would leave a workspace with no owner. The message names each workspace you own alone. Make another member an owner of each one, or delete it, then delete your account.
two_factor_enabled
| Status | Type | Message | Hint |
|---|---|---|---|
| 409 | conflict | Two-step verification is already on. | Turn it off first to set up a new authenticator app. |
Dashboard only. Two-step verification is already on for your account. To move it to a new authenticator app, turn it off on the Account page, then set it up again.
two_factor_not_enabled
| Status | Type | Message | Hint |
|---|---|---|---|
| 409 | conflict | Two-step verification is off. | Turn it on in your account first. |
Dashboard only. Two-step verification is off, so there is nothing to turn off and no backup codes to replace. Turn it on from the Account page first.
subscription_exists
| Status | Type | Message | Hint |
|---|---|---|---|
| 409 | conflict | This workspace already has a subscription. | Change its plan instead of starting a new one. |
Dashboard only. The workspace already pays for a plan, so it cannot start a second subscription. Change the plan on the Billing page instead: the difference is prorated.
no_subscription
| Status | Type | Message | Hint |
|---|---|---|---|
| 409 | conflict | This workspace has no subscription to change. | Choose a plan to start one. |
Dashboard only. The workspace is on the Free plan or its subscription ended, so there is nothing to change, cancel or resume. Choose a plan on the Billing page to start one.
subscription_active
| Status | Type | Message | Hint |
|---|---|---|---|
| 409 | conflict | This workspace still has a subscription. | Cancel it in Billing and wait for it to end, then delete the workspace. |
Dashboard only. A workspace with a subscription that has not ended cannot be deleted, so nobody keeps paying for a workspace that is gone. An owner cancels it on the Billing page. The plan holds until the end of the period; delete the workspace after that.
checkout_unavailable
| Status | Type | Message | Hint |
|---|---|---|---|
| 409 | conflict | Plans cannot be bought in the dashboard right now. | Contact us and we will set it up. |
Dashboard only. Plans cannot be bought in the dashboard right now. Nothing was charged. Write to hello@betterfans.link and we will set the plan up for you.
plan_not_available
| Status | Type | Message | Hint |
|---|---|---|---|
| 409 | conflict | This plan cannot be bought in the dashboard. | Contact us for Enterprise. |
Dashboard only. The plan is not sold in the dashboard, for example Enterprise, which is quoted. Write to hello@betterfans.link for it, or pick a plan the Billing page offers.
last_admin
| Status | Type | Message | Hint |
|---|---|---|---|
| 409 | conflict | Link needs at least one admin. | Make someone else an admin first. |
Dashboard only. BetterFans Link always keeps one staff admin. Make someone else an admin before removing the last one.
user_banned
| Status | Type | Message | Hint |
|---|---|---|---|
| 409 | conflict | This person is banned. | Lift the ban first. |
Dashboard only. This person is banned, and this action needs them unbanned. Lift the ban first.
email_unverified
| Status | Type | Message | Hint |
|---|---|---|---|
| 409 | conflict | This person has not confirmed their email. | Staff need a confirmed email. Ask them to confirm it first. |
Dashboard only. Staff need a confirmed email. Ask the person to confirm their email, then try again.
account_in_use
| Status | Type | Message | Hint |
|---|---|---|---|
| 409 | conflict | This account is already connected to another workspace. | Remove it from the other workspace first, then link it again. |
Dashboard only. A Fansly account can be connected in one place at a time, and this one is already connected somewhere else. Remove it from the other workspace first, then link it again.
Account unavailable
account_unavailable
| Status | Type | Message | Hint |
|---|---|---|---|
| 409 | account_unavailable | The platform is not accepting calls for this account right now. | See error.accountStatus. Reads without fresh=true still work. |
The platform is not accepting calls for this account. error.accountStatus is one of needs_relink, awaiting_2fa, awaiting_selfie, restricted or disconnected. Live reads, Call OnlyFans and Call Fansly stop until the account is healthy again, and approved actions cannot run. Reads without fresh=true keep working. Do not retry until the status changes: see account status for what fixes each one.
Rate limited
rate_limited
| Status | Type | Message | Hint |
|---|---|---|---|
| 429 | rate_limited | Too many requests. | Wait for Retry-After seconds. |
The key sent more requests than its rate limit allows. Wait the number of seconds in the Retry-After header, then retry. See rate limits.
Upstream
onlyfans_error
| Status | Type | Message | Hint |
|---|---|---|---|
| 502 | upstream | OnlyFans returned an error. | Retry later. If it keeps failing, check the account's status. |
OnlyFans answered a live read with an error. Retry after a short wait. If it keeps failing, read the account with GET /v1/accounts/{accountId}: its status may have changed.
onlyfans_timeout
| Status | Type | Message | Hint |
|---|---|---|---|
| 502 | upstream | OnlyFans did not answer in time. | Retry later, or read without fresh=true to get synced data. |
OnlyFans did not answer in time. Retry later, or leave out fresh=true to read synced data.
fansly_error
| Status | Type | Message | Hint |
|---|---|---|---|
| 502 | upstream | Fansly returned an error. | Retry later. If it keeps failing, check the account's status. |
Fansly answered a live read with an error. Retry after a short wait. If it keeps failing, read the account with GET /v1/accounts/{accountId}: its status may have changed.
fansly_timeout
| Status | Type | Message | Hint |
|---|---|---|---|
| 502 | upstream | Fansly did not answer in time. | Retry later, or read without fresh=true to get synced data. |
Fansly did not answer in time. Retry later, or leave out fresh=true to read synced data.
billing_provider_error
| Status | Type | Message | Hint |
|---|---|---|---|
| 502 | upstream | The payment provider did not answer. | Nothing was charged. Retry in a minute. |
Dashboard only. The payment provider did not answer in time. Nothing was charged and the plan did not change. Retry in a minute.
Internal
internal_error
| Status | Type | Message | Hint |
|---|---|---|---|
| 500 | internal | Something went wrong on our side. | Retry. If it keeps happening, send us the requestId. |
Something failed inside BetterFans Link. In test mode it comes back as 503 while the sandbox is briefly unavailable. Retry with a growing delay. If it keeps happening, send the requestId to hello@betterfans.link.