Skip to content
Preview. The endpoints on this page are illustrative and are likely to change as they move toward general availability. Documentation is published in advance so you can start shaping your integration; treat request and response details as subject to revision.

Users

Read and write user records — the human members of a Cirrus organization. Responses include the user's role and invitation state.

OperationScope required
Read (GET)users:read
Invite / updateusers: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:

ValueMeaning
UserStandard org member.
AdminOrganization administrator. Can manage users, settings, and integrations.
Partner AdminAdditional role available only on partner-admin organizations. Grants delegation over child orgs — see Partner Delegation.

Endpoints on this page

MethodEndpointDescription
GET/api/v1alpha1/usersList 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/usersInvite 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

ParameterTypeDescription
cursorstringStandard keyset cursor.
limitnumberClamped 1–100, default 25.
sortstringupdatedAt, createdAt, lastLoginAt, email, firstName, lastName. Prefix - for descending. Default -createdAt.
searchstring(optional) Case-insensitive substring on email, firstName, lastName, displayName.
rolestring(optional) One of User, Admin, Partner Admin.
activestringtrue (default) returns active users; false returns deactivated only; all returns both.
sincestring(optional) ISO 8601. Users whose updatedAt >= since. Useful for delta sync.

Response

See the Usermodel for the full field reference.

  • invitationStatus enum: accepted / pending / expired. A user created via POST /users starts pending; transitions to accepted when they set a password via the invitation link. Expires after 14 days.
  • active: true is the default filter because rendering deactivated users in a "team members" table is almost always a bug in partner UIs. Use active=false for offboarding reports.

Errors

  • Standard pagination errors (invalid-cursor, invalid-sort-field)
  • 400 invalid-filterrole value is not one of User, Admin, Partner Admin

bash
curl -s "https://altus.cirrusinsight.com/api/v1alpha1/users?role=Admin&limit=25" \
  -H "X-Cirrus-Api-Key: $TOKEN"
python
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()
csharp
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();
json
{
  "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:

HeaderValueNotes
ETagW/"<version>"Required on subsequent PATCH via If-Match.

See the Usermodel for the full field reference.

  • email is not maskable. users:read tokens 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)

bash
curl -s "https://altus.cirrusinsight.com/api/v1alpha1/users/usr_01H8YKQ2N9DEMO" \
  -H "X-Cirrus-Api-Key: $TOKEN"
python
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"]
csharp
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;
json
{
  "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

FieldTypeRequiredDescription
emailstringyesSyntactically valid email. Case-normalised server-side.
firstNamestringnoOverridable by the invitee at signup.
lastNamestringno
titlestringno
departmentstringno
rolestringyesOne of User, Admin, Partner Admin. Partner Admin only accepted on partner-admin organizations.

Response (201)

Same shape as GET /users/{userId}, with:

  • invitationStatus: "pending"
  • invitedBy set to the caller's org context
  • invitedAt set to now
  • active: true — the user record exists and counts against the seat cap; they simply haven't accepted yet
  • lastLoginAt: 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-exceeded at 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 for invitationStatus: accepted to confirm delivery + acceptance.

Errors

StatustypeWhen
400invalid-emailemail not syntactically valid
400invalid-rolerole is not one of User, Admin, Partner Admin, or Partner Admin was requested on a non-partner-admin org
403seat-limit-exceededActive users + pending invites would exceed the org's seat cap. Problem detail includes remainingSeats and seatLimit.
409email-already-in-useActive or pending user with same email already exists. Problem detail includes the existing usr_... id.
422idempotency-key-conflictSame Idempotency-Key, different request body

bash
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"
  }'
python
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()
csharp
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:

FieldTypeNotes
firstName, lastName, title, department, phoneNumberstring | nullStraight update. null clears the field.
rolestringOne of User, Admin, Partner Admin.
activebooleanfalse 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-Match is 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:write token 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

StatustypeWhen
400invalid-fieldUnknown field in patch body
400invalid-rolerole value is not one of User, Admin, Partner Admin, or Partner Admin was requested on a non-partner-admin org
404user-not-foundCross-org or unknown id
412precondition-failedIf-Match doesn't match current version — someone else updated the user in the meantime
422last-administratorRemoving Admin from the org's last Admin
422administrator-transfer-requiredDeactivating the org's contractual owner without first transferring ownership
422idempotency-key-conflictStandard replay violation
bash
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"
  }'
python
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()
csharp
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();
  • User — the full field reference.
  • Organization — users belong to exactly one org; orgId on every user matches the token's org context.

Raleigh, NC — a Cirruspath, Inc. company