Skip to main content
As an example, you can view our OAuth flow in action on Zapier. Try to connect your Cal.com account here. To enable OAuth in one of your apps, you will need a Client ID, Client Secret, Authorization URL, Access Token Request URL, and Refresh Token Request URL.

Get your OAuth “Continue with Cal.com” Badge

1. OAuth Client Credentials

You can create an OAuth client via the following page https://app.cal.com/settings/developer/oauth. The OAuth client will be in a “pending” state and not yet ready to use. You must select at least one scope when creating the OAuth client. An admin from Cal.com will then review your OAuth client and you will receive an email if it was accepted or rejected. If it was accepted then your OAuth client is ready to be used.

Available Scopes

Scopes control which API endpoints the OAuth token can access. Once a user authorizes your client with a given set of scopes, the issued access token can only be used to call endpoints covered by those scopes — any request to an endpoint outside the granted scopes will be rejected. The following scopes are available:
ScopeDescriptionEndpoints
EVENT_TYPE_READView event typesGet all event types,
Get an event type,
Get event type private links
EVENT_TYPE_WRITECreate, edit, and delete event typesCreate an event type,
Update an event type,
Delete an event type,
Create a private link,
Update a private link,
Delete a private link
BOOKING_READView bookingsGet all bookings,
Get booking recordings,
Get transcript download links,
Get calendar links,
Get booking references,
Get conferencing sessions
BOOKING_WRITECreate, edit, and delete bookingsAdd guests to a booking,
Update booking location,
Mark a booking absence,
Reassign to auto-selected host,
Reassign to a specific host,
Confirm a booking,
Decline a booking
SCHEDULE_READView availabilityGet all schedules,
Get a schedule,
Get default schedule
SCHEDULE_WRITECreate, edit, and delete availabilityCreate a schedule,
Update a schedule,
Delete a schedule
APPS_READView connected appsCheck a calendar connection
APPS_WRITEConnect and disconnect appsNo endpoints currently use this scope
PROFILE_READView personal infoGet my profile
PROFILE_WRITEEdit personal infoUpdate my profile
Some endpoints like POST /v2/bookings (create), POST /v2/bookings/:bookingUid/cancel (cancel), and POST /v2/bookings/:bookingUid/reschedule (reschedule) are public endpoints that do not require any scope.

Team Scopes

Team scopes control access to team-level resources. These are used for endpoints under /v2/teams/:teamId/... and /v2/organizations/:orgId/teams/:teamId/....
ScopeDescriptionEndpoints
TEAM_EVENT_TYPE_READView team event typesGET /v2/teams/:teamId/event-types,
GET /v2/teams/:teamId/event-types/:eventTypeId,
GET /v2/organizations/:orgId/teams/:teamId/event-types,
GET /v2/organizations/:orgId/teams/:teamId/event-types/:eventTypeId
TEAM_EVENT_TYPE_WRITECreate, edit, and delete team event typesPOST /v2/teams/:teamId/event-types,
PATCH /v2/teams/:teamId/event-types/:eventTypeId,
DELETE /v2/teams/:teamId/event-types/:eventTypeId,
POST /v2/teams/:teamId/event-types/:eventTypeId/create-phone-call,
POST /v2/organizations/:orgId/teams/:teamId/event-types,
PATCH /v2/organizations/:orgId/teams/:teamId/event-types/:eventTypeId,
DELETE /v2/organizations/:orgId/teams/:teamId/event-types/:eventTypeId,
POST /v2/organizations/:orgId/teams/:teamId/event-types/:eventTypeId/create-phone-call
TEAM_BOOKING_READView team bookingsGET /v2/teams/:teamId/bookings,
GET /v2/organizations/:orgId/teams/:teamId/bookings,
GET /v2/organizations/:orgId/teams/:teamId/bookings/:bookingUid/references
TEAM_BOOKING_WRITECreate, edit, and delete team bookingsNo endpoints currently use this scope
TEAM_SCHEDULE_READView team schedulesGET /v2/teams/:teamId/schedules,
GET /v2/organizations/:orgId/teams/:teamId/schedules,
GET /v2/organizations/:orgId/teams/:teamId/users/:userId/schedules
TEAM_SCHEDULE_WRITECreate, edit, and delete team schedulesNo endpoints currently use this scope
TEAM_PROFILE_READView team profilesGET /v2/teams,
GET /v2/teams/:teamId,
GET /v2/organizations/:orgId/teams/:teamId
TEAM_PROFILE_WRITECreate, edit, and delete teamsPOST /v2/teams,
PATCH /v2/teams/:teamId,
DELETE /v2/teams/:teamId
TEAM_MEMBERSHIP_READView team membershipsGET /v2/teams/:teamId/memberships,
GET /v2/teams/:teamId/memberships/:membershipId,
GET /v2/organizations/:orgId/teams/:teamId/memberships,
GET /v2/organizations/:orgId/teams/:teamId/memberships/:membershipId
TEAM_MEMBERSHIP_WRITECreate, edit, and delete team membershipsPOST /v2/teams/:teamId/memberships,
PATCH /v2/teams/:teamId/memberships/:membershipId,
DELETE /v2/teams/:teamId/memberships/:membershipId,
POST /v2/teams/:teamId/invite,
POST /v2/organizations/:orgId/teams/:teamId/memberships,
PATCH /v2/organizations/:orgId/teams/:teamId/memberships/:membershipId,
DELETE /v2/organizations/:orgId/teams/:teamId/memberships/:membershipId,
POST /v2/organizations/:orgId/teams/:teamId/invite

