Back to Cal

OAuth

docs/api-reference/v2/oauth.mdx

6.2.013.2 KB
Original Source

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.

<Frame> ![](/images/oauth-zapier.png) </Frame>

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.

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.

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

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
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 (e.g., the user denies access or has insufficient permissions), the user is redirected to the redirect_uri with an error:

https://your-app.com/callback?error=access_denied&error_description=team_not_found_or_no_access&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
<Tabs> <Tab title="cURL"> ```bash 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" }' ``` </Tab> <Tab title="fetch"> ```typescript const response = await fetch("https://api.cal.com/v2/auth/oauth2/token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ client_id: "YOUR_CLIENT_ID", client_secret: "YOUR_CLIENT_SECRET", grant_type: "authorization_code", code: "AUTHORIZATION_CODE", redirect_uri: "https://your-app.com/callback", }), });
const tokens = await response.json();
```
</Tab> <Tab title="axios"> ```typescript import axios from "axios";
const { data } = await axios.post(
  "https://api.cal.com/v2/auth/oauth2/token",
  {
    client_id: "YOUR_CLIENT_ID",
    client_secret: "YOUR_CLIENT_SECRET",
    grant_type: "authorization_code",
    code: "AUTHORIZATION_CODE",
    redirect_uri: "https://your-app.com/callback",
  }
);
```
</Tab> </Tabs>

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
<Tabs> <Tab title="cURL"> ```bash 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" }' ``` </Tab> <Tab title="fetch"> ```typescript const response = await fetch("https://api.cal.com/v2/auth/oauth2/token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ client_id: "YOUR_CLIENT_ID", grant_type: "authorization_code", code: "AUTHORIZATION_CODE", redirect_uri: "https://your-app.com/callback", code_verifier: "YOUR_CODE_VERIFIER", }), });
const tokens = await response.json();
```
</Tab> <Tab title="axios"> ```typescript import axios from "axios";
const { data } = await axios.post(
  "https://api.cal.com/v2/auth/oauth2/token",
  {
    client_id: "YOUR_CLIENT_ID",
    grant_type: "authorization_code",
    code: "AUTHORIZATION_CODE",
    redirect_uri: "https://your-app.com/callback",
    code_verifier: "YOUR_CODE_VERIFIER",
  }
);
```
</Tab> </Tabs>

Success Response (200)

json
{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "bearer",
  "expires_in": 1800
}
<Note> Access tokens expire after 30 minutes (`expires_in: 1800`). Use the refresh token to obtain a new access token. </Note>

Error Responses

Error responses include error and error_description fields.

<AccordionGroup> <Accordion title="Invalid or expired authorization code (400)"> ```json { "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. </Accordion> <Accordion title="Invalid client credentials (401)"> ```json { "error": "invalid_client", "error_description": "invalid_client_credentials" } ``` The `client_secret` does not match the `client_id`. Verify your credentials. </Accordion> <Accordion title="Client not found (401)"> ```json { "error": "invalid_client", "error_description": "client_not_found" } ``` No OAuth client exists with the provided `client_id`. </Accordion> <Accordion title="Missing client_id (400)"> ```json { "error": "invalid_request", "error_description": "client_id is required" } ``` The `client_id` field is missing from the request body. </Accordion> <Accordion title="Invalid grant_type (400)"> ```json { "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`. </Accordion> </AccordionGroup>

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
<Tabs> <Tab title="cURL"> ```bash 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" }' ``` </Tab> <Tab title="fetch"> ```typescript const response = await fetch("https://api.cal.com/v2/auth/oauth2/token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ client_id: "YOUR_CLIENT_ID", client_secret: "YOUR_CLIENT_SECRET", grant_type: "refresh_token", refresh_token: "YOUR_REFRESH_TOKEN", }), });
const tokens = await response.json();
```
</Tab> <Tab title="axios"> ```typescript import axios from "axios";
const { data } = await axios.post(
  "https://api.cal.com/v2/auth/oauth2/token",
  {
    client_id: "YOUR_CLIENT_ID",
    client_secret: "YOUR_CLIENT_SECRET",
    grant_type: "refresh_token",
    refresh_token: "YOUR_REFRESH_TOKEN",
  }
);
```
</Tab> </Tabs>

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
<Tabs> <Tab title="cURL"> ```bash 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" }' ``` </Tab> <Tab title="fetch"> ```typescript const response = await fetch("https://api.cal.com/v2/auth/oauth2/token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ client_id: "YOUR_CLIENT_ID", grant_type: "refresh_token", refresh_token: "YOUR_REFRESH_TOKEN", }), });
const tokens = await response.json();
```
</Tab> <Tab title="axios"> ```typescript import axios from "axios";
const { data } = await axios.post(
  "https://api.cal.com/v2/auth/oauth2/token",
  {
    client_id: "YOUR_CLIENT_ID",
    grant_type: "refresh_token",
    refresh_token: "YOUR_REFRESH_TOKEN",
  }
);
```
</Tab> </Tabs>

Success Response (200)

json
{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "bearer",
  "expires_in": 1800
}

Error Responses

<AccordionGroup> <Accordion title="Invalid refresh token (400)"> ```json { "error": "invalid_grant", "error_description": "invalid_refresh_token" } ``` The refresh token is invalid, expired, or malformed. The user must re-authorize. </Accordion> <Accordion title="Invalid client credentials (401)"> ```json { "error": "invalid_client", "error_description": "invalid_client_credentials" } ``` The `client_secret` does not match the `client_id`. </Accordion> <Accordion title="Client not found (401)"> ```json { "error": "invalid_client", "error_description": "client_not_found" } ``` No OAuth client exists with the provided `client_id`. </Accordion> </AccordionGroup>

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

<Tabs> <Tab title="cURL"> ```bash curl -X GET https://api.cal.com/v2/me \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` </Tab> <Tab title="fetch"> ```typescript const response = await fetch("https://api.cal.com/v2/me", { headers: { Authorization: "Bearer YOUR_ACCESS_TOKEN" }, });
const user = await response.json();
```
</Tab> <Tab title="axios"> ```typescript import axios from "axios";
const { data } = await axios.get("https://api.cal.com/v2/me", {
  headers: { Authorization: "Bearer YOUR_ACCESS_TOKEN" },
});
```
</Tab> </Tabs>