Errors
Every error code, what it means and what to do about it.
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, permission, not_found, conflict, account_unavailable, rate_limited, upstream or internal. |
error.code | enum | Stable code to branch on. One of the 31 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.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": "OnlyFans is not accepting calls for this account right now.",
"hint": "See error.accountStatus. Reads without fresh=true still work.",
"accountStatus": "needs_relink",
"docsUrl": "https://app.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. |
500 or 503 internal_error | Retry with a growing delay. |
409 account_unavailable | Do not retry until the account status changes. |
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 |
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 |
missing_scope | 403 | permission |
writes_disabled | 403 | permission |
missing_permission | 403 | permission |
dashboard_key_only | 403 | permission |
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 |
idempotency_conflict | 409 | conflict |
action_not_pending | 409 | conflict |
action_expired | 409 | conflict |
link_not_ready (dashboard) | 409 | conflict |
last_owner (dashboard) | 409 | conflict |
account_unavailable | 409 | account_unavailable |
rate_limited | 429 | rate_limited |
onlyfans_error | 502 | upstream |
onlyfans_timeout | 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 OnlyFans. |
Call OnlyFans only reads. To send, take back or change anything, create an action instead; a person approves it before it runs.
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.
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 an action, after a person approves it. API and OAuth keys create actions and read their status.
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 OnlyFans user ids. 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.
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 OnlyFans asked for one. 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.
Account unavailable
account_unavailable
| Status | Type | Message | Hint |
|---|---|---|---|
| 409 | account_unavailable | OnlyFans is not accepting calls for this account right now. | See error.accountStatus. Reads without fresh=true still work. |
OnlyFans is not accepting calls for this account. error.accountStatus is one of needs_relink, awaiting_2fa, awaiting_selfie, restricted or disconnected. Live reads and Call OnlyFans 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.
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.