Organization and roles
Portal actions and API calls are authorized by the same role rules. Roles are per organization; a person can hold several.
Roles
| Role | Purpose |
|---|---|
owner | Membership, recovery, support access and audit export rights. Must be explicitly assigned billing_admin to transact. |
trust_admin | Trust spaces, issuers and space policy; can do anything a service owner can. |
service_owner | Controls grants, invitations and gateways for services. |
partner_admin | Accepts the organization’s invitations, approves workload issuers and enrollments, manages its workloads. |
billing_admin | Plan, checkout, invoices and the Stripe portal. No trust configuration. |
auditor | Read-only inspection of configuration and evidence; export only with a separate grant. |
Role matrix
| Action | owner | trust_admin | service_owner | partner_admin | billing_admin | auditor |
|---|---|---|---|---|---|---|
Read the organization, members, workload issuers, spaces and servicesorg.read | Yes | Yes | Yes | Yes | Yes | Yes |
Assign member rolesorg.members.manage | Yes | No | No | No | No | No |
Create trust spacesspace.create | Yes | Yes | No | No | No | No |
Manage issuers and space policyissuer.manage | No | Yes | No | No | No | No |
Create servicesservice.create | Yes | Yes | Yes | No | No | No |
Manage servicesservice.manage | No | Yes | Yes | No | No | No |
Pair and disable gatewaysgateway.pair | No | Yes | Yes | No | No | No |
Create and cancel invitationsinvitation.create | No | Yes | Yes | No | No | No |
Accept invitations for the organizationinvitation.respond | Yes | No | No | Yes | No | No |
Suspend, resume, withdraw, change scope; revoke sponsor-side leavesgrant.manage | No | Yes | Yes | No | No | No |
Read grants, invitations and receiptsgrant.read | Yes | Yes | Yes | Yes | No | Yes |
Approve workload issuers; revoke the organization’s own leavesworkload.manage | No | No | No | Yes | No | No |
Approve sandbox enrollmentsenrollment.approve | No | No | No | Yes | No | No |
Download certificate chainscertificate.read | Yes | Yes | Yes | Yes | No | Yes |
Checkout and billing portalbilling.manage | No | No | No | No | Yes | No |
Read billing summary and invoicesbilling.read | Yes | No | No | No | Yes | Yes |
Read the audit logaudit.read | Yes | Yes | No | No | No | Yes |
Change approval policies on spaces, services and integrationsapproval_policy.manage | Yes | Yes | No | No | No | No |
Export the audit log (needs a separate export grant)audit.export | No | No | No | No | No | No |
Two rules sit outside the matrix: support access grants, export grants and role assignment require owner; and a caller with no role in an organization gets 404 (not 403) for its resources, so inaccessible tenants are concealed.
Bootstrap and membership
- Creating an organization (
POST /v1/organizations) makes you itsowner,trust_admin,service_ownerandpartner_admin. It does not make youbilling_admin. - Claiming a new partner organization through an invitation makes you
ownerandpartner_admin. - Owners add roles with
POST /v1/organizations/{id}/roles{"user_id":"usr_…","role":"auditor"}. Assignment is additive and idempotent; the target must already be a member. GET /v1/organizations/{id}/membersreturns each member’suser_id,display_name,emailandroles.
Remove roles and members
Owners manage membership under Organization > Members and roles, or through the API:
DELETE /v1/organizations/{id}/members/{user_id}/roles/{role}removes one role. API credentials the member created lose that role immediately, because a credential’s effective roles are re-checked against its creator’s current roles on every request. Removing a member’s last role ends their membership.DELETE /v1/organizations/{id}/members/{user_id}removes every role at once.- When a membership ends, every API credential the member created in the organization is revoked and their audit export rights end, in the same batch. The response reports
credentials_revoked. - An organization always keeps an owner: removing the last owner’s
ownerrole, or removing the last owner (including yourself), returns409 last_owner. Make another member an owner first. - Every change is written to the audit log as
membership.role_removedormembership.removed.
Partner handle
Each organization has a partner handle such as harbor-logistics-7kq2m9xd, shown under Organization > Partner handle. Share this handle with API owners who want to invite you. Looking it up reveals only your organization name, and an invitation sent to it can be accepted only by your partner administrators or owners. See Invite an existing partner by handle.
Any member reads it with GET /v1/organizations/{id}/partner-handle. Owners replace it with POST /v1/organizations/{id}/partner-handle/rotate when it has been shared more widely than intended: the old handle stops resolving at once, and invitations already sent stay valid. Replacements are audited as organization.partner_handle_rotated.
Billing administration is explicit
Holding owner is not enough to spend money. To check out or open the billing portal, an owner assigns billing_admin to themselves or to a colleague:
POST https://app.mutualtrust.io/v1/organizations/org_01jcqa7m2k9x4v8r3t6p5w2h7n/roles
X-CSRF-Token: <csrf_token>
Content-Type: application/json
{ "user_id": "usr_01jcq9z8y6x4w2v0t8s6r4q2p0", "role": "billing_admin" }{
"user_id": "usr_01jcq9z8y6x4w2v0t8s6r4q2p0",
"role": "billing_admin"
}A billing admin can update a card or download invoices without gaining access to trust configuration, and a finance or support colleague at Sectigo cannot change your payment method at all.
API credentials
API credentials let automation act for you on https://api.mutualtrust.io with a subset of your roles in one organization. Create them from a signed-in portal session (a credential cannot mint another credential).
POST https://app.mutualtrust.io/v1/organizations/org_01jcqa7m2k9x4v8r3t6p5w2h7n/api-credentials
X-CSRF-Token: <csrf_token>
Content-Type: application/json
{ "name": "terraform-ci", "roles": ["service_owner"], "expires_in_days": 90 }{
"id": "cred_01jcqd5h7j9k1m3n5p7q9r1s3t",
"secret": "mtk_Jx4c…",
"expires_at": "2027-02-12T16:20:00Z",
"roles": [
"service_owner"
],
"name": "terraform-ci",
"note": "Shown once. Send as \"Authorization: Bearer <secret>\" to https://api.mutualtrust.io."
}rolesmust be a subset of the roles you hold in that organization, or the request fails with403naming the excess roles.expires_in_daysis 1 to 365 (default 90).- Effective roles on each request are the credential’s roles intersected with the creator’s current roles: remove a role from the creator and every credential they made loses it too.
- List with
GET /v1/organizations/{id}/api-credentials(name, roles, prefix, purpose, expiry, last used, creator, active). The secret is never shown again. - Revoke with
POST /v1/organizations/{id}/api-credentials/{cid}/revoke→{"id":"cred_…","active":false}. The creator or an owner can revoke. - Bearer credentials are not ambient, so they need no CSRF token. Credentials made for MCP clients have
purpose: "mcp"and a 30-day lifetime (see MCP server). - API credentials cannot make high-risk changes in production unless they were created with
"high_risk_allowed": true. Creating such a credential needs a step-up (a sign-in or passkey re-authentication from the last 15 minutes) and it expires within 30 days (expires_in_days1 to 30, default 30). Without the flag the API answers403 high_risk_credential_required.
Support access grants
Sectigo support staff have no standing access to your audit detail or invitations. An owner grants a narrow, reason-coded, expiring permission to one named staff member:
POST https://app.mutualtrust.io/v1/organizations/org_01jcqa7m2k9x4v8r3t6p5w2h7n/support-grants
X-CSRF-Token: <csrf_token>
Content-Type: application/json
{ "staff_email": "jane.doe@sectigo.example", "action": "audit.detail", "reason_code": "customer_request", "expires_in_hours": 24 }{
"id": "sg_01jcqd3f5g7h9j1k3m5n7p9q1r",
"staff_email": "jane.doe@sectigo.example",
"action": "audit.detail",
"reason_code": "customer_request",
"expires_at": "2026-11-15T16:25:00Z"
}| Field | Values |
|---|---|
action | audit.detail (view unredacted audit detail) or invitation.resend. |
reason_code | customer_request, incident, diagnostics. |
expires_in_hours | 1 to 72. |
List with GET …/support-grants and end early with POST …/support-grants/{gid}/revoke. Every staff request is logged with the staff identity, purpose and your tenant, and every staff write appears in your audit log with actor_type: "staff". Support cannot alter PKI keys, prices or payment methods.
Audit log and export rights
GET /v1/organizations/{id}/audit (owner, trust admin, auditor) returns 100 events per page, newest first; pass ?before=<next_cursor> for the next page.
{
"data": [
{
"id": "aud_01jcqm4x7h3j5k7m9n1p3q5r7s",
"actor_type": "user",
"actor_id": "usr_01jcq9z8y6x4w2v0t8s6r4q2p0",
"action": "grant.revoke",
"resource_type": "grant",
"resource_id": "gr_01jcqb3p1n3p5q7r9s1t3v5w7x",
"resource_revision": 4,
"reason": "contract_ended",
"operation_id": "op_01jcqm4x7f9g1h3j5k7m9n1p3q",
"request_id": "req_8e1f3a9c2b7d4e60",
"environment": "sandbox",
"detail": null,
"created_at": "2026-11-14T16:02:11Z"
}
],
"next_cursor": null
}Exporting is a separate right. An owner grants it, for a limited time, to a member who holds auditor, owner or trust_admin:
POST https://app.mutualtrust.io/v1/organizations/org_01jcqa7m2k9x4v8r3t6p5w2h7n/export-grants
X-CSRF-Token: <csrf_token>
Content-Type: application/json
{ "user_id": "usr_01jcqc1d3e5f7g9h1j3k5m7n9p", "expires_in_hours": 48 }{
"id": "exg_01jcqd4g6h8j0k2m4n6p8q0r2s",
"user_id": "usr_01jcqc1d3e5f7g9h1j3k5m7n9p",
"expires_at": "2026-11-16T16:30:00Z"
}GET https://api.mutualtrust.io/v1/organizations/org_01jcqa7m2k9x4v8r3t6p5w2h7n/audit-export?from=2026-11-01T00:00:00Z&to=2026-12-01T00:00:00Z
Authorization: Bearer mtk_…The export needs both audit.read and an active export grant (otherwise 403). It returns a canonical JSON document (mt.audit_export.v1, up to 50,000 rows) as an attachment with an X-Content-SHA256 header; a copy is retained in protected storage and the export itself is audited. See Wire formats.
Approval policies
An approval policy says how strict the second person in an approval must be. The same five settings apply to integration plan approvals, to approving partner grants on services that use sponsor review, and to the Mutual Trust staff approvals that affect your organization (credits and refunds, destroying a key in one of your spaces, and a policy epoch bump on one of your services).
| Setting | Meaning | Stricter when |
|---|---|---|
min_approver_age_hours (0 to 720) | How long the approver must have been a member of your organization (for staff: enrolled as staff). | Higher |
require_second_approver | Someone other than the requester must approve: not the person who submitted the plan, and not the person who created the invitation behind a pending grant. | true |
require_step_up | The approver must have signed in, or confirmed with their passkey, recently. API credentials can never step up. | true |
step_up_max_age_minutes (1 to 60) | How recent that sign-in must be. | Lower |
independence | standard lets the only eligible member approve their own request; strict never does (the request waits for a second person). | strict |
Where values come from. Policies are set on nested scopes: the Mutual Trust platform, your organization, a space, a service and an integration. A scope sets only the values it needs; everything else is inherited. Mutual Trust sets the platform and organization scopes (on request, with two staff members involved); these are floors. Your owners and trust administrators set the space, service and integration scopes: the most specific scope that sets a value wins, but never below the floor. A value below the floor is refused with 422 approval_policy_below_floor, and a stored value that a later floor overtakes is shown as not applied.
Defaults. With nothing set, approvals behave as before: integration plans need a passkey or organization sign-in from the last 15 minutes and, when the integration requires it, a second approver; partner grants need no extra checks. These built-in rules are minimums no policy can relax.
Changing a policy. In the portal, open a service or integration and use Approval policy: it shows each effective value and where it comes from. Through the API, read the scope, then send a partial policy with the revision you read (0 when nothing is set yet); null clears a value so it is inherited again. Tightening is accepted from API credentials, so Terraform can manage it (mutualtrust_approval_policy). Relaxing a production scope needs a browser session that signed in or confirmed with a passkey within 15 minutes, otherwise 403 step_up_required. Every change is in the audit log (approval_policy.updated, with the effective policy before and after) and emits an approval_policy.updated event; every approval records the policy it was checked against and where each value came from.
PUT https://api.mutualtrust.io/v1/services/svc_01jcqb2n4p6r8t0v2x4z6b8d0f/approval-policy
Authorization: Bearer mtk_…
If-Match: "0"
Content-Type: application/json
{ "require_second_approver": true, "independence": "strict", "min_approver_age_hours": 72 }The same call works on /v1/spaces/{id}/approval-policy and /v1/integrations/{id}/approval-policy. GET /v1/organizations/{id}/approval-policy shows the floor and every scope in the organization that sets a value. Refused approvals return second_approver_required, approver_too_new or step_up_required, each with the approval_policy that applied.
Workload issuers
Partner admins approve the OIDC issuers their workloads may use; anyone in the organization can list them with GET /v1/organizations/{id}/workload-issuers. Partner admins also bind and rebind Kubernetes service account UIDs on workloads (PATCH /v1/workloads/{id}/binding). See Enrollment.
Domain and legal-entity verification
Production needs a verified organization (production onboarding). There are two separate verifications with two separate labels. They are never merged: a verified domain says nothing about the legal entity, and a verified legal entity says nothing about DNS.
| Label | What it proves | How | Lasts |
|---|---|---|---|
| Domain control verified | The organization controls a DNS domain. | An owner adds the domain and publishes a TXT record at _mutualtrust-challenge.<domain>. Mutual Trust checks it over DNS-over-HTTPS through two resolvers that must agree (details). | 90 days, re-checked automatically. |
| Legal entity verified | The organization is the registered legal entity it says it is. | An owner submits the legal name, registration number, jurisdiction and evidence notes. Mutual Trust staff review it and a second, independent staff member approves it. | Until a newer submission is approved. |
- Domains:
GETandPOST /v1/organizations/{id}/domains,POST /v1/organizations/{id}/domains/{domain_id}/check(check now),DELETE /v1/organizations/{id}/domains/{domain_id}. Owners add and remove domains; any member can read them. Up to 10 domains per organization. - Legal entity:
GETandPOST /v1/organizations/{id}/legal-entity. One submission can await review at a time (409 verification_open). When staff reject a submission, the reason is shown so you can correct it and submit again.
Single sign-on and provisioning
Owners connect the organization's own identity provider, enforce SSO-only sign-in with break-glass owners, and provision members over SCIM with a group to role mapping. See Your organization's identity provider and SCIM provisioning.
Partner delegations
A reseller or managed service provider can administer your organization only through a delegation that an owner of your organization accepts (POST /v1/delegations/{id}/accept, fresh sign-in). The Partner delegations tab shows the partner, the roles, the effective dates and the state. A delegation never includes the owner role, never lets the partner mint API credentials or grant support access in your organization, and ends when either side revokes it (POST /v1/delegations/{id}/revoke) or its end date passes. Who pays for your organization (a reseller payer) and who referred you are separate records: they grant no access, and ending a delegation does not change them. See Resellers and MSPs.
Owner recovery
If every owner is locked out, a person at your organization can ask Mutual Trust to restore ownership with fresh DNS evidence and a contact attestation; two independent staff members decide and the named person claims it with a fresh sign-in. Current owners see open requests under Owner recovery and can object. See Owner recovery.
High-risk changes and step-up
These changes need a recent sign-in: a passkey re-authentication or a fresh sign-in with your organization identity provider within the step-up window (15 minutes, shorter if an approval policy says so). The portal asks you to confirm with your passkey and names the affected service, organization and environment before it proceeds. See Identity and access.
- Withdraw or suspend a production grant, or widen the routes of a production grant.
- Remove a member, or remove a role from a member.
- Create an API credential with the owner or trust administrator role, or one with
high_risk_allowed. - Relax a production approval policy.
- Approve a workload issuer.
- Rotate the partner handle.
- Generate new recovery codes.
- Create a production space or production service.
- Disable a production gateway.
- Turn off the second approver on a production integration. API credentials without
high_risk_allowedcannot turn it off in any environment. - Change the organization identity provider, SSO-only sign-in or break-glass owners; create or rotate a SCIM token; accept a partner delegation; claim ownership after an approved recovery. These always need a fresh browser sign-in; API credentials cannot make them.
- Change which roles SCIM groups grant (once the organization is production enabled).
Production changes always need step-up. Organization-wide changes (members, roles, credentials, workload issuers, partner handle) need it once the organization is production enabled: production access approved or a production space exists. Sandbox-only organizations are not asked. API credentials can make these changes only when created with high_risk_allowed; otherwise the API answers 403 high_risk_credential_required. A stale session gets 403 step_up_required with high_risk_action and max_age_seconds.