Organization Scopes

Organization scopes control access to organization-wide resources. These are used for endpoints under /v2/organizations/:orgId/... that do not target a specific team.
An ORG_ scope automatically grants the corresponding TEAM_ scope. For example, a token with ORG_PROFILE_READ can also access endpoints that require TEAM_PROFILE_READ.
ScopeDescriptionEndpoints
ORG_EVENT_TYPE_READView all event types across the organizationGET /v2/organizations/:orgId/teams/event-types
ORG_EVENT_TYPE_WRITECreate, edit, and delete event types across the organizationNo endpoints currently use this scope
ORG_BOOKING_READView all bookings across the organizationGET /v2/organizations/:orgId/bookings
ORG_BOOKING_WRITECreate, edit, and delete bookings across the organizationNo endpoints currently use this scope
ORG_SCHEDULE_READView schedules across the organizationGET /v2/organizations/:orgId/schedules,
GET /v2/organizations/:orgId/users/:userId/schedules,
GET /v2/organizations/:orgId/users/:userId/schedules/:scheduleId
ORG_SCHEDULE_WRITECreate, edit, and delete schedules across the organizationPOST /v2/organizations/:orgId/users/:userId/schedules,
PATCH /v2/organizations/:orgId/users/:userId/schedules/:scheduleId,
DELETE /v2/organizations/:orgId/users/:userId/schedules/:scheduleId
ORG_PROFILE_READView organization teamsGET /v2/organizations/:orgId/teams,
GET /v2/organizations/:orgId/teams/me
ORG_PROFILE_WRITECreate, edit, and delete organization teamsPOST /v2/organizations/:orgId/teams,
PATCH /v2/organizations/:orgId/teams/:teamId,
DELETE /v2/organizations/:orgId/teams/:teamId

2. Authorize

To initiate the OAuth flow, direct users to the following authorization URL: https://app.cal.com/auth/oauth2/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT_URI&state=YOUR_STATE&scope=BOOKING_READ%20BOOKING_WRITE URL Parameters:
ParameterRequiredDescription
client_idYesYour OAuth client ID
redirect_uriYesWhere users will be redirected after authorization. Must exactly match the registered redirect URI.
stateRecommendedA securely generated random string to mitigate CSRF attacks
scopeYesSpace or comma-separated list of scopes to request (e.g. BOOKING_READ BOOKING_WRITE or BOOKING_READ,BOOKING_WRITE). Must be a subset of scopes enabled on the OAuth client.
code_challengeFor public clientsPKCE code challenge (S256 method)
After users click Allow, they will be redirected to the redirect_uri with code (authorization code) and state as URL parameters:
https://your-app.com/callback?code=AUTHORIZATION_CODE&state=YOUR_STATE

