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

# Authorization code flow

> The user consent flow with PKCE for apps that act on behalf of a user

Use this flow when people sign in to your app with their own account. It requires a **Web
application** and PKCE with the S256 method.

## How it works

<Steps>
  <Step title="Send the user to the authorize endpoint">
    Your app redirects the browser to `/oauth/authorize` with your `client_id`, a
    `redirect_uri`, the `scope`, a `state`, and a PKCE `code_challenge`.
  </Step>

  <Step title="The user approves">
    We show the consent screen with the scopes you requested. The user approves or denies.
  </Step>

  <Step title="We redirect back with a code">
    On approval we redirect to your `redirect_uri` with a one time `code` and your `state`.
    On denial it redirects with `error=access_denied`.
  </Step>

  <Step title="Exchange the code for tokens">
    Your server calls the token endpoint with the `code` and the PKCE `code_verifier` and
    receives an access token and a refresh token.
  </Step>
</Steps>

## Generate PKCE values

PKCE is required. Create a random `code_verifier`, then derive the `code_challenge` as the
base64url encoded SHA256 of the verifier.

```ts pkce.ts theme={null}
import { createHash, randomBytes } from "node:crypto";

function base64url(input: Buffer) {
  return input.toString("base64url");
}

const code_verifier = base64url(randomBytes(32));
const code_challenge = base64url(
  createHash("sha256").update(code_verifier).digest(),
);
```

<Tip>
  The SDK does this for you through `oauth.authorize_url()`. Reach for it unless you need the
  raw flow.
</Tip>

## Step 1: authorize request

Redirect the user to the authorize endpoint.

```
GET https://platform.nemu.cc/oauth/authorize
```

<ParamField query="response_type" type="string" required>
  Must be `code`.
</ParamField>

<ParamField query="client_id" type="string" required>
  Your application `client_id`.
</ParamField>

<ParamField query="redirect_uri" type="string" required>
  Must exactly match one of the redirect URIs registered on the application.
</ParamField>

<ParamField query="scope" type="string" required>
  A space separated list of scopes. See [Scopes](/oauth/scopes).
</ParamField>

<ParamField query="code_challenge" type="string" required>
  The base64url SHA256 of your `code_verifier`.
</ParamField>

<ParamField query="code_challenge_method" type="string" required>
  Must be `S256`.
</ParamField>

<ParamField query="state" type="string">
  An opaque value your app generates. We return it unchanged so you can defend against cross
  site request forgery.
</ParamField>

A complete URL looks like this.

```
https://platform.nemu.cc/oauth/authorize?response_type=code&client_id=cl_123&redirect_uri=https%3A%2F%2Fyour.app%2Fcallback&scope=profile%3Aread%20workspaces%3Aread&state=xyz&code_challenge=abc&code_challenge_method=S256
```

## Step 2: the redirect back

After the user approves, we redirect to your `redirect_uri`.

```
https://your.app/callback?code=one_time_code&state=xyz
```

If the user denies, or the request is invalid, we redirect with an error.

```
https://your.app/callback?error=access_denied&state=xyz
```

<Warning>
  Always compare the returned `state` to the value you sent before you trust the `code`.
</Warning>

## Step 3: exchange the code

Your server exchanges the `code` at the token endpoint. Send your `client_secret` and the
matching `code_verifier`.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST 'https://platform.nemu.cc/api/oauth/token' \
    -H 'Content-Type: application/x-www-form-urlencoded' \
    -d 'grant_type=authorization_code' \
    -d 'code=one_time_code' \
    -d 'redirect_uri=https://your.app/callback' \
    -d 'code_verifier=your_code_verifier' \
    -d 'client_id=cl_123' \
    -d 'client_secret=cls_456'
  ```

  ```ts Node theme={null}
  const body = new URLSearchParams({
    grant_type: "authorization_code",
    code: "one_time_code",
    redirect_uri: "https://your.app/callback",
    code_verifier: "your_code_verifier",
    client_id: "cl_123",
    client_secret: "cls_456",
  });

  const res = await fetch("https://platform.nemu.cc/api/oauth/token", {
    method: "POST",
    headers: { "content-type": "application/x-www-form-urlencoded" },
    body,
  });

  const tokens = await res.json();
  ```
</CodeGroup>

A successful response returns the tokens.

```json theme={null}
{
  "access_token": "eyJhbGciOi...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "rt_...",
  "scope": "profile:read workspaces:read"
}
```

## Step 4: call the API

Send the access token as a bearer token. Read the user who approved access from
`GET /api/v1/user`.

```bash theme={null}
curl 'https://platform.nemu.cc/api/v1/user' \
  -H 'Authorization: Bearer eyJhbGciOi...'
```

```json theme={null}
{
  "data": {
    "id": "3f0e...",
    "email": "user@example.com",
    "username": "user",
    "role": "USER",
    "account_type": "PERSONAL",
    "plan": "pro",
    "email_verified": true,
    "created_at": "2026-01-04T10:22:00.000Z"
  }
}
```

## Refresh the access token

Access tokens last one hour. Use the refresh token to get a new one. Refresh tokens rotate,
so replace the stored refresh token with the one you receive back.

```bash theme={null}
curl -X POST 'https://platform.nemu.cc/api/oauth/token' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=refresh_token' \
  -d 'refresh_token=rt_...' \
  -d 'client_id=cl_123' \
  -d 'client_secret=cls_456'
```

## Effective scopes

The access token is limited to the scopes the user approved, intersected with the scopes
the application currently holds. If you later remove a scope from the application, existing
tokens lose that scope on their next request.

## Errors

<ResponseField name="access_denied" type="redirect error">
  The user declined on the consent screen.
</ResponseField>

<ResponseField name="invalid_grant" type="token error">
  The code was already used, expired, or the `code_verifier` did not match the
  `code_challenge`.
</ResponseField>

<ResponseField name="invalid_client" type="token error">
  The `client_id` or `client_secret` is wrong, or the app is disabled.
</ResponseField>

<ResponseField name="401 Insufficient scope" type="api error">
  The token is valid but does not carry the scope the endpoint requires.
</ResponseField>
