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

# OAuth Integration

> Integrate Profy user identity and billing using the OAuth 2.0 Authorization Code flow

Profy implements the standard OAuth 2.0 Authorization Code flow, allowing your App to call Profy APIs on behalf of users. With OAuth authorization, credits are deducted from the user's account.

## Flow Overview

```
┌──────┐     ┌──────────┐     ┌────────────┐     ┌────────────┐
│ User │────→│ Your App │────→│ Profy Auth │────→│   User     │
└──────┘     └──────────┘     │   Page     │     │  Consents  │
                              └────────────┘     └─────┬──────┘
                                                       │
     ┌──────────┐     ┌────────────┐     ┌─────────────┘
     │ Call API │←───│ Exchange   │←───│ Auth Code
     └──────────┘     │  Token     │     │ Callback
                      └────────────┘     └─────────────
```

## Step 1: Register Your App

Create your App in Profy Studio:

1. Go to [app.profy.cn/studio](https://app.profy.cn/studio)
2. Create a new App
3. Configure OAuth settings in the App settings:
   * **Client ID**: Automatically generated App UUID
   * **Client Secret**: Generated in settings (stored as SHA-256 hash, displayed only once)
   * **Redirect URIs**: Register your callback URLs (multiple supported)

<Warning>
  The Client Secret is displayed only once when generated — save it securely. The Redirect URI must exactly match one of the registered addresses, or authorization will be rejected.
</Warning>

## Step 2: Redirect the User to Authorize

Redirect the user to the Profy authorization page:

```
GET https://api.profy.cn/oauth/authorize
  ?client_id={your_app_uuid}
  &redirect_uri={your_redirect_uri}
  &response_type=code
  &scope=events:write
  &state={random_state}
```

| Parameter       | Required    | Description                                   |
| --------------- | ----------- | --------------------------------------------- |
| `client_id`     | ✅           | Your App UUID                                 |
| `redirect_uri`  | ✅           | A registered callback URL                     |
| `response_type` | ✅           | Must be `code`                                |
| `scope`         | No          | Requested permission scopes (space-separated) |
| `state`         | Recommended | Random string for CSRF protection             |

The user will see the Profy authorization page displaying your App name and the requested permissions. After the user consents, Profy will redirect them back to your callback URL.

## Step 3: Receive the Authorization Code

After the user authorizes, Profy redirects to your `redirect_uri`:

```
https://your-app.com/callback?code={authorization_code}&state={your_state}
```

<Warning>
  The Authorization Code is valid for **5 minutes** and can only be used once. Exchange it for a token immediately upon receipt.
</Warning>

## Step 4: Exchange for Tokens

Use the authorization code to obtain an Access Token and Refresh Token:

<CodeGroup>
  ```python Python theme={null}
  import httpx

  async def exchange_token(code: str, redirect_uri: str):
      async with httpx.AsyncClient() as client:
          resp = await client.post(
              "https://api.profy.cn/oauth/token",
              json={
                  "grant_type": "authorization_code",
                  "code": code,
                  "redirect_uri": redirect_uri,
                  "client_id": "your-app-uuid",
                  "client_secret": "your-client-secret",
              },
          )
          return resp.json()

  # Example response:
  # {
  #   "access_token": "eyJhbGciOiJSUzI1NiIs...",
  #   "token_type": "Bearer",
  #   "expires_in": 3600,
  #   "refresh_token": "a1b2c3d4e5f6...",
  #   "scope": "events:write"
  # }
  ```

  ```typescript TypeScript theme={null}
  async function exchangeToken(code: string, redirectUri: string) {
    const resp = await fetch("https://api.profy.cn/oauth/token", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        grant_type: "authorization_code",
        code,
        redirect_uri: redirectUri,
        client_id: "your-app-uuid",
        client_secret: "your-client-secret",
      }),
    });
    return resp.json();
  }
  ```

  ```bash curl theme={null}
  curl -X POST https://api.profy.cn/oauth/token \
    -H "Content-Type: application/json" \
    -d '{
      "grant_type": "authorization_code",
      "code": "your-auth-code",
      "redirect_uri": "https://your-app.com/callback",
      "client_id": "your-app-uuid",
      "client_secret": "your-client-secret"
    }'
  ```
</CodeGroup>

The token endpoint also supports `application/x-www-form-urlencoded` format.

## Token Lifecycle

| Token Type        | Validity | Purpose                       |
| ----------------- | -------- | ----------------------------- |
| **Access Token**  | 1 hour   | API call authentication       |
| **Refresh Token** | 90 days  | Exchange for a new token pair |

### Access Token

* JWT format (RS256 signed)
* Contains `sub` (user ID), `aud` (App UUID), `scope` (permission scopes)
* Use the Refresh Token to obtain a new token when expired

### Refresh Token

* Opaque string format
* **Rotation mechanism**: The old token is invalidated after each use; a new Refresh Token is returned
* A used Refresh Token cannot be reused

## Step 5: Refresh Tokens

When the Access Token expires, use the Refresh Token to obtain a new token pair:

<CodeGroup>
  ```python Python theme={null}
  async def refresh_tokens(refresh_token: str):
      async with httpx.AsyncClient() as client:
          resp = await client.post(
              "https://api.profy.cn/oauth/token",
              json={
                  "grant_type": "refresh_token",
                  "refresh_token": refresh_token,
                  "client_id": "your-app-uuid",
                  "client_secret": "your-client-secret",
              },
          )
          data = resp.json()
          # Important: Save the new refresh_token — the old one is now invalid
          new_access_token = data["access_token"]
          new_refresh_token = data["refresh_token"]
          return new_access_token, new_refresh_token
  ```

  ```typescript TypeScript theme={null}
  async function refreshTokens(refreshToken: string) {
    const resp = await fetch("https://api.profy.cn/oauth/token", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        grant_type: "refresh_token",
        refresh_token: refreshToken,
        client_id: "your-app-uuid",
        client_secret: "your-client-secret",
      }),
    });
    const data = await resp.json();
    // Important: Save the new refresh_token — the old one is now invalid
    return { accessToken: data.access_token, refreshToken: data.refresh_token };
  }
  ```

  ```bash curl theme={null}
  curl -X POST https://api.profy.cn/oauth/token \
    -H "Content-Type: application/json" \
    -d '{
      "grant_type": "refresh_token",
      "refresh_token": "your-refresh-token",
      "client_id": "your-app-uuid",
      "client_secret": "your-client-secret"
    }'
  ```
</CodeGroup>

<Warning>
  Refresh Tokens use a rotation mechanism. After each refresh, you must save the **new** Refresh Token. The old Refresh Token is immediately invalidated after use.
</Warning>

## Revoking Tokens

When a user logs out or you need to revoke access, call the revocation endpoint:

```bash theme={null}
curl -X POST https://api.profy.cn/oauth/revoke \
  -H "Content-Type: application/json" \
  -d '{
    "token": "your-refresh-token",
    "client_id": "your-app-uuid",
    "client_secret": "your-client-secret"
  }'
```

The revocation endpoint follows RFC 7009 — it returns `200 OK` even if the token is already invalid.

## Available Scopes

| Scope          | Description                                                                         |
| -------------- | ----------------------------------------------------------------------------------- |
| `events:write` | Allows calling `/v1/agents/run`, `/v1/chat/completions`, `/v1/events`, `/v1/models` |

## Security Best Practices

<AccordionGroup>
  <Accordion title="Use the state parameter to prevent CSRF">
    Generate a random state value when initiating the authorization request, and verify that the state matches in the callback.
  </Accordion>

  <Accordion title="Store tokens securely">
    * Server-side: Use encrypted storage (encrypted database columns, secrets management services)
    * Client-side: Use httpOnly cookies or secure storage
    * Do not store Refresh Tokens in URLs, logs, or frontend localStorage
  </Accordion>

  <Accordion title="Strictly match Redirect URIs">
    Profy strictly validates that the redirect\_uri is in the registered list. Ensure registered URIs include the full path to avoid open redirect vulnerabilities.
  </Accordion>

  <Accordion title="Store Client Secret securely">
    The Client Secret should only be used on the server side. Do not expose it in frontend code. Use environment variables or a secrets management service for storage.
  </Accordion>
</AccordionGroup>

## Error Handling

| Error                       | HTTP Status | Description                                                               |
| --------------------------- | ----------- | ------------------------------------------------------------------------- |
| `invalid_client`            | 401         | Client ID does not exist or Client Secret is incorrect                    |
| `invalid_grant`             | 400         | Authorization code expired/invalid, or Refresh Token already used/expired |
| `invalid_request`           | 400         | Missing required parameters or redirect\_uri not registered               |
| `unsupported_grant_type`    | 400         | grant\_type is not `authorization_code` or `refresh_token`                |
| `unsupported_response_type` | 400         | response\_type is not `code`                                              |

## Next Steps

<CardGroup cols={2}>
  <Card title="Events API" icon="bolt" href="/en/developers/api/events">
    Report custom billing events via OAuth Token
  </Card>

  <Card title="App Development Guide" icon="grid-2" href="/en/developers/app-development">
    Complete App development workflow
  </Card>
</CardGroup>
