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

# OAuth 2.0

> Implement OAuth 2.0 authentication for third-party Cal.com integrations

Cal.com's Platform API supports OAuth 2.0 for secure third-party application authentication. This allows your application to access Cal.com data on behalf of users with their explicit permission.

## OAuth 2.0 Overview

OAuth 2.0 provides:

* **Secure delegated access** without sharing passwords
* **Scope-based permissions** to limit what your app can access
* **Token-based authentication** with automatic expiration
* **User consent flow** for transparency

## OAuth Client Setup

### Creating an OAuth Client

1. Log in to your Cal.com account
2. Navigate to **Settings** > **Security** > **OAuth Clients**
3. Click **Create New OAuth Client**
4. Configure your client:
   * **Name**: Your application name
   * **Redirect URIs**: Allowed callback URLs (comma-separated)
   * **Scopes**: Required permissions

### Client Types

Cal.com supports two OAuth client types:

<CardGroup cols={2}>
  <Card title="Confidential Clients" icon="lock">
    Server-side apps that can securely store secrets
  </Card>

  <Card title="Public Clients" icon="globe">
    Browser-based or mobile apps using PKCE
  </Card>
</CardGroup>

## OAuth Flows

### Authorization Code Flow (Confidential Clients)

Best for server-side applications with a backend.

<Steps>
  <Step title="Redirect to Authorization">
    Send users to the Cal.com authorization page
  </Step>

  <Step title="User Authorizes">
    User logs in and grants permissions
  </Step>

  <Step title="Receive Authorization Code">
    Cal.com redirects back with an authorization code
  </Step>

  <Step title="Exchange Code for Tokens">
    Exchange the code for access and refresh tokens
  </Step>
</Steps>

### Authorization Code with PKCE (Public Clients)

Best for single-page apps, mobile apps, or applications without a backend.

<Steps>
  <Step title="Generate Code Verifier">
    Create a cryptographically random string
  </Step>

  <Step title="Generate Code Challenge">
    Hash the verifier with SHA-256
  </Step>

  <Step title="Redirect to Authorization">
    Include the code challenge in the authorization request
  </Step>

  <Step title="Exchange Code with Verifier">
    Exchange code using the original code verifier
  </Step>
</Steps>

## Authorization Endpoint

### Step 1: Redirect User to Authorization

**Endpoint:** `GET /v2/auth/oauth2/authorize`

**Parameters:**

| Parameter               | Required    | Description                    |
| ----------------------- | ----------- | ------------------------------ |
| `client_id`             | Yes         | Your OAuth client ID           |
| `redirect_uri`          | Yes         | Must match registered URI      |
| `response_type`         | Yes         | Always `code`                  |
| `scope`                 | Yes         | Space-separated list of scopes |
| `state`                 | Recommended | CSRF protection token          |
| `code_challenge`        | PKCE only   | SHA-256 hash of code verifier  |
| `code_challenge_method` | PKCE only   | Always `S256`                  |

**Example Authorization URL:**

```
https://app.cal.com/v2/auth/oauth2/authorize?
  client_id=your_client_id&
  redirect_uri=https://yourapp.com/callback&
  response_type=code&
  scope=READ_BOOKING READ_PROFILE&
  state=random_state_string
```

**With PKCE:**

```
https://app.cal.com/v2/auth/oauth2/authorize?
  client_id=your_client_id&
  redirect_uri=https://yourapp.com/callback&
  response_type=code&
  scope=READ_BOOKING READ_PROFILE&
  state=random_state_string&
  code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
  code_challenge_method=S256
```

### Step 2: User Authorization

The user will see a consent screen showing:

* Your application name
* Requested permissions (scopes)
* Option to approve or deny access

### Step 3: Receive Authorization Code

After the user approves, Cal.com redirects to your `redirect_uri` with:

```
https://yourapp.com/callback?
  code=AUTH_CODE_HERE&
  state=random_state_string
```

<Warning>
  Always verify the `state` parameter matches what you sent to prevent CSRF attacks.
</Warning>

## Token Endpoint

### Exchange Authorization Code for Tokens

**Endpoint:** `POST /v2/auth/oauth2/token`

**Content-Type:** `application/x-www-form-urlencoded` or `application/json`

#### For Confidential Clients

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.cal.com/v2/auth/oauth2/token \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "grant_type=authorization_code" \
    -d "code=AUTH_CODE" \
    -d "redirect_uri=https://yourapp.com/callback" \
    -d "client_id=your_client_id" \
    -d "client_secret=your_client_secret"
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch('https://api.cal.com/v2/auth/oauth2/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code: 'AUTH_CODE',
      redirect_uri: 'https://yourapp.com/callback',
      client_id: 'your_client_id',
      client_secret: 'your_client_secret'
    })
  });

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

#### For Public Clients (PKCE)

