> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ofauth.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors

> Error envelope, codes, and recovery actions for V3

V3 has two error shapes. The token and revoke endpoints follow RFC 6749. Everything under `/v3/access`, `/v3/vault-plus`, and `/v3/linked-accounts` uses the runtime envelope.

## Runtime envelope

```json theme={null}
{
  "error": "linked_account_reconnect_required",
  "recovery": { "action": "reconnect_linked_account" },
  "requestId": "4f5c3f0e-8d7c-4ff5-9b7f-7d0d9e7c2a61"
}
```

| Field | Description |
| - | - |
| `error` | Stable machine-readable code. |
| `recovery.action` | What your application should do next. Branch on this. |
| `requestId` | Also sent as the `x-request-id` header. Quote it in support requests. |

## Runtime error codes

| HTTP | `error` | `recovery.action` | Cause |
| - | - | - | - |
| 401 | `external_oauth_bearer_required` | `obtain_external_oauth_token` | No bearer token, or an API key header was sent. |
| 401 | `invalid_token` | `obtain_external_oauth_token` | The access token is expired, malformed, or not valid here. Refresh and retry. |
| 401 | `grant_revoked_or_expired` | `restart_authorization` | Your access behind this token no longer exists. Run the authorization flow again. |
| 401 | `authorization_required` | `restart_authorization` | The authorization is not usable. Run the authorization flow again. |
| 401 | `linked_account_reconnect_required` | `reconnect_linked_account` | The account's OnlyFans session ended. The owner must reconnect. |
| 403 | `linked_account_required` | `select_linked_account` | Missing or malformed `x-linked-account-id`. |
| 403 | `linked_account_not_granted` | `grant_linked_account` | The account is not part of this authorization. |
| 404 | `linked_account_not_found` | `list_linked_accounts` | The account in the path is not part of this authorization (`/v3/linked-accounts/{id}` routes). |
| 403 | `linked_account_vault_plus_disabled` | `enable_linked_account_vault_plus` | Vault+ is off for this account. See [linked account settings](/v3/linked-accounts#linked-account-settings). |
| 403 | `vault_plus_disabled` | `enable_vault_plus` | Vault+ is off for this environment. |
| 403 | `missing_scope` | `request_additional_consent` | The operation needs a scope that is not in the token. Authorize again with the scope. |
| 403 | `additional_consent_required` | `request_additional_consent` | The scope is in the token but was not granted for this specific account. Authorize again. |
| 403 | `integration_not_approved` | `complete_integration_approval` | Live is not approved for this integration. |
| 403 | `subscription_required` | `update_developer_plan` | The organization has no active Developer plan. |
| 403 | `integration_suspended` | `contact_integration_owner` | The client or capability is unavailable. |
| 403 | `security_suspended` | `contact_integration_owner` | OFAuth suspended the integration for security reasons. |
| 403 | `client_deleted` | `contact_integration_owner` | The environment client was deleted. |
| 409 | `linked_account_settings_conflict` | `retry` | A concurrent settings change. Retry. |
| 409 | `linked_account_settings_fixed` | | Sandbox settings cannot change. |
| 429 | `quota_exhausted` | `retry_after_reset` | Test daily quota used up. `Retry-After` gives seconds until the UTC reset. |
| 429 | `rate_limited` | `retry` | Too many requests. Read `RateLimit-Limit`, `RateLimit-Remaining`, and `RateLimit-Reset`. |
| 503 | `authorization_temporarily_unavailable` | `retry` | OFAuth could not validate the request right now. Retry with backoff. |

Validation errors on request bodies and query parameters return `400` with the same envelope and a descriptive `error`, such as `invalid_request`.

When OnlyFans rejects an admitted request, the error is one of `provider_request_rejected` (`do_not_repeat` or `check_request`), `provider_resource_not_found` (`check_resource`), `linked_account_reconnect_required` (`reconnect_linked_account`), `rate_limited` (`retry`), or `transient_provider_failure` (`retry`).

## Recovery actions

| `recovery.action` | Implementation |
| - | - |
| `obtain_external_oauth_token` | Refresh the access token. If the refresh fails with `invalid_grant`, start authorization. |
| `restart_authorization` | Delete the stored refresh token and send the user through [Authorize](/v3/authorize). |
| `reconnect_linked_account` | Ask the owner to reconnect the account. Its id stays the same. You receive `linked_account.reconnected` when it works again. |
| `select_linked_account` | Send a valid `x-linked-account-id`. |
| `grant_linked_account` | Run authorization so the owner can share this account. |
| `list_linked_accounts` | Refresh your account list from `GET /v3/linked-accounts`. |
| `request_additional_consent` | Run authorization with the required scopes. |
| `enable_linked_account_vault_plus` | Turn Vault+ on for the account with `PATCH /v3/linked-accounts/{id}/settings`. |
| `enable_vault_plus` | Turn Vault+ on for the environment in the dashboard. |
| `retry_after_reset` | Wait `Retry-After` seconds. |
| `retry` | Retry with exponential backoff. |
| `complete_integration_approval` | Request Live approval from the dashboard. |
| `update_developer_plan` | Activate the Developer plan. |
| `contact_integration_owner` | Not recoverable in code. Surface to the integration owner. |
| `do_not_repeat` | OnlyFans refused the request. Do not retry it unchanged. |
| `check_request` | Fix the request before retrying. |
| `check_resource` | The OnlyFans resource does not exist. |

## OAuth endpoint errors

`POST /v3/link/token` and `POST /v3/link/revoke` return RFC 6749 bodies.

```json theme={null}
{
  "error": "invalid_grant",
  "error_description": "request rejected",
  "reason": "developer_plan_unavailable",
  "recovery": { "action": "contact_integration_owner" }
}
```

| HTTP | `error` | Cause |
| - | - | - |
| 400 | `invalid_request` | Missing, duplicate, or unsupported field; body is not form-encoded; unsupported `grant_type`. |
| 401 | `invalid_client` | Bad Basic credentials, or the client is unavailable. |
| 400 | `invalid_grant` | Code or refresh token expired, used, replayed, or revoked; PKCE mismatch; redirect URI mismatch. |
| 400 | `invalid_scope` | Unknown scope or scope outside the grant. |
| 400 | `invalid_target` | `resource` is not `https://api.ofauth.com/v3`, or the client is in the wrong environment for the grant. |
| 429 | `rate_limited` | Too many requests or failed client authentications. Wait for `Retry-After`. |
| 500 | `server_error` | Retry with backoff. |

`reason` and `recovery` appear only for integration lifecycle problems (`client_deleted`, `developer_plan_unavailable`, `security_suspended`).

## Callback errors

The authorization redirect can return `error=access_denied` (user declined or cancelled) or `error=invalid_request` (the request expired or was malformed). Both include your original `state`.

## Handling pattern

```javascript Node.js theme={null}
const response = await fetch(url, { headers })
if (response.ok) return response.json()

const body = await response.json()
log.warn("ofauth error", { code: body.error, requestId: body.requestId })

switch (body.recovery?.action) {
  case "obtain_external_oauth_token":
    await refreshTokens(userId)
    return retryOnce()
  case "retry":
  case "retry_after_reset":
    return retryAfter(response.headers.get("Retry-After") ?? backoff())
  case "reconnect_linked_account":
    return promptReconnect(linkedAccountId)
  case "list_linked_accounts":
    return refreshLinkedAccounts(userId)
  case "restart_authorization":
  case "grant_linked_account":
  case "request_additional_consent":
    return promptReauthorize(userId)
  default:
    throw new Error(body.error)
}
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.