Users
Read and write user records — the human members of a Cirrus organization. Responses include the user's role and invitation state.
| Operation | Scope required |
|---|---|
Read (GET) | users:read |
| Invite / update | users:write |
Write endpoints require Idempotency-Key on every call. PATCH /users/{userId} also requires If-Match (an ETag from a prior GET) — optimistic concurrency on role changes and activation flips.
No /users/me in v1alpha1. Every token is org-scoped (per Authentication) — there is no "authenticated user" to resolve.
Roles
Every user carries exactly one role. Possible values:
| Value | Meaning |
|---|---|
User | Standard org member. |
Admin | Organization administrator. Can manage users, settings, and integrations. |
Partner Admin | Additional role available only on partner-admin organizations. Grants delegation over child orgs — see Partner Delegation. |
Endpoints on this page
| Method | Endpoint | Description |
|---|---|---|
GET | /api/v1alpha1/users | List users in the caller's org. Filter by role, activation, or updated-since. Details ↓ |
GET | /api/v1alpha1/users/{userId} | Fetch one user's full record. Details ↓ |
POST | /api/v1alpha1/users | Invite a user. Idempotency-Key required. Details ↓ |
PATCH | /api/v1alpha1/users/{userId} | Partial-merge update. Role, activation, profile fields. Details ↓ |
List users
GET /api/v1alpha1/users
Query parameters
| Parameter | Type | Description |
|---|---|---|
cursor | string | Standard keyset cursor. |
limit | number | Clamped 1–100, default 25. |
sort | string | updatedAt, createdAt, lastLoginAt, email, firstName, lastName. Prefix - for descending. Default -createdAt. |
search | string | (optional) Case-insensitive substring on email, firstName, lastName, displayName. |
role | string | (optional) One of User, Admin, Partner Admin. |
active | string | true (default) returns active users; false returns deactivated only; all returns both. |
since | string | (optional) ISO 8601. Users whose updatedAt >= since. Useful for delta sync. |
Response
See the Usermodel for the full field reference.
invitationStatusenum:accepted/pending/expired. A user created viaPOST /usersstartspending; transitions toacceptedwhen they set a password via the invitation link. Expires after 14 days.active: trueis the default filter because rendering deactivated users in a "team members" table is almost always a bug in partner UIs. Useactive=falsefor offboarding reports.
Errors
- Standard pagination errors (
invalid-cursor,invalid-sort-field) 400 invalid-filter—rolevalue is not one ofUser,Admin,Partner Admin
curl -s "https://altus.cirrusinsight.com/api/v1alpha1/users?role=Admin&limit=25" \
-H "X-Cirrus-Api-Key: $TOKEN"import requests
response = requests.get(
"https://altus.cirrusinsight.com/api/v1alpha1/users",
headers={"X-Cirrus-Api-Key": token},
params={"role": "Admin", "limit": 25},
)
response.raise_for_status()
page = response.json()using System.Net.Http;
using System.Net.Http.Headers;
using System.Web;
var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-Cirrus-Api-Key", token);
var query = HttpUtility.ParseQueryString(string.Empty);
query["role"] = "Admin";
query["limit"] = "25";
var uri = new UriBuilder("https://altus.cirrusinsight.com/api/v1alpha1/users")
{
Query = query.ToString()
}.Uri;
var response = await client.GetAsync(uri);
response.EnsureSuccessStatusCode();
var page = await response.Content.ReadAsStringAsync();{
"items": [
{
"id": "usr_...",
"orgId": "org_...",
"email": "alex@example.com",
"firstName": "Alex",
"lastName": "Rivera",
"displayName": "Alex Rivera",
"title": "Director of Sales Ops",
"department": "Revenue Operations",
"profileImageUrl": "https://cdn.example.com/avatars/usr_....png",
"role": "Admin",
"active": true,
"invitationStatus": "accepted",
"lastLoginAt": "2026-08-19T14:22:00Z",
"createdAt": "2026-05-01T12:00:00Z",
"updatedAt": "2026-07-14T09:30:00Z"
}
],
"nextCursor": null
}Get a user
GET /api/v1alpha1/users/{userId}
Response
Response headers:
| Header | Value | Notes |
|---|---|---|
ETag | W/"<version>" | Required on subsequent PATCH via If-Match. |
See the Usermodel for the full field reference.
emailis not maskable.users:readtokens see the raw email. Only issue this scope to admin-owned integrations.- Sign-in lockout state is not exposed via the API — it varies by identity-provider setup and is managed in the dashboard.
Errors
404 user-not-found— unknown id, or id belongs to another org (cross-org access never leaks)
curl -s "https://altus.cirrusinsight.com/api/v1alpha1/users/usr_01H8YKQ2N9DEMO" \
-H "X-Cirrus-Api-Key: $TOKEN"import requests
user_id = "usr_01H8YKQ2N9DEMO"
response = requests.get(
f"https://altus.cirrusinsight.com/api/v1alpha1/users/{user_id}",
headers={"X-Cirrus-Api-Key": token},
)
response.raise_for_status()
user = response.json()
etag = response.headers["ETag"]using System.Net.Http;
using System.Net.Http.Headers;
var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-Cirrus-Api-Key", token);
var userId = "usr_01H8YKQ2N9DEMO";
var response = await client.GetAsync(
$"https://altus.cirrusinsight.com/api/v1alpha1/users/{userId}");
response.EnsureSuccessStatusCode();
var user = await response.Content.ReadAsStringAsync();
var etag = response.Headers.ETag?.Tag;{
"id": "usr_...",
"orgId": "org_...",
"email": "alex@example.com",
"firstName": "Alex",
"lastName": "Rivera",
"displayName": "Alex Rivera",
"title": "Director of Sales Ops",
"department": "Revenue Operations",
"phoneNumber": "+1-415-555-0199",
"profileImageUrl": "https://cdn.example.com/avatars/usr_....png",
"role": "Admin",
"active": true,
"invitationStatus": "accepted",
"invitedBy": {
"userId": "usr_...",
"displayName": "Sam Chen"
},
"invitedAt": "2026-05-01T12:00:00Z",
"lastLoginAt": "2026-08-19T14:22:00Z",
"twoFactorEnabled": true,
"sourceRefs": {
"salesforce": {
"userId": "0051U00000abcXYZ"
}
},
"createdAt": "2026-05-01T12:00:00Z",
"updatedAt": "2026-07-14T09:30:00Z"
}Invite a user
POST /api/v1alpha1/users
Creates a pending user and dispatches an invitation email. Requires users:write scope + Idempotency-Key header.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
email | string | yes | Syntactically valid email. Case-normalised server-side. |
firstName | string | no | Overridable by the invitee at signup. |
lastName | string | no | |
title | string | no | |
department | string | no | |
role | string | yes | One of User, Admin, Partner Admin. Partner Admin only accepted on partner-admin organizations. |
Response (201)
Same shape as GET /users/{userId}, with:
invitationStatus: "pending"invitedByset to the caller's org contextinvitedAtset to nowactive: true— the user record exists and counts against the seat cap; they simply haven't accepted yetlastLoginAt: null
Response includes an ETag header for subsequent PATCH calls.
- Seat check is per-invite, not per-batch. Three concurrent invites when two seats remain returns two 201s and one 403 — explicit-per-invite is easier to handle than "invalid batch."
- Pending invites count against the seat cap. Prevents the "invite 200 people, hit
seat-limit-exceededat random" surprise. - Invite email dispatch is best-effort — 201 returns before SMTP handoff completes. If the email fails, the user record still exists (
invitationStatus: pending) and can be re-sent from the dashboard. Partners can poll forinvitationStatus: acceptedto confirm delivery + acceptance.
Errors
| Status | type | When |
|---|---|---|
| 400 | invalid-email | email not syntactically valid |
| 400 | invalid-role | role is not one of User, Admin, Partner Admin, or Partner Admin was requested on a non-partner-admin org |
| 403 | seat-limit-exceeded | Active users + pending invites would exceed the org's seat cap. Problem detail includes remainingSeats and seatLimit. |
| 409 | email-already-in-use | Active or pending user with same email already exists. Problem detail includes the existing usr_... id. |
| 422 | idempotency-key-conflict | Same Idempotency-Key, different request body |
curl -s -X POST "https://altus.cirrusinsight.com/api/v1alpha1/users" \
-H "X-Cirrus-Api-Key: $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"email": "alice@example.com",
"firstName": "Alice",
"lastName": "Nguyen",
"title": "Account Executive",
"department": "Sales",
"role": "User"
}'import requests
import uuid
body = {
"email": "alice@example.com",
"firstName": "Alice",
"lastName": "Nguyen",
"title": "Account Executive",
"department": "Sales",
"role": "User",
}
response = requests.post(
"https://altus.cirrusinsight.com/api/v1alpha1/users",
headers={
"X-Cirrus-Api-Key": token,
"Idempotency-Key": str(uuid.uuid4()),
},
json=body,
)
response.raise_for_status()
user = response.json()using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-Cirrus-Api-Key", token);
var body = new
{
email = "alice@example.com",
firstName = "Alice",
lastName = "Nguyen",
title = "Account Executive",
department = "Sales",
role = "User",
};
var request = new HttpRequestMessage(HttpMethod.Post,
"https://altus.cirrusinsight.com/api/v1alpha1/users")
{
Content = new StringContent(
JsonSerializer.Serialize(body),
Encoding.UTF8,
"application/json"),
};
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
var user = await response.Content.ReadAsStringAsync();Update a user
PATCH /api/v1alpha1/users/{userId}
Partial-merge update — role change, activation flip, profile-field corrections. Requires users:write scope, Idempotency-Key, and If-Match (from a prior GET's ETag).
Mutable fields
Any subset of these fields:
| Field | Type | Notes |
|---|---|---|
firstName, lastName, title, department, phoneNumber | string | null | Straight update. null clears the field. |
role | string | One of User, Admin, Partner Admin. |
active | boolean | false deactivates (soft — tokens invalidated, row preserved); true reactivates. Deactivating the org's Administrator returns 422 administrator-transfer-required. |
Response
The updated user record — same shape as GET /users/{userId}. Fresh ETag header.
If-Matchis required. Role-change races in HR-sync integrations are the primary motivator. GET first, PATCH with the returned ETag.- Email is not mutable via PATCH. Changing a user's email is a security-sensitive flow (invalidates sessions, needs a confirmation-email round trip). Stays in the dashboard.
- Deactivation is soft — the row is not deleted. Existing bookings hosted by the user are not cancelled; the internal booking flow continues to resolve the user by id. Only new logins and token issuance are blocked.
- Reactivation restores the user's role as it was at deactivation time. Doesn't re-fire the invitation email.
- Last-Administrator protection is server-side. A partner cannot bypass it by first assigning Admin to a placeholder user then demoting the incumbent — the check happens on every mutation, evaluating post-change state.
- Authorization is scope-based, not caller-role-based. A
users:writetoken can change any user's role. There is no "caller must be Administrator" runtime check because there is no caller user in v1alpha1 (tokens are org-scoped).
Errors
| Status | type | When |
|---|---|---|
| 400 | invalid-field | Unknown field in patch body |
| 400 | invalid-role | role value is not one of User, Admin, Partner Admin, or Partner Admin was requested on a non-partner-admin org |
| 404 | user-not-found | Cross-org or unknown id |
| 412 | precondition-failed | If-Match doesn't match current version — someone else updated the user in the meantime |
| 422 | last-administrator | Removing Admin from the org's last Admin |
| 422 | administrator-transfer-required | Deactivating the org's contractual owner without first transferring ownership |
| 422 | idempotency-key-conflict | Standard replay violation |
curl -s -X PATCH "https://altus.cirrusinsight.com/api/v1alpha1/users/usr_01H8YKQ2N9DEMO" \
-H "X-Cirrus-Api-Key: $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-H 'If-Match: W/"7"' \
-d '{
"role": "Admin",
"title": "VP of Sales"
}'import requests
import uuid
user_id = "usr_01H8YKQ2N9DEMO"
response = requests.patch(
f"https://altus.cirrusinsight.com/api/v1alpha1/users/{user_id}",
headers={
"X-Cirrus-Api-Key": token,
"Idempotency-Key": str(uuid.uuid4()),
"If-Match": etag,
},
json={"role": "Admin", "title": "VP of Sales"},
)
response.raise_for_status()
updated = response.json()using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-Cirrus-Api-Key", token);
var userId = "usr_01H8YKQ2N9DEMO";
var body = new { role = "Admin", title = "VP of Sales" };
var request = new HttpRequestMessage(HttpMethod.Patch,
$"https://altus.cirrusinsight.com/api/v1alpha1/users/{userId}")
{
Content = new StringContent(
JsonSerializer.Serialize(body),
Encoding.UTF8,
"application/json"),
};
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Headers.Add("If-Match", etag);
var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
var updated = await response.Content.ReadAsStringAsync();Related models
- User — the full field reference.
- Organization — users belong to exactly one org;
orgIdon every user matches the token's org context.