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

# Authorization with OAuth

## Overview

Scalev uses OAuth 2.0 Authorization Code with PKCE.

The flow is split across two surfaces:

* the merchant-facing authorization step starts on the Scalev frontend authorize page
* the token, introspection, revocation, and installation endpoints live on the `/v3/oauth/*` API surface

In practice, the flow looks like this:

1. Register your application with Scalev
2. Generate PKCE parameters
3. Redirect the merchant's browser to Scalev's authorize page
4. The merchant signs in and approves your app for one or more businesses
5. Your backend receives an authorization code
6. Your backend exchanges the code for tokens
7. Your backend uses the access token to call Scalev `/v3` APIs, selecting one business for each business-scoped request when needed
8. Your backend refreshes, introspects, or revokes tokens as needed

> The authorize page is a frontend route, not a machine API endpoint. It is intentionally separate from the `/v3` API contract.

## URLs

### Frontend Authorization URL

```text theme={null}
https://app.scalev.com/oauth/authorize
```

### API Base URL

```text theme={null}
https://api.scalev.com
```

All public machine OAuth endpoints in this guide live under:

```text theme={null}
https://api.scalev.com/v3/oauth/*
```

## Step 0: Register and Prepare Your App

Before merchants can install your app, create and configure your OAuth application in Scalev.

Important app fields include:

* `client_id`
* `client_secret`
* `redirect_uri`
* optional `manage_url`
* requested scopes
* optional webhook settings
* optional billing tags
* optional IP allowlist

Important rules:

* `redirect_uri` must match exactly
* `client_secret` must stay server-side
* if your app uses webhook events, configure the app webhook settings correctly
* if your app supports merchant launch links from Scalev, set `manage_url`
* merchant installation is blocked until the app is verified

## Step 1: Generate PKCE Parameters

Before redirecting the merchant, generate:

* a `code_verifier`
* a `code_challenge` derived from that verifier using SHA-256 and Base64 URL encoding

### Code Verifier Example (JavaScript)

```javascript theme={null}
function generateCodeVerifier(length = 100) {
  if (!Number.isInteger(length) || length < 43 || length > 128) {
    throw new Error("Length must be an integer between 43 and 128.");
  }

  const charset =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
  const randomValues = crypto.getRandomValues(new Uint8Array(length));

  return Array.from(
    randomValues,
    (byte) => charset[byte % charset.length],
  ).join("");
}
```

### Code Challenge Example (JavaScript)

```javascript theme={null}
function base64UrlEncode(buffer) {
  return btoa(String.fromCharCode(...new Uint8Array(buffer)))
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=+$/, "");
}

async function generateCodeChallenge(codeVerifier) {
  const encoder = new TextEncoder();
  const data = encoder.encode(codeVerifier);
  const digest = await crypto.subtle.digest("SHA-256", data);
  return base64UrlEncode(digest);
}
```

Store the `code_verifier` securely. You will need it during token exchange.

## Step 2: Redirect the Merchant to Scalev

Redirect the merchant's browser to:

```text theme={null}
https://app.scalev.com/oauth/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT_URI&response_type=code&state=RANDOM_STATE_STRING&code_challenge=CODE_CHALLENGE&code_challenge_method=S256
```

Required query parameters:

| Parameter               | Required | Description                                          |
| ----------------------- | -------- | ---------------------------------------------------- |
| `client_id`             | Yes      | Your OAuth app client ID                             |
| `redirect_uri`          | Yes      | Must exactly match the configured redirect URI       |
| `response_type`         | Yes      | Must be `code`                                       |
| `state`                 | Yes      | Random CSRF-protection value generated by your app   |
| `code_challenge`        | Yes      | Base64 URL encoded SHA-256 hash of the code verifier |
| `code_challenge_method` | Yes      | Must be `S256`                                       |

Current behavior:

* PKCE is required
* only `S256` is accepted
* the authorize entrypoint is the frontend URL, not the `/v3` API
* if the merchant is not signed in, Scalev will take them through login and then return them to the authorize page with the same OAuth query parameters

## Optional: Fetch Public App Metadata Before Redirect

If you want to preflight or display app metadata from your backend, call:

```http theme={null}
GET https://api.scalev.com/v3/oauth/application?client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT_URI
```

This returns public app metadata such as:

* `client_id`
* `name`
* `description`
* `logo_url`
* `homepage_url`
* `redirect_uri`

## Step 3: Merchant Reviews and Approves the App

On the Scalev authorize page, the merchant can review and approve:

* the app identity
* the businesses they want to connect
* the requested scopes
* webhook permissions, if applicable
* billing-tag approvals, if applicable

The merchant can select multiple businesses in one OAuth approval when their membership in each business allows app authorization. Scalev records access separately for each selected business. The same approved scopes, webhook settings, and billing-tag approvals apply to every selected business in the current flow.

