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
Log in to your Cal.com account
Navigate to Settings > Security > OAuth Clients
Click Create New OAuth Client
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.
Redirect to Authorization
Send users to the Cal.com authorization page
User Authorizes
User logs in and grants permissions
Receive Authorization Code
Cal.com redirects back with an authorization code
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.
Generate Code Verifier
Create a cryptographically random string
Generate Code Challenge
Hash the verifier with SHA-256
Redirect to Authorization
Include the code challenge in the authorization request
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:
Parameter Required Description client_idYes Your OAuth client ID redirect_uriYes Must match registered URI response_typeYes Always code scopeYes Space-separated list of scopes stateRecommended CSRF protection token code_challengePKCE only SHA-256 hash of code verifier code_challenge_methodPKCE 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
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
Scope Description READ_BOOKINGRead booking information WRITE_BOOKINGCreate and update bookings
Profile Scopes
Scope Description READ_PROFILERead user profile information WRITE_PROFILEUpdate user profile
Event Type Scopes
Scope Description READ_EVENT_TYPERead event type information WRITE_EVENT_TYPECreate and update event types
Availability Scopes
Scope Description READ_AVAILABILITYRead availability schedules WRITE_AVAILABILITYUpdate availability schedules
Webhook Scopes
Scope Description READ_WEBHOOKRead webhook configurations WRITE_WEBHOOKCreate and update webhooks
Team Scopes
Scope Description 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 );
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 Code Description 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:
Visit the OAuth Playground
Enter your client ID
Select scopes
Test the authorization flow
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:
Create an OAuth client
Implement the authorization flow
Update API requests to use access tokens
Test thoroughly before switching production traffic
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