Error Handling

Errors during the authorization step are displayed directly to the user on the Cal.com authorization page. Your application will not receive a JSON error response for these cases:
  • Client not found: No OAuth client exists with the provided client_id.
  • Client not approved: The OAuth client has not been approved by a Cal.com admin yet.
  • Mismatched redirect URI: The redirect_uri does not match the one registered for the OAuth client.
If an error occurs after the client is validated, the user is redirected to the redirect_uri with an error:
  • Scope required: If the scope parameter is missing, the error scope parameter is required for this OAuth client is displayed on the authorization page.
  • Unknown scope: If the scope parameter includes scope values that do not exist, the user is redirected with error=invalid_scope and error_description=Requested scope is not a recognized scope. This applies to both regular and legacy clients.
  • Invalid scope: If the scope parameter includes scopes not enabled on the OAuth client, the user is redirected with error=invalid_request and error_description=Requested scope exceeds the client's registered scopes.
  • Access denied: If the user denies access or has insufficient permissions, the user is redirected with an error.
https://your-app.com/callback?error=invalid_request&error_description=Requested+scope+exceeds+the+client%27s+registered+scopes&state=YOUR_STATE

3. Exchange Token

Exchange an authorization code for access and refresh tokens. The token endpoint also accepts application/x-www-form-urlencoded content type. Endpoint: POST https://api.cal.com/v2/auth/oauth2/token

3.1 Confidential Clients

Confidential clients authenticate with a client_secret. All parameters are required:
ParameterDescription
client_idYour OAuth client ID
client_secretYour OAuth client secret
grant_typeMust be authorization_code
codeThe authorization code received in the redirect URI
redirect_uriMust match the redirect URI used in the authorization request
curl -X POST https://api.cal.com/v2/auth/oauth2/token \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET",
    "grant_type": "authorization_code",
    "code": "AUTHORIZATION_CODE",
    "redirect_uri": "https://your-app.com/callback"
  }'

3.2 Public Clients (PKCE)

Public clients (e.g. single-page apps, mobile apps) use PKCE instead of a client_secret. You must have sent a code_challenge during the authorization step. All parameters are required:
ParameterDescription
client_idYour OAuth client ID
grant_typeMust be authorization_code
codeThe authorization code received in the redirect URI
redirect_uriMust match the redirect URI used in the authorization request
code_verifierThe original PKCE code verifier used to generate the code_challenge
curl -X POST https://api.cal.com/v2/auth/oauth2/token \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "YOUR_CLIENT_ID",
    "grant_type": "authorization_code",
    "code": "AUTHORIZATION_CODE",
    "redirect_uri": "https://your-app.com/callback",
    "code_verifier": "YOUR_CODE_VERIFIER"
  }'

Success Response (200)

{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "bearer",
  "expires_in": 1800,
  "scope": "BOOKING_READ BOOKING_WRITE"
}
Access tokens expire after 30 minutes (expires_in: 1800). Use the refresh token to obtain a new access token. The scope field contains the granted scopes as a space-separated string.

Error Responses

Error responses include error and error_description fields.
{
  "error": "invalid_grant",
  "error_description": "code_invalid_or_expired"
}
The authorization code has already been used, has expired, or is invalid. Request a new authorization code.
{
  "error": "invalid_client",
  "error_description": "invalid_client_credentials"
}
The client_secret does not match the client_id. Verify your credentials.
{
  "error": "invalid_client",
  "error_description": "client_not_found"
}
No OAuth client exists with the provided client_id.
{
  "error": "invalid_request",
  "error_description": "client_id is required"
}
The client_id field is missing from the request body.
{
  "error": "invalid_request",
  "error_description": "grant_type must be 'authorization_code' or 'refresh_token'"
}
The grant_type field must be either authorization_code or refresh_token.

4. Refresh Token

