Partner Delegation
A partner-admin organization — a Cirrus reseller, systems integrator, MSP, or enterprise HQ with subsidiary orgs — can hold a single Platform API token that acts on behalf of any of its child organizations. One token, N children, per-request selection via an HTTP header.
When to use delegation
Use delegation if you're building an integration that:
- Manages many customer orgs from a single admin surface (systems integrators, MSPs)
- Runs cross-subsidiary reporting where the same query fans out across children of a corporate HQ
- Provisions Cirrus configuration for downstream customers as part of a broader onboarding flow (resellers)
Do not use delegation if:
- You're building a single-tenant integration for one customer — the standard org-scoped token in Authentication is what you want.
- You need to act on peer orgs that don't have a parent-child relationship with yours — delegation requires an existing partnership linkage that Cirrus has provisioned.
The X-Cirrus-Acting-On header
Any request from a partner-admin token can carry:
When the header is present and authorised, the org-scoping middleware treats the request as if it originated from the child. Every existing endpoint keeps working with no other changes — the same URL, same request body, same response shape.
Successful delegated responses echo the resolved child back in a response header so your client can verify its intent was honoured:
Requests without the header behave exactly as before — the token acts on its own issuing org. Fully backward-compatible.
X-Cirrus-Acting-On: org_<childId>Cirrus-Acting-On: org_<childId>Requirements
To use delegation, your token needs:
- The
partner:act-as-childscope. Only issuable to tokens minted from a partner-admin organization. If your org isn't a partner-admin org, this scope option won't appear in the developer dashboard — contact us to discuss enabling the partnership relationship. - The scope(s) for the action you're taking on the child. For example, to invite a user into a child org, your token needs both
partner:act-as-childandusers:write. Delegation gives you the authority to act on the child; the resource scopes gate what you're allowed to do.
The effective scope on a child is the intersection of:
- Your token's scopes
- The scopes the child has granted to your parent org
If your token holds users:write but the child has granted only users:read to your parentship, a write attempt against the child returns 403 delegation-scope-insufficient — the response body names the exact granted subset.
Endpoints on this page
| Method | Endpoint | Description |
|---|---|---|
GET | /api/v1alpha1/partner/managed-organizations | List the child orgs your token can act on, with per-child scope grants. Details ↓ |
Every other endpoint on the Platform API honours the X-Cirrus-Acting-On header — see Endpoint delegation contract below.
List managed organizations
GET /api/v1alpha1/partner/managed-organizations
Returns the child organizations your token can act on. Requires partner:act-as-child. Do not send X-Cirrus-Acting-On on this endpoint — it reads about your children, not on one of them.
Query parameters
| Parameter | Type | Description |
|---|---|---|
cursor | string | Standard keyset cursor. |
limit | number | Clamped 1–100, default 25. |
sort | string | updatedAt, createdAt, name, seats.total. Prefix - for descending. Default -updatedAt. |
search | string | (optional) Case-insensitive substring on child org name. |
industry | string | (optional) Filter to a specific industry. |
licenseTier | string | (optional) Filter to a tier slug (starter, professional, enterprise). |
hasActiveIssues | boolean | (optional) Filter to children with an active support flag (e.g., expired trial, payment failure). |
Response
grantedScopes[]— the intersection of your token's scopes and the child's grants to your parent org. This is exactly what your token can exercise against this child right now; it's the source of truth for UI capability gates.hasActiveIssues— coarse partner-facing health flag.truewhen the child has an expired trial, a failed payment, an active security incident, or another condition Cirrus wants partners to surface in their admin UIs.relationshipCreatedAt— when the parent-child linkage was established in Cirrus.
Errors
- Standard pagination errors (
invalid-cursor,invalid-sort-field) 403 insufficient-scope— token doesn't holdpartner:act-as-child
curl -s "https://altus.cirrusinsight.com/api/v1alpha1/partner/managed-organizations?limit=25" \
-H "X-Cirrus-Api-Key: $TOKEN"import requests
response = requests.get(
"https://altus.cirrusinsight.com/api/v1alpha1/partner/managed-organizations",
headers={"X-Cirrus-Api-Key": token},
params={"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["limit"] = "25";
var uri = new UriBuilder(
"https://altus.cirrusinsight.com/api/v1alpha1/partner/managed-organizations")
{
Query = query.ToString()
}.Uri;
var response = await client.GetAsync(uri);
response.EnsureSuccessStatusCode();
var page = await response.Content.ReadAsStringAsync();{
"items": [
{
"id": "org_...",
"name": "Acme Retail",
"primaryEmailDomain": "retail.acme.com",
"industry": "Retail",
"licenseTier": { "slug": "professional", "name": "Professional" },
"seats": { "total": 25, "used": 18, "available": 7 },
"grantedScopes": [
"users:read",
"users:write",
"deals:read",
"qna:read"
],
"hasActiveIssues": false,
"relationshipCreatedAt": "2026-05-01T00:00:00Z",
"updatedAt": "2026-07-14T09:30:00Z"
}
],
"nextCursor": null
}Endpoint delegation contract
Every endpoint on /api/v1alpha1/ honours X-Cirrus-Acting-On. The contract is uniform:
On success:
- The endpoint operates as if the request originated from the child. All returned ids belong to the child; all writes affect the child's records.
- The response includes a
Cirrus-Acting-On: org_<childId>header echoing the resolved child. - Rate limits deduct from the child's per-org quota (with a per-partner sub-bucket so your fan-out doesn't starve the child's other callers).
- Audit-log entries carry both your parent-org id (the authority) and the child-org id (the subject).
On failure:
| Status | Error type | When |
|---|---|---|
| 400 | invalid-acting-on-org | Header value isn't a valid org_... id (malformed, empty, or wrong prefix) |
| 403 | delegation-scope-required | Token doesn't hold partner:act-as-child |
| 403 | cross-org-delegation-forbidden | Target org isn't a child of your token's org (existence is never leaked — same response whether the child doesn't exist, isn't yours, or the relationship has been revoked) |
| 403 | delegation-scope-insufficient | Your token holds the required scope, but the child hasn't granted it to your parent. Problem detail includes the granted subset. |
Idempotency keys are scoped per (token, child) — the same Idempotency-Key sent against two different children creates two independent operations, not a single deduped one.
Worked example
The end-to-end reseller flow — enumerate children, then act on one:
# 1. Enumerate the child orgs this token can act on
curl -s "https://altus.cirrusinsight.com/api/v1alpha1/partner/managed-organizations" \
-H "X-Cirrus-Api-Key: $TOKEN"
# 2. Read the target child's entitlements to preflight the invite
curl -s "https://altus.cirrusinsight.com/api/v1alpha1/organizations/me/entitlements" \
-H "X-Cirrus-Api-Key: $TOKEN" \
-H "X-Cirrus-Acting-On: org_01H8YKQ2N9RETAIL"
# 3. Invite a user into the child
curl -s -X POST "https://altus.cirrusinsight.com/api/v1alpha1/users" \
-H "X-Cirrus-Api-Key: $TOKEN" \
-H "X-Cirrus-Acting-On: org_01H8YKQ2N9RETAIL" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"email": "casey@retail.acme.com",
"firstName": "Casey",
"role": "User"
}'import requests
import uuid
parent_token = "ci_live_..."
target_child = "org_01H8YKQ2N9RETAIL"
def child_headers(child_id: str, extra: dict | None = None) -> dict:
headers = {
"X-Cirrus-Api-Key": parent_token,
"X-Cirrus-Acting-On": child_id,
}
if extra:
headers.update(extra)
return headers
# 1. Enumerate children
children = requests.get(
"https://altus.cirrusinsight.com/api/v1alpha1/partner/managed-organizations",
headers={"X-Cirrus-Api-Key": parent_token},
).json()
# 2. Preflight the child's entitlements
entitlements = requests.get(
"https://altus.cirrusinsight.com/api/v1alpha1/organizations/me/entitlements",
headers=child_headers(target_child),
).json()
# 3. Invite
invite = requests.post(
"https://altus.cirrusinsight.com/api/v1alpha1/users",
headers=child_headers(target_child, {"Idempotency-Key": str(uuid.uuid4())}),
json={
"email": "casey@retail.acme.com",
"firstName": "Casey",
"role": "User",
},
)
invite.raise_for_status()using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
var parentToken = "ci_live_...";
var targetChild = "org_01H8YKQ2N9RETAIL";
var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-Cirrus-Api-Key", parentToken);
// 1. Enumerate children
var childrenResponse = await client.GetAsync(
"https://altus.cirrusinsight.com/api/v1alpha1/partner/managed-organizations");
childrenResponse.EnsureSuccessStatusCode();
// 2. Preflight entitlements for the target child
var entitlementsRequest = new HttpRequestMessage(HttpMethod.Get,
"https://altus.cirrusinsight.com/api/v1alpha1/organizations/me/entitlements");
entitlementsRequest.Headers.Add("X-Cirrus-Acting-On", targetChild);
var entitlementsResponse = await client.SendAsync(entitlementsRequest);
entitlementsResponse.EnsureSuccessStatusCode();
// 3. Invite a user into the child
var inviteBody = new
{
email = "casey@retail.acme.com",
firstName = "Casey",
role = "User",
};
var inviteRequest = new HttpRequestMessage(HttpMethod.Post,
"https://altus.cirrusinsight.com/api/v1alpha1/users")
{
Content = new StringContent(
JsonSerializer.Serialize(inviteBody),
Encoding.UTF8,
"application/json"),
};
inviteRequest.Headers.Add("X-Cirrus-Acting-On", targetChild);
inviteRequest.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
var inviteResponse = await client.SendAsync(inviteRequest);
inviteResponse.EnsureSuccessStatusCode();Rate limits
Rate limits protect the subject system, not the actor. When you send X-Cirrus-Acting-On: org_<childId>, that request deducts from the child's per-org quota — not your parent org's.
To prevent your fan-out from starving a child's other callers, each (parent, child) pair gets its own sub-bucket within the child's overall bucket. In practice: a burst of 100 delegated calls to child A consumes A's partner-delegated sub-bucket, leaving A's direct callers untouched. But once that sub-bucket is exhausted, your calls receive 429 with a Retry-After header even though A's overall bucket may have room.
Your parent org has its own quota for requests that don't carry the header (GET /partner/managed-organizations, GET /organizations/me, etc.).
Audit trail
Every write executed via delegation records dual attribution in the audit log:
parentOrgId— your token's issuing org (the authority)orgId— the child (the subject)tokenId— which of your tokens was used
Child-org admins see "delegated activity by Partner X" as a distinct filter in their own audit surface. This lets a customer whose data is managed by a partner see exactly what was done on their behalf, without having to trust the partner's own reporting.
Limitations
- One-level hierarchy only.
X-Cirrus-Acting-Ontargets a direct child of your token's issuing org. Grandparent chains don't exist in Cirrus's org model. - No cross-parent delegation. You can only act on children of your org; you can't act on another parent's children even if they've granted you scopes there. This is a security-boundary property, not a technical limit.
- No user impersonation. The header switches org context, not user context. It doesn't pretend the request came from a specific user of the child. When per-user OAuth ships, delegation and impersonation will need to be reconciled.
- No ad-hoc peer sharing. Delegation requires an existing partnership relationship that Cirrus has provisioned. Two unrelated orgs can't grant each other delegation on their own.
Related
- Authentication — token issuance and the full scope catalog
- Errors — the four delegation-specific error types
- Organizations endpoints — every read here honours
X-Cirrus-Acting-On - Organization model