Skip to main content
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:

Confidential Clients

Server-side apps that can securely store secrets

Public Clients

Browser-based or mobile apps using PKCE

OAuth Flows

Authorization Code Flow (Confidential Clients)

Best for server-side applications with a backend.
1

Redirect to Authorization

Send users to the Cal.com authorization page
2

User Authorizes

User logs in and grants permissions
3

Receive Authorization Code

Cal.com redirects back with an authorization code
4

Exchange Code for Tokens

Exchange the code for access and refresh tokens

Authorization Code with PKCE (Public Clients)

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

Generate Code Verifier

Create a cryptographically random string
2

Generate Code Challenge

Hash the verifier with SHA-256
3

Redirect to Authorization

Include the code challenge in the authorization request
4

Exchange Code with Verifier

Exchange code using the original code verifier

Authorization Endpoint

Step 1: Redirect User to Authorization

Endpoint: GET /v2/auth/oauth2/authorize Parameters:
ParameterRequiredDescription
client_idYesYour OAuth client ID
redirect_uriYesMust match registered URI
response_typeYesAlways code
scopeYesSpace-separated list of scopes
stateRecommendedCSRF protection token
code_challengePKCE onlySHA-256 hash of code verifier
code_challenge_methodPKCE onlyAlways 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
Always verify the state parameter matches what you sent to prevent CSRF attacks.

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

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"

For Public Clients (PKCE)

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:
{
  "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

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

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:
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "READ_BOOKING READ_PROFILE"
}
Refresh tokens are single-use. Each refresh returns a new access token AND a new refresh token.

Using Access Tokens

Include the access token in the Authorization header:
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

ScopeDescription
READ_BOOKINGRead booking information
WRITE_BOOKINGCreate and update bookings

Profile Scopes

ScopeDescription
READ_PROFILERead user profile information
WRITE_PROFILEUpdate user profile

Event Type Scopes

ScopeDescription
READ_EVENT_TYPERead event type information
WRITE_EVENT_TYPECreate and update event types

Availability Scopes

ScopeDescription
READ_AVAILABILITYRead availability schedules
WRITE_AVAILABILITYUpdate availability schedules

Webhook Scopes

ScopeDescription
READ_WEBHOOKRead webhook configurations
WRITE_WEBHOOKCreate and update webhooks

Team Scopes

ScopeDescription
READ_TEAMRead team information
WRITE_TEAMManage team settings

PKCE Implementation

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

Generating Code Verifier and Challenge

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);

Client Information

Retrieve OAuth client details: Endpoint: GET /v2/auth/oauth2/clients/:clientId Request:
curl -X GET https://api.cal.com/v2/auth/oauth2/clients/your_client_id \
  -H "Authorization: Bearer YOUR_API_KEY"
Response:
{
  "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 CodeDescription
invalid_requestMissing required parameter
invalid_clientInvalid client ID or secret
invalid_grantInvalid or expired authorization code
unauthorized_clientClient not authorized for this grant type
unsupported_grant_typeGrant type not supported
invalid_scopeRequested scope is invalid or unknown
access_deniedUser denied authorization
Example Error Response:
{
  "error": "invalid_grant",
  "error_description": "The authorization code is invalid or expired"
}

Best Practices

  • 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
  • Refresh tokens proactively before expiration
  • Implement automatic retry with refresh on 401 errors
  • Handle refresh token expiration gracefully
  • Queue API requests during token refresh
  • 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
  • Request only the scopes you need
  • Explain scope usage to users clearly
  • Handle scope changes gracefully
  • Re-authorize if you need additional scopes

Testing OAuth Flow

Use the OAuth playground to test your implementation:
  1. Visit the 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 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

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);
}

Next Steps

Rate Limits

Learn about OAuth rate limits

Webhooks

Set up event notifications

API Reference

Browse all available endpoints

Security Best Practices

Implement security measures

Build docs developers (and LLMs) love