The frontend then completes the consent flow and redirects the merchant back to your configured `redirect_uri`.

## Step 4: Receive the Authorization Code

After approval, your backend callback receives:

```text theme={null}
https://your-app.example.com/oauth/callback?code=AUTHORIZATION_CODE&state=RANDOM_STATE_STRING
```

Required validation:

1. verify the returned `state`
2. exchange the code promptly

Current authorization code behavior:

* authorization-code flow only
* code TTL is 10 minutes
* each code can be exchanged only once

## Step 5: Exchange the Code for Tokens

From your backend, exchange the code at:

```http theme={null}
POST https://api.scalev.com/v3/oauth/token
Content-Type: application/json

{
  "grant_type": "authorization_code",
  "code": "AUTHORIZATION_CODE",
  "code_verifier": "CODE_VERIFIER",
  "client_id": "YOUR_CLIENT_ID",
  "client_secret": "YOUR_CLIENT_SECRET"
}
```

Successful response:

```json theme={null}
{
  "access_token": "ACCESS_TOKEN",
  "refresh_token": "REFRESH_TOKEN",
  "token_type": "Bearer",
  "expires_in": 3600
}
```

Response fields:

| Field           | Description                                     |
| --------------- | ----------------------------------------------- |
| `access_token`  | Token used for authenticated `/v3` API requests |
| `refresh_token` | Token used to obtain a new access token         |
| `token_type`    | Always `Bearer`                                 |
| `expires_in`    | Access-token lifetime in seconds                |

## Step 6: Use the Access Token

Use the access token in the `Authorization` header:

```http theme={null}
Authorization: Bearer ACCESS_TOKEN
```

Your backend can then call the merchant-authorized `/v3` endpoints that match the granted scopes.

### Token Identity and Connected Businesses

Call `/v3/me` after receiving an access token:

```http theme={null}
GET https://api.scalev.com/v3/me
Authorization: Bearer ACCESS_TOKEN
```

For OAuth tokens, this returns token and user identity plus the active businesses connected to the token:

```json theme={null}
{
  "auth_method": "oauth",
  "user": {
    "id": 123,
    "unique_id": "USER123",
    "email": "user@example.com",
    "fullname": "Jane Doe",
    "avatar": "https://..."
  },
  "oauth_application": {
    "client_id": "client_abc",
    "name": "Example App"
  },
  "connected_businesses": [
    {
      "unique_id": "ABC123",
      "username": "store-a",
      "name": "Store A",
      "is_enabled": true,
      "scopes": ["order:list", "order:read"]
    }
  ]
}
```

`/v3/me` is token and user identity. It is not selected-business identity. Revoked or inactive OAuth installations are omitted from `connected_businesses[]`. If no active connected business remains, Scalev returns `403` without identity data.

### Select a Business for API Calls

Business-scoped `/v3` API calls always run against one selected business.

* if the token has one connected business, you can omit the selector
* if the token has multiple connected businesses, pass `b_uid=<business.unique_id>` on each business-scoped request
* use the `unique_id` from `/v3/me.connected_businesses[]`
* if access to the selected business is revoked or disabled, that business returns `403` while other active connected businesses remain usable

Example:

```http theme={null}
GET https://api.scalev.com/v3/orders?b_uid=ABC123
Authorization: Bearer ACCESS_TOKEN
```

Use the business `unique_id` returned by `/v3/me` as the API selector, for example `b_uid=ABC123`.

### Useful First Checks

#### Token Introspection

```http theme={null}
POST https://api.scalev.com/v3/oauth/introspect
Content-Type: application/json

{
  "token": "ACCESS_TOKEN",
  "token_type": "access",
  "client_id": "YOUR_CLIENT_ID",
  "client_secret": "YOUR_CLIENT_SECRET"
}
```

Use this to check whether the token is currently active for runtime use.

#### Installation Status Snapshot

```http theme={null}
POST https://api.scalev.com/v3/oauth/installation/status
Content-Type: application/json

{
  "token": "ACCESS_TOKEN",
  "token_type": "access",
  "client_id": "YOUR_CLIENT_ID",
  "client_secret": "YOUR_CLIENT_SECRET"
}
```

This returns the merchant installation snapshot, including:

* `authorized_business_id`
* `client_id`
* `is_active`
* `is_enabled`
* `granted_scopes`
* `webhook_status`
* `granted_webhook_events`
* `approved_billing_tags`
* `manage_launch_available`
* `updated_at`

Example:

```json theme={null}
{
  "authorized_business_id": 123,
  "client_id": "client_abc",
  "is_active": true,
  "is_enabled": true,
  "granted_scopes": ["order:list", "order:read"],
  "webhook_status": "active",
  "granted_webhook_events": ["payment.received", "shipment.status.updated"],
  "approved_billing_tags": [],
  "manage_launch_available": true,
  "updated_at": "2026-04-01T06:10:12.345678Z"
}
```

