curl --request POST \
--url https://api.cal.com/v2/organizations/{orgId}/users \
--header 'Content-Type: application/json' \
--data '
{
"email": "user@example.com",
"username": "user123",
"name": "Alice Smith",
"weekday": "Monday",
"brandColor": "#FFFFFF",
"bio": "I am a bio",
"metadata": {
"key": "value"
},
"darkBrandColor": "#000000",
"hideBranding": false,
"timeZone": "America/New_York",
"theme": "dark",
"appTheme": "light",
"timeFormat": 24,
"defaultScheduleId": 1,
"locale": "en",
"avatarUrl": "https://example.com/avatar.jpg",
"organizationRole": "MEMBER",
"autoAccept": true,
"skipNotificationEmail": false
}
'import requests
url = "https://api.cal.com/v2/organizations/{orgId}/users"
payload = {
"email": "user@example.com",
"username": "user123",
"name": "Alice Smith",
"weekday": "Monday",
"brandColor": "#FFFFFF",
"bio": "I am a bio",
"metadata": { "key": "value" },
"darkBrandColor": "#000000",
"hideBranding": False,
"timeZone": "America/New_York",
"theme": "dark",
"appTheme": "light",
"timeFormat": 24,
"defaultScheduleId": 1,
"locale": "en",
"avatarUrl": "https://example.com/avatar.jpg",
"organizationRole": "MEMBER",
"autoAccept": True,
"skipNotificationEmail": False
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
email: 'user@example.com',
username: 'user123',
name: 'Alice Smith',
weekday: 'Monday',
brandColor: '#FFFFFF',
bio: 'I am a bio',
metadata: {key: 'value'},
darkBrandColor: '#000000',
hideBranding: false,
timeZone: 'America/New_York',
theme: 'dark',
appTheme: 'light',
timeFormat: 24,
defaultScheduleId: 1,
locale: 'en',
avatarUrl: 'https://example.com/avatar.jpg',
organizationRole: 'MEMBER',
autoAccept: true,
skipNotificationEmail: false
})
};
fetch('https://api.cal.com/v2/organizations/{orgId}/users', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.cal.com/v2/organizations/{orgId}/users",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'email' => 'user@example.com',
'username' => 'user123',
'name' => 'Alice Smith',
'weekday' => 'Monday',
'brandColor' => '#FFFFFF',
'bio' => 'I am a bio',
'metadata' => [
'key' => 'value'
],
'darkBrandColor' => '#000000',
'hideBranding' => false,
'timeZone' => 'America/New_York',
'theme' => 'dark',
'appTheme' => 'light',
'timeFormat' => 24,
'defaultScheduleId' => 1,
'locale' => 'en',
'avatarUrl' => 'https://example.com/avatar.jpg',
'organizationRole' => 'MEMBER',
'autoAccept' => true,
'skipNotificationEmail' => false
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.cal.com/v2/organizations/{orgId}/users"
payload := strings.NewReader("{\n \"email\": \"user@example.com\",\n \"username\": \"user123\",\n \"name\": \"Alice Smith\",\n \"weekday\": \"Monday\",\n \"brandColor\": \"#FFFFFF\",\n \"bio\": \"I am a bio\",\n \"metadata\": {\n \"key\": \"value\"\n },\n \"darkBrandColor\": \"#000000\",\n \"hideBranding\": false,\n \"timeZone\": \"America/New_York\",\n \"theme\": \"dark\",\n \"appTheme\": \"light\",\n \"timeFormat\": 24,\n \"defaultScheduleId\": 1,\n \"locale\": \"en\",\n \"avatarUrl\": \"https://example.com/avatar.jpg\",\n \"organizationRole\": \"MEMBER\",\n \"autoAccept\": true,\n \"skipNotificationEmail\": false\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.cal.com/v2/organizations/{orgId}/users")
.header("Content-Type", "application/json")
.body("{\n \"email\": \"user@example.com\",\n \"username\": \"user123\",\n \"name\": \"Alice Smith\",\n \"weekday\": \"Monday\",\n \"brandColor\": \"#FFFFFF\",\n \"bio\": \"I am a bio\",\n \"metadata\": {\n \"key\": \"value\"\n },\n \"darkBrandColor\": \"#000000\",\n \"hideBranding\": false,\n \"timeZone\": \"America/New_York\",\n \"theme\": \"dark\",\n \"appTheme\": \"light\",\n \"timeFormat\": 24,\n \"defaultScheduleId\": 1,\n \"locale\": \"en\",\n \"avatarUrl\": \"https://example.com/avatar.jpg\",\n \"organizationRole\": \"MEMBER\",\n \"autoAccept\": true,\n \"skipNotificationEmail\": false\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.cal.com/v2/organizations/{orgId}/users")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"email\": \"user@example.com\",\n \"username\": \"user123\",\n \"name\": \"Alice Smith\",\n \"weekday\": \"Monday\",\n \"brandColor\": \"#FFFFFF\",\n \"bio\": \"I am a bio\",\n \"metadata\": {\n \"key\": \"value\"\n },\n \"darkBrandColor\": \"#000000\",\n \"hideBranding\": false,\n \"timeZone\": \"America/New_York\",\n \"theme\": \"dark\",\n \"appTheme\": \"light\",\n \"timeFormat\": 24,\n \"defaultScheduleId\": 1,\n \"locale\": \"en\",\n \"avatarUrl\": \"https://example.com/avatar.jpg\",\n \"organizationRole\": \"MEMBER\",\n \"autoAccept\": true,\n \"skipNotificationEmail\": false\n}"
response = http.request(request)
puts response.read_body{
"status": "success",
"data": {
"id": 1,
"uuid": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"email": "john@example.com",
"timeZone": "America/New_York",
"weekStart": "Monday",
"hideBranding": false,
"createdDate": "2022-01-01T00:00:00Z",
"profile": {
"id": 1,
"organizationId": 1,
"userId": 1,
"username": "john_doe"
},
"membership": {
"id": 123,
"accepted": false
},
"username": "john_doe",
"name": "John Doe",
"emailVerified": "2022-01-01T00:00:00Z",
"bio": "I am a software developer",
"avatarUrl": "https://example.com/avatar.jpg",
"appTheme": "light",
"theme": "default",
"defaultScheduleId": 1,
"locale": "en-US",
"timeFormat": 12,
"brandColor": "#ffffff",
"darkBrandColor": "#000000",
"allowDynamicBooking": true,
"verified": true,
"invitedTo": 1,
"metadata": {
"key": "value"
}
}
}Create a user
Required membership role: org admin. PBAC permission: organization.invite. Learn more about API access control at https://cal.com/docs/api-reference/v2/access-control. If accessed using an OAuth access token, the ORG_MEMBERSHIP_WRITE scope is required.
This endpoint creates a brand-new Cal.com user account and attaches them to the organization. If the email already belongs to an existing Cal.com user, the request fails with 409 Conflict. When the user is already a member of this organization the message is A user already exists with that email and is already a member of this organization. When the user exists but is not a member, the message is A user already exists with this email. Use POST /v2/organizations/{orgId}/memberships to add them to this organization.. A common case is re-adding someone who was previously removed from the organization via the dashboard: the membership is gone, but the underlying Cal.com user account still exists.
To attach an existing Cal.com user to the organization, use POST /v2/organizations/{orgId}/memberships with the email field instead.
Membership is accepted automatically only when autoAccept is true, the organization is verified, and the email’s domain is its auto-accept domain or one of its verified secondary domains. An email outside those domains creates a pending membership with no organization profile and always sends an invite email, even when skipNotificationEmail is true. Until the user accepts the invite, these outside-domain users are not returned by GET /v2/organizations/{orgId}/users, and /v2/organizations/{orgId}/users/{userId}/... routes return 403.
A matching-domain user keeps an organization profile even when their membership is pending because autoAccept is false or the organization is unverified. These users remain listed and accessible through organization-user routes. An unverified organization still triggers an invite, even when skipNotificationEmail is true.
Platform organizations follow the same rule. With no organization settings, or an auto-accept domain of UNUSED_FOR_PLATFORM and no matching verified secondary domain, users get a pending membership, no organization profile, and a Cal.com invite even when skipNotificationEmail is true. Platform-managed user creation through POST /v2/oauth-clients/{clientId}/users is a separate flow and is unchanged.
The response includes membership.id and membership.accepted. When accepted is false, the membership is pending until the user accepts the invite; poll GET /v2/organizations/{orgId}/memberships/{membershipId} with membership.id until accepted is true.
curl --request POST \
--url https://api.cal.com/v2/organizations/{orgId}/users \
--header 'Content-Type: application/json' \
--data '
{
"email": "user@example.com",
"username": "user123",
"name": "Alice Smith",
"weekday": "Monday",
"brandColor": "#FFFFFF",
"bio": "I am a bio",
"metadata": {
"key": "value"
},
"darkBrandColor": "#000000",
"hideBranding": false,
"timeZone": "America/New_York",
"theme": "dark",
"appTheme": "light",
"timeFormat": 24,
"defaultScheduleId": 1,
"locale": "en",
"avatarUrl": "https://example.com/avatar.jpg",
"organizationRole": "MEMBER",
"autoAccept": true,
"skipNotificationEmail": false
}
'import requests
url = "https://api.cal.com/v2/organizations/{orgId}/users"
payload = {
"email": "user@example.com",
"username": "user123",
"name": "Alice Smith",
"weekday": "Monday",
"brandColor": "#FFFFFF",
"bio": "I am a bio",
"metadata": { "key": "value" },
"darkBrandColor": "#000000",
"hideBranding": False,
"timeZone": "America/New_York",
"theme": "dark",
"appTheme": "light",
"timeFormat": 24,
"defaultScheduleId": 1,
"locale": "en",
"avatarUrl": "https://example.com/avatar.jpg",
"organizationRole": "MEMBER",
"autoAccept": True,
"skipNotificationEmail": False
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
email: 'user@example.com',
username: 'user123',
name: 'Alice Smith',
weekday: 'Monday',
brandColor: '#FFFFFF',
bio: 'I am a bio',
metadata: {key: 'value'},
darkBrandColor: '#000000',
hideBranding: false,
timeZone: 'America/New_York',
theme: 'dark',
appTheme: 'light',
timeFormat: 24,
defaultScheduleId: 1,
locale: 'en',
avatarUrl: 'https://example.com/avatar.jpg',
organizationRole: 'MEMBER',
autoAccept: true,
skipNotificationEmail: false
})
};
fetch('https://api.cal.com/v2/organizations/{orgId}/users', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.cal.com/v2/organizations/{orgId}/users",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'email' => 'user@example.com',
'username' => 'user123',
'name' => 'Alice Smith',
'weekday' => 'Monday',
'brandColor' => '#FFFFFF',
'bio' => 'I am a bio',
'metadata' => [
'key' => 'value'
],
'darkBrandColor' => '#000000',
'hideBranding' => false,
'timeZone' => 'America/New_York',
'theme' => 'dark',
'appTheme' => 'light',
'timeFormat' => 24,
'defaultScheduleId' => 1,
'locale' => 'en',
'avatarUrl' => 'https://example.com/avatar.jpg',
'organizationRole' => 'MEMBER',
'autoAccept' => true,
'skipNotificationEmail' => false
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.cal.com/v2/organizations/{orgId}/users"
payload := strings.NewReader("{\n \"email\": \"user@example.com\",\n \"username\": \"user123\",\n \"name\": \"Alice Smith\",\n \"weekday\": \"Monday\",\n \"brandColor\": \"#FFFFFF\",\n \"bio\": \"I am a bio\",\n \"metadata\": {\n \"key\": \"value\"\n },\n \"darkBrandColor\": \"#000000\",\n \"hideBranding\": false,\n \"timeZone\": \"America/New_York\",\n \"theme\": \"dark\",\n \"appTheme\": \"light\",\n \"timeFormat\": 24,\n \"defaultScheduleId\": 1,\n \"locale\": \"en\",\n \"avatarUrl\": \"https://example.com/avatar.jpg\",\n \"organizationRole\": \"MEMBER\",\n \"autoAccept\": true,\n \"skipNotificationEmail\": false\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.cal.com/v2/organizations/{orgId}/users")
.header("Content-Type", "application/json")
.body("{\n \"email\": \"user@example.com\",\n \"username\": \"user123\",\n \"name\": \"Alice Smith\",\n \"weekday\": \"Monday\",\n \"brandColor\": \"#FFFFFF\",\n \"bio\": \"I am a bio\",\n \"metadata\": {\n \"key\": \"value\"\n },\n \"darkBrandColor\": \"#000000\",\n \"hideBranding\": false,\n \"timeZone\": \"America/New_York\",\n \"theme\": \"dark\",\n \"appTheme\": \"light\",\n \"timeFormat\": 24,\n \"defaultScheduleId\": 1,\n \"locale\": \"en\",\n \"avatarUrl\": \"https://example.com/avatar.jpg\",\n \"organizationRole\": \"MEMBER\",\n \"autoAccept\": true,\n \"skipNotificationEmail\": false\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.cal.com/v2/organizations/{orgId}/users")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"email\": \"user@example.com\",\n \"username\": \"user123\",\n \"name\": \"Alice Smith\",\n \"weekday\": \"Monday\",\n \"brandColor\": \"#FFFFFF\",\n \"bio\": \"I am a bio\",\n \"metadata\": {\n \"key\": \"value\"\n },\n \"darkBrandColor\": \"#000000\",\n \"hideBranding\": false,\n \"timeZone\": \"America/New_York\",\n \"theme\": \"dark\",\n \"appTheme\": \"light\",\n \"timeFormat\": 24,\n \"defaultScheduleId\": 1,\n \"locale\": \"en\",\n \"avatarUrl\": \"https://example.com/avatar.jpg\",\n \"organizationRole\": \"MEMBER\",\n \"autoAccept\": true,\n \"skipNotificationEmail\": false\n}"
response = http.request(request)
puts response.read_body{
"status": "success",
"data": {
"id": 1,
"uuid": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"email": "john@example.com",
"timeZone": "America/New_York",
"weekStart": "Monday",
"hideBranding": false,
"createdDate": "2022-01-01T00:00:00Z",
"profile": {
"id": 1,
"organizationId": 1,
"userId": 1,
"username": "john_doe"
},
"membership": {
"id": 123,
"accepted": false
},
"username": "john_doe",
"name": "John Doe",
"emailVerified": "2022-01-01T00:00:00Z",
"bio": "I am a software developer",
"avatarUrl": "https://example.com/avatar.jpg",
"appTheme": "light",
"theme": "default",
"defaultScheduleId": 1,
"locale": "en-US",
"timeFormat": 12,
"brandColor": "#ffffff",
"darkBrandColor": "#000000",
"allowDynamicBooking": true,
"verified": true,
"invitedTo": 1,
"metadata": {
"key": "value"
}
}
}Headers
Body
If true, the organization membership is accepted only when the email's domain is the organization's auto-accept domain or one of its verified secondary domains, in a verified organization; any other email gets a pending membership and an invite email. If false, the membership is always pending and the user cannot access the organization until they accept.
Was this page helpful?