```bash theme={null}
curl -X POST https://api.cal.com/v2/auth/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=AUTH_CODE" \
  -d "redirect_uri=https://yourapp.com/callback" \
  -d "client_id=your_client_id" \
  -d "code_verifier=ORIGINAL_CODE_VERIFIER"
```

**Response:**

```json theme={null}
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "READ_BOOKING READ_PROFILE"
}
```

## Refreshing Access Tokens

Access tokens expire after 1 hour. Use the refresh token to get a new access token without requiring user interaction.

**Endpoint:** `POST /v2/auth/oauth2/token`

### For Confidential Clients

```bash theme={null}
curl -X POST https://api.cal.com/v2/auth/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=REFRESH_TOKEN" \
  -d "client_id=your_client_id" \
  -d "client_secret=your_client_secret"
```

### For Public Clients

```bash theme={null}
curl -X POST https://api.cal.com/v2/auth/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=REFRESH_TOKEN" \
  -d "client_id=your_client_id"
```

**Response:**

```json theme={null}
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "READ_BOOKING READ_PROFILE"
}
```

<Note>
  Refresh tokens are single-use. Each refresh returns a new access token AND a new refresh token.
</Note>

## Using Access Tokens

Include the access token in the `Authorization` header:

```bash theme={null}
curl -X GET https://api.cal.com/v2/bookings \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

## OAuth Scopes

Scopes define what your application can access:

### Booking Scopes

| Scope           | Description                |
| --------------- | -------------------------- |
| `READ_BOOKING`  | Read booking information   |
| `WRITE_BOOKING` | Create and update bookings |

### Profile Scopes

| Scope           | Description                   |
| --------------- | ----------------------------- |
| `READ_PROFILE`  | Read user profile information |
| `WRITE_PROFILE` | Update user profile           |

### Event Type Scopes

| Scope              | Description                   |
| ------------------ | ----------------------------- |
| `READ_EVENT_TYPE`  | Read event type information   |
| `WRITE_EVENT_TYPE` | Create and update event types |

### Availability Scopes

| Scope                | Description                   |
| -------------------- | ----------------------------- |
| `READ_AVAILABILITY`  | Read availability schedules   |
| `WRITE_AVAILABILITY` | Update availability schedules |

### Webhook Scopes

| Scope           | Description                 |
| --------------- | --------------------------- |
| `READ_WEBHOOK`  | Read webhook configurations |
| `WRITE_WEBHOOK` | Create and update webhooks  |

### Team Scopes

| Scope        | Description           |
| ------------ | --------------------- |
| `READ_TEAM`  | Read team information |
| `WRITE_TEAM` | Manage team settings  |

## PKCE Implementation

PKCE (Proof Key for Code Exchange) adds security for public clients.

### Generating Code Verifier and Challenge

<CodeGroup>
  ```javascript JavaScript theme={null}
  function generateCodeVerifier() {
    const array = new Uint8Array(32);
    crypto.getRandomValues(array);
    return base64URLEncode(array);
  }

  async function generateCodeChallenge(verifier) {
    const encoder = new TextEncoder();
    const data = encoder.encode(verifier);
    const hash = await crypto.subtle.digest('SHA-256', data);
    return base64URLEncode(new Uint8Array(hash));
  }

  function base64URLEncode(buffer) {
    return btoa(String.fromCharCode(...buffer))
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
      .replace(/=/g, '');
  }

  // Usage
  const codeVerifier = generateCodeVerifier();
  const codeChallenge = await generateCodeChallenge(codeVerifier);

  // Store codeVerifier for later use
  sessionStorage.setItem('code_verifier', codeVerifier);
  ```

  ```python Python theme={null}
  import hashlib
  import base64
  import secrets

  def generate_code_verifier():
      return base64.urlsafe_b64encode(
          secrets.token_bytes(32)
      ).decode('utf-8').rstrip('=')

  def generate_code_challenge(verifier):
      digest = hashlib.sha256(verifier.encode('utf-8')).digest()
      return base64.urlsafe_b64encode(digest).decode('utf-8').rstrip('=')

  # Usage
  code_verifier = generate_code_verifier()
  code_challenge = generate_code_challenge(code_verifier)

  # Store code_verifier for later use
  ```
</CodeGroup>

## Client Information

Retrieve OAuth client details:

**Endpoint:** `GET /v2/auth/oauth2/clients/:clientId`

**Request:**

```bash theme={null}
curl -X GET https://api.cal.com/v2/auth/oauth2/clients/your_client_id \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response:**

```json theme={null}
{
  "status": "success",
  "data": {
    "id": "client_123",
    "name": "Your Application",
    "redirectUris": [
      "https://yourapp.com/callback"
    ],
    "scopes": [
      "READ_BOOKING",
      "WRITE_BOOKING",
      "READ_PROFILE"
    ]
  }
}
```