`/v3/oauth/installation/status` returns one merchant connection snapshot. If you pass a multi-business token reference, Scalev returns an error because this endpoint has no business selector. Use `/v3/me` to list businesses connected to a multi-business token.

## Step 7: Refresh Access Tokens

When the access token expires, use the refresh token:

```http theme={null}
POST https://api.scalev.com/v3/oauth/token
Content-Type: application/json

{
  "grant_type": "refresh_token",
  "refresh_token": "REFRESH_TOKEN",
  "client_id": "YOUR_CLIENT_ID",
  "client_secret": "YOUR_CLIENT_SECRET"
}
```

Successful response:

```json theme={null}
{
  "access_token": "NEW_ACCESS_TOKEN",
  "refresh_token": "NEW_REFRESH_TOKEN",
  "token_type": "Bearer",
  "expires_in": 3600
}
```

Important notes:

* always replace both the access token and the refresh token after a successful refresh
* refresh-token exchange is part of the `/v3/oauth/token` flow
* refresh reloads the token's connected businesses and keeps only business access that is active and enabled
* if one business is revoked or disabled, the refreshed tokens contain only the remaining businesses
* if the last connected business is revoked or disabled, refresh fails
* the old access token is not rewritten; until refresh or expiry, requests for a revoked or disabled selected business fail, but other connected businesses can still work

## Token Lifecycle

Current behavior:

* access token TTL: 1 hour
* refresh token TTL: 30 days

## Token Revocation

To revoke a token:

```http theme={null}
POST https://api.scalev.com/v3/oauth/revoke
Content-Type: application/json

{
  "token": "TOKEN_TO_REVOKE",
  "token_type": "refresh",
  "client_id": "YOUR_CLIENT_ID",
  "client_secret": "YOUR_CLIENT_SECRET"
}
```

Current `token_type` values are:

* `access`
* `refresh`

Successful revocation returns:

* HTTP `204 No Content`

## Optional: Merchant Manage Launch Flow

If your app defines a `manage_url`, Scalev can launch merchants into your app with a short-lived `launch_token`.

Exchange that token from your backend with:

```http theme={null}
POST https://api.scalev.com/v3/oauth/installation/launch-token/exchange
Content-Type: application/json

{
  "launch_token": "LAUNCH_TOKEN",
  "client_id": "YOUR_CLIENT_ID",
  "client_secret": "YOUR_CLIENT_SECRET"
}
```

This returns the same installation snapshot shape as `/v3/oauth/installation/status`.

Important notes:

* the `launch_token` is one-time and short-lived
* it is not an access token
* there is no shared manage-link secret; your backend authenticates the exchange with `client_id` and `client_secret`

## Best Practices

1. Redirect merchants to the frontend authorize page, not to a backend `/v2` API endpoint.
2. Generate a new PKCE verifier and challenge for every install attempt.
3. Validate `state` on every callback.
4. Keep `client_secret`, access tokens, and refresh tokens on the server only.
5. Use only the scopes, webhook events, and billing tags your app actually needs.
6. Treat `redirect_uri` as an exact-match value.
7. Use `/v3/oauth/installation/status` when you need the authoritative merchant connection snapshot.
8. Use `/v3/oauth/introspect` for runtime token diagnostics.
9. Handle disabled business access separately from revoked business access.

## Rate Limiting

OAuth access tokens use rate limits scoped to the OAuth application and connected business.

Current default:

* **10,000 requests per hour per connected business**
* **100 requests per 10 seconds per connected business** for short bursts

Rate-limit responses return `429 Too Many Requests` with `X-Ratelimit-Limit`, `X-Ratelimit-Remaining`, and `X-Ratelimit-Reset` headers. Some endpoint-specific limits are stricter, and rate-limit responses may be plain text instead of the normal JSON error shape.

## Error Handling

OAuth endpoints may return errors such as:

| Error Code            | Description                                                               |
| --------------------- | ------------------------------------------------------------------------- |
| `invalid_request`     | Missing or malformed request parameters                                   |
| `invalid_client`      | Client authentication failed                                              |
| `invalid_grant`       | Authorization code, refresh token, or PKCE verifier is invalid or expired |
| `unauthorized_client` | The client is not allowed to use the requested grant type                 |
| `server_error`        | Unexpected server-side failure                                            |

Typical error response shape:

```json theme={null}
{
  "error": "Error message",
  "error_code": "invalid_grant"
}
```

PKCE-related failures typically surface as:

* `invalid_grant` when the code verifier does not match
* `invalid_request` when required PKCE parameters are missing or malformed