Refresh an expired access token using a refresh token. Endpoint: POST https://api.cal.com/v2/auth/oauth2/token

4.1 Confidential Clients

Confidential clients authenticate with a client_secret. All parameters are required:
ParameterDescription
client_idYour OAuth client ID
client_secretYour OAuth client secret
grant_typeMust be refresh_token
refresh_tokenThe refresh token received from a previous token response
curl -X POST https://api.cal.com/v2/auth/oauth2/token \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET",
    "grant_type": "refresh_token",
    "refresh_token": "YOUR_REFRESH_TOKEN"
  }'

4.2 Public Clients

Public clients do not use a client_secret. All parameters are required:
ParameterDescription
client_idYour OAuth client ID
grant_typeMust be refresh_token
refresh_tokenThe refresh token received from a previous token response
curl -X POST https://api.cal.com/v2/auth/oauth2/token \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "YOUR_CLIENT_ID",
    "grant_type": "refresh_token",
    "refresh_token": "YOUR_REFRESH_TOKEN"
  }'

Success Response (200)

{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "bearer",
  "expires_in": 1800,
  "scope": "BOOKING_READ BOOKING_WRITE"
}
Scopes are preserved from the original authorization. You do not need to re-request scopes when refreshing tokens.

Error Responses

{
  "error": "invalid_grant",
  "error_description": "invalid_refresh_token"
}
The refresh token is invalid, expired, or malformed. The user must re-authorize.
{
  "error": "invalid_client",
  "error_description": "invalid_client_credentials"
}
The client_secret does not match the client_id.
{
  "error": "invalid_client",
  "error_description": "client_not_found"
}
No OAuth client exists with the provided client_id.

Legacy Client Migration

If your OAuth client was created before scopes were introduced, it is considered a legacy client. A client is treated as legacy if it has no scopes configured, or if it only has the old legacy scope values (READ_BOOKING and/or READ_PROFILE). Access tokens issued by legacy clients can access any resource on behalf of the authorizing user — scopes are not enforced. You can migrate a legacy client to use explicit scopes without creating a new client. Order matters — follow these steps to avoid breaking existing integrations:

Step 1: Update your authorization URL

Add a scope query parameter to your authorization URL before changing any client settings. Legacy clients skip scope validation during authorization, so users can already authorize with a scope parameter even while the client is still in legacy mode.
https://app.cal.com/auth/oauth2/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT_URI&state=YOUR_STATE&scope=BOOKING_READ%20BOOKING_WRITE
New access tokens issued after this change will carry only the scopes you specified. For the full list of available scopes, see Available Scopes.

Step 2: Update client scopes in settings

Once your authorization URL is updated and you have verified that new tokens are being issued with the correct scopes, open your OAuth client settings and select the matching scopes. Save the client. After this step, the client is no longer treated as a legacy client. Scope validation is enforced for all new authorization requests.
Do not update the client scopes before updating your authorization URL. Doing so will immediately break the authorization flow for any user who visits the old URL without a scope parameter.

Re-approval

Changing properties below will trigger a re-review by Cal.com admins and set client to a “pending” state:
  • Name
  • Logo
  • Website URL
  • Redirect URI
  • Scope expansion (adding new scopes that the client did not previously have)
While pending, the client can only be used by the client owner for testing — other users cannot authorize with it. Changing properties below will NOT trigger a re-review and client will remain in the state it is:
  • Adding a _READ scope when the corresponding _WRITE scope is already granted (e.g. adding BOOKING_READ when BOOKING_WRITE is already present)
  • Removing scopes
  • Purpose description change
  • Updating scopes on a legacy client, as long as only user-level scopes are added — adding TEAM_ or ORG_ scopes to a legacy client will trigger re-approval. See Legacy Client Migration for details.

Existing tokens

Tokens issued before the migration continue to work until users re-authorize. There is no forced invalidation of existing tokens during the migration.

5. Verify Access Token

To verify the correct setup and functionality of OAuth credentials, use the following endpoint: Endpoint: GET https://api.cal.com/v2/me
curl -X GET https://api.cal.com/v2/me \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"