## Error Responses

### OAuth Errors

| Error Code               | Description                               |
| ------------------------ | ----------------------------------------- |
| `invalid_request`        | Missing required parameter                |
| `invalid_client`         | Invalid client ID or secret               |
| `invalid_grant`          | Invalid or expired authorization code     |
| `unauthorized_client`    | Client not authorized for this grant type |
| `unsupported_grant_type` | Grant type not supported                  |
| `invalid_scope`          | Requested scope is invalid or unknown     |
| `access_denied`          | User denied authorization                 |

**Example Error Response:**

```json theme={null}
{
  "error": "invalid_grant",
  "error_description": "The authorization code is invalid or expired"
}
```

## Best Practices

<AccordionGroup>
  <Accordion title="Secure Token Storage">
    * Store tokens securely (encrypted database or secure storage)
    * Never expose tokens in URLs or logs
    * Use HTTPOnly cookies for browser-based apps
    * Implement token encryption at rest
  </Accordion>

  <Accordion title="Token Refresh Strategy">
    * Refresh tokens proactively before expiration
    * Implement automatic retry with refresh on 401 errors
    * Handle refresh token expiration gracefully
    * Queue API requests during token refresh
  </Accordion>

  <Accordion title="State Parameter Usage">
    * Always use the state parameter for CSRF protection
    * Generate cryptographically random state values
    * Store state server-side or in session
    * Verify state matches on callback
  </Accordion>

  <Accordion title="Scope Management">
    * Request only the scopes you need
    * Explain scope usage to users clearly
    * Handle scope changes gracefully
    * Re-authorize if you need additional scopes
  </Accordion>
</AccordionGroup>

## Testing OAuth Flow

Use the OAuth playground to test your implementation:

1. Visit the [OAuth Playground](https://app.cal.com/oauth-playground)
2. Enter your client ID
3. Select scopes
4. Test the authorization flow
5. Inspect tokens and responses

## Rate Limits

OAuth-authenticated requests have higher rate limits:

* **OAuth Client**: 500 requests per 60 seconds
* **Access Token**: 500 requests per 60 seconds

See [Rate Limits](/api/rate-limits) for more details.

## Migration from API Keys

If you're migrating from API keys to OAuth:

1. Create an OAuth client
2. Implement the authorization flow
3. Update API requests to use access tokens
4. Test thoroughly before switching production traffic
5. Maintain API key support during transition

## Example Implementation

<CodeGroup>
  ```typescript TypeScript (Express) theme={null}
  import express from 'express';
  import axios from 'axios';

  const app = express();

  const CLIENT_ID = process.env.CAL_CLIENT_ID;
  const CLIENT_SECRET = process.env.CAL_CLIENT_SECRET;
  const REDIRECT_URI = 'http://localhost:3000/callback';

  // Step 1: Redirect to authorization
  app.get('/auth', (req, res) => {
    const state = generateRandomState();
    req.session.oauthState = state;
    
    const authUrl = new URL('https://app.cal.com/v2/auth/oauth2/authorize');
    authUrl.searchParams.set('client_id', CLIENT_ID);
    authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
    authUrl.searchParams.set('response_type', 'code');
    authUrl.searchParams.set('scope', 'READ_BOOKING WRITE_BOOKING');
    authUrl.searchParams.set('state', state);
    
    res.redirect(authUrl.toString());
  });

  // Step 2: Handle callback
  app.get('/callback', async (req, res) => {
    const { code, state } = req.query;
    
    // Verify state
    if (state !== req.session.oauthState) {
      return res.status(400).send('Invalid state');
    }
    
    try {
      // Exchange code for tokens
      const response = await axios.post(
        'https://api.cal.com/v2/auth/oauth2/token',
        new URLSearchParams({
          grant_type: 'authorization_code',
          code,
          redirect_uri: REDIRECT_URI,
          client_id: CLIENT_ID,
          client_secret: CLIENT_SECRET
        }),
        {
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
          }
        }
      );
      
      const { access_token, refresh_token } = response.data;
      
      // Store tokens securely
      req.session.accessToken = access_token;
      req.session.refreshToken = refresh_token;
      
      res.redirect('/dashboard');
    } catch (error) {
      res.status(500).send('Token exchange failed');
    }
  });

  function generateRandomState() {
    return Math.random().toString(36).substring(7);
  }
  ```
</CodeGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Rate Limits" icon="gauge" href="/api/rate-limits">
    Learn about OAuth rate limits
  </Card>

  <Card title="Webhooks" icon="webhook" href="/api/webhooks">
    Set up event notifications
  </Card>

  <Card title="API Reference" icon="book" href="/api/reference">
    Browse all available endpoints
  </Card>

  <Card title="Security Best Practices" icon="shield" href="/security">
    Implement security measures
  </Card>
</CardGroup>
