> ## 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.

# Authorize

> Build the authorization request and handle the callback

V3 uses the OAuth 2.0 Authorization Code flow with PKCE. Your backend owns the client secret, PKCE verifier, state, and token exchange.

## Create the authorization request

Build the URL on your backend and send the user to it.

```http theme={null}
GET https://auth.ofauth.com/link/authorize
```

| Parameter | Requirement |
| - | - |
| `response_type` | Use `code`. |
| `client_id` | The client ID for the target environment. It selects the environment. |
| `redirect_uri` | An exact registered redirect URI for that client. |
| `state` | A random, one-use value. Returned on the redirect. |
| `code_challenge` | base64url SHA-256 digest of the PKCE verifier (43 characters). |
| `code_challenge_method` | Use `S256`. |
| `scope` | Space-separated scopes. Add `offline_access` to receive a refresh token. See [scopes](/v3/access#scopes). |
| `purpose` | Required for Live. A short statement of why the application needs access, shown on the consent screen. Must be omitted for Sandbox and Test. |
| `linked_account_ids` | Optional, Live only. Used after a V2 migration. See [Group accounts under one person](/guides/migrate-v2-to-v3#group-accounts-under-one-person). |

Each parameter may appear once. Unknown or repeated parameters return `invalid_request`.

You do not send `resource`. If your OAuth client library always sends one, it must be `https://api.ofauth.com/v3`; any other value returns `invalid_request`.

<CodeGroup>
  ```bash curl theme={null}
  # Build the URL; open it in the user's browser.
  VERIFIER=$(openssl rand -base64 32 | tr -d '=+/' | cut -c1-43)
  CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=')
  STATE=$(openssl rand -hex 16)

  echo "https://auth.ofauth.com/link/authorize?\
  response_type=code&\
  client_id=$OFAUTH_SANDBOX_CLIENT_ID&\
  redirect_uri=https%3A%2F%2Fdeveloper.example.com%2Foauth%2Fofauth%2Fcallback&\
  state=$STATE&\
  code_challenge=$CHALLENGE&\
  code_challenge_method=S256&\
  scope=profile%3Aread%20messages%3Aread%20offline_access"
  ```

  ```javascript Node.js theme={null}
  import { createHash, randomBytes } from "node:crypto"

  const codeVerifier = randomBytes(32).toString("base64url")
  const codeChallenge = createHash("sha256")
    .update(codeVerifier)
    .digest("base64url")
  const state = randomBytes(32).toString("base64url")

  await storePendingAuthorization({ state, codeVerifier, userId })

  const authorizationUrl = new URL("https://auth.ofauth.com/link/authorize")
  authorizationUrl.search = new URLSearchParams({
    response_type: "code",
    client_id: process.env.OFAUTH_LIVE_CLIENT_ID,
    redirect_uri: "https://developer.example.com/oauth/ofauth/callback",
    state,
    code_challenge: codeChallenge,
    code_challenge_method: "S256",
    scope: "profile:read messages:read offline_access",
    purpose: "Read the selected OnlyFans profile and inbox"
  }).toString()

  // Send only authorizationUrl to the browser.
  ```
</CodeGroup>

Keep the PKCE verifier, client secret, and state record on your backend. Send only the authorization URL to the browser or native app.

## Redirect URI rules

Redirect URIs are registered per environment client in the dashboard. The `redirect_uri` parameter must match a registered value exactly.

| Rule | Sandbox and Test | Live |
| - | - | - |
| HTTPS with a domain hostname | Allowed | Required |
| `http://127.0.0.1:<port>/...` or `http://[::1]:<port>/...` | Allowed | Rejected |
| `http://localhost` or any other `http://` host | Rejected | Rejected |
| IP address, `localhost`, or `*.localhost` over HTTPS | Rejected | Rejected |
| Fragment (`#`) | Rejected | Rejected |
| Wildcard (`*`) | Rejected | Rejected |
| Userinfo (`user:pass@`) | Rejected | Rejected |

The URI must be an absolute, canonical URL. Register the exact string you will send. Each client can have up to 20 redirect URIs.

## Open the system browser

Open OFAuth as a top-level browser journey.

| Platform | Required browser surface |
| - | - |
| Web | The system browser or a top-level browser window. |
| iOS | `ASWebAuthenticationSession`. |
| Android | A Custom Tab. |

Do not use an iframe or `postMessage`. Do not send the client secret or PKCE verifier to the browser or native app.

## What the user sees

1. The user signs in to OFAuth or creates an account.
2. The user links an OnlyFans account, or chooses accounts they or their organization already linked.
3. The user approves the scopes you requested for the chosen accounts.
4. OFAuth redirects to your callback.

A linked account belongs to its owner, not to your integration. The owner can share the same account with several apps, and can revoke your access from the OFAuth dashboard at any time.

### Accounts linked later

The consent screen can offer the owner **Also share accounts I link later**. It is off by default, and it is not offered on every consent screen. If the owner turns it on, accounts are added to your authorization with the scopes they approved, and you receive `authorization.updated` with `reason: "auto_added"`:

* In Live, each OnlyFans account the owner newly links afterwards is added.
* In Test, only accounts designated as test accounts are added, when they are designated, and only if they were linked after auto-add was turned on.
* Only new accounts are added. Reconnecting an existing account does not add it, and an account the owner removed from your authorization is never added back.
* The owner can turn the setting off from the third-party apps page in their dashboard. Accounts already shared stay shared.

## Handle the callback

OFAuth returns only to the exact registered redirect URI.

| Result | Query parameters |
| - | - |
| Success | `code=<opaque>&state=<original>` |
| Denial or cancellation | `error=access_denied&state=<original>` |
| Expired or invalid request | `error=invalid_request&state=<original>` |

If the request cannot be tied to a registered redirect URI, OFAuth shows the error instead of redirecting (`invalid_request`, `invalid_scope`, `unauthorized_client`, or `access_denied` with a `reason`).

The authorization code expires after 5 minutes and is single-use.

Your backend must consume the state once. Reject a missing, changed, expired, or reused state. Then handle the error or [exchange the code](/v3/tokens#exchange-the-authorization-code).

```javascript Node.js theme={null}
const callbackUrl = new URL(request.url)
const state = callbackUrl.searchParams.get("state")
if (!state) throw new Error("Missing OAuth state")

const pending = await consumePendingAuthorization(state)
if (!pending) throw new Error("Invalid OAuth state")

const error = callbackUrl.searchParams.get("error")
if (error) return handleAuthorizationError(error)

const code = callbackUrl.searchParams.get("code")
if (!code) throw new Error("Missing authorization code")

const tokens = await exchangeAuthorizationCode({
  code,
  codeVerifier: pending.codeVerifier
})
```

## Return to a native app

The native app asks its backend for the authorization URL. It opens that URL in `ASWebAuthenticationSession` or a Custom Tab.

After the HTTPS callback, the backend can open a one-use Universal Link or App Link. Put only an opaque developer-owned attempt reference in that link. Keep the authorization code, OAuth state, tokens, client secret, and PKCE verifier on the backend.

Do not use a custom URL protocol. Do not send OFAuth directly to a native callback. Do not exchange tokens from a public native client.

## Requesting more scopes or accounts later

Run the authorization flow again with the larger scope set. The owner approves the additional scopes, and can add or remove accounts. The authorization keeps its `authorization_id`, and you receive `authorization.updated` with `reason: "consent_updated"`. Access calls that need a scope you have not been granted fail with `missing_scope` or `additional_consent_required`. See [Errors](/v3/errors).


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