Skip to content

Team & roles API ​

Invite people to your account and control what they can do with roles. A role is a named bundle of permissions; you assign one to each member (and, with the same model, to API keys).

All endpoints need a bearer token. In practice team, role, and account management is Owner work done from a login session - API keys can never hold team, roles, or account permissions, so these endpoints always return 403 to a key.

For the concept-level overview, see Team & roles.

Owner vs member ​

  • The Owner is whoever registered the account. They hold the global wildcard * - full access to everything, implicitly. There is no role to assign and nothing to configure, and the wildcard is never stored on a role.
  • Everyone else is a Member with exactly one assigned role. Their access is whatever that role grants, and nothing more.

You can never grant more than you hold yourself. Creating, editing, or assigning a role whose permissions exceed your own returns 403 ("A role cannot grant permissions beyond your own"). The Owner is the only account holder this never bites.

Permission tokens ​

A permission is a resource:action string, for example pods:read or billing:pay. A wildcard resource:* grants every action on that resource (pods:*). Roles are built from these tokens. The global * is not grantable - it belongs to the Owner alone.

A few actions are more sensitive than the rest - grant them deliberately. They are flagged "sensitive": true in the catalogue:

  • pods:console - open a shell inside a pod
  • domains:ssl - issue and manage SSL certificates
  • billing:pay - spend money (top up credit)

The permission catalogue ​

http
GET /api/permissions

Needs roles:read. Returns every resource and the actions it supports - the source of truth for what you can put in a role. Each action is an object, not a bare string:

json
{
  "resources": [
    {
      "key": "pods",
      "label": "Pods",
      "actions": [
        { "key": "read", "label": "View", "sensitive": false },
        { "key": "create", "label": "Create", "sensitive": false },
        { "key": "update", "label": "Update", "sensitive": false },
        { "key": "delete", "label": "Delete", "sensitive": false },
        { "key": "console", "label": "Console access", "sensitive": true }
      ]
    }
  ]
}

Build a token by joining the two keys with a colon: pods + console gives pods:console. The array is in display order, so you can render it straight into a permission grid.

The 16 resources and their actions:

ResourcelabelActions
podsPodsread, create, update, delete, console
networksNetworksread, create, update, delete
firewallsFirewallsread, create, update, delete
domainsDomainsread, create, update, delete, ssl
snapshotsSnapshotsread, create, update, delete
backupsBackupsread, create, update, delete
emailEmailread, create, update, delete
sshkeysSSH keysread, create, delete
monitoringMonitoringread, update
billingBillingread, pay
supportSupportread, create, update
apikeysAPI keysread, create, delete
rolesRolesread, create, update, delete
teamTeam membersread, create, update, delete
accountAccount settingsread, update
auditActivity logread

Not every resource has all four verbs. sshkeys and apikeys have no update because there is nothing to edit - delete and re-add. audit is read-only, and monitoring has no create or delete.

Roles ​

List roles ​

http
GET /api/roles/

Needs roles:read. Returns your account's roles as a plain array, seeding the four system roles on first call if they are not there yet. A role looks like:

json
{
  "id": "6a4d5b83b7006ccae6391841",
  "accountId": "6a4d5b83b7006ccae6391800",
  "name": "Deployer",
  "description": "Can manage pods and domains",
  "permissions": ["pods:*", "domains:read", "domains:update"],
  "isSystem": false,
  "createdAt": "2026-07-16T09:41:02Z",
  "updatedAt": null
}

System roles ​

Every account starts with four ready-made roles. They are marked isSystem: true and cannot be edited or deleted:

RoleGrants
Full accessEverything except team, roles, and account settings. The Owner always has those; a custom role can grant them too if you want to delegate.
DeveloperManage pods, networks, firewalls, domains, snapshots, backups, SSH keys, monitoring, and support; read-only email and activity log.
Read-onlyThe read action on everything. Look, don't touch.
BillingFull billing; read-only account and activity log.

Their permission lists are expanded from the catalogue rather than written out, so a resource added to the platform flows into them automatically.

Create a role ​

http
POST /api/roles/

Needs roles:create.

json
{
  "name": "Deployer",
  "description": "Can manage pods and domains",
  "permissions": ["pods:*", "domains:read", "domains:update"]
}

Returns 201 with the new role. Duplicate tokens are de-duplicated for you.

Update a role ​

http
PUT /api/roles/:roleId

Needs roles:update.

json
{
  "name": "Deployer",
  "description": "Pods, domains, and now snapshots",
  "permissions": ["pods:*", "domains:*", "snapshots:*"]
}

Only your own custom roles - a system role returns 403 ("System roles cannot be edited"). Every field is replaced, so send the full permissions list, not a delta.

Editing a role takes effect immediately: everyone holding it is re-evaluated on their next request, and a permission you removed stops working right away rather than at the end of a cache window.

Delete a role ​

http
DELETE /api/roles/:roleId

Needs roles:delete.

json
{ "message": "Role deleted" }
ProblemStatusMessage
Role is a system role403System roles cannot be deleted
Role is still assigned to someone409This role is still assigned to team members. Reassign them before deleting it.
Unknown token in permissions400Unknown or disallowed permission: pods:deploy
Empty name400Role name is required

Team members ​

List members ​

http
GET /api/team/

Needs team:read. A plain array of users on the account, including you. The Owner's entry has accountRole: "owner" and no accountRoleId:

json
[
  {
    "id": "6a4d5b83b7006ccae6391802",
    "username": "bob",
    "firstName": "Bob",
    "lastName": "Lee",
    "email": "bob@example.com",
    "lastLogin": null,
    "isActive": true,
    "isVerified": true,
    "role": "user",
    "accountRole": "member",
    "accountRoleId": "6a4d5b83b7006ccae6391841",
    "accountId": "6a4d5b83b7006ccae6391800",
    "createdAt": "2026-08-02T14:30:00Z",
    "updatedAt": null
  }
]

An invited member is created verified (you vouched for them) with lastLogin: null until they set a password and sign in.

Invite a member ​

http
POST /api/team/

Needs team:create.

json
{
  "username": "bob",
  "firstName": "Bob",
  "lastName": "Lee",
  "email": "bob@example.com",
  "roleId": "6a4d5b83b7006ccae6391841"
}

Sends an email invitation. The invited person clicks the link and sets their own password through the password-reset endpoint - you never see or set a temporary one, and the invitation link is good for 7 days. Leave roleId empty and they start on the least-privilege Read-only role, which you can change later. Returns 201 with the new member.

A taken username or email returns 409. A roleId that is not one of your account's roles returns 400.

Change a member's role ​

http
PATCH /api/team/:userId/role

Needs team:update.

json
{ "roleId": "6a4d5b83b7006ccae6391841" }
json
{ "message": "Member role updated" }

The Owner's role cannot be changed (400), and the member's live sessions pick up the new permissions on their next request.

Enable or disable a member ​

http
PATCH /api/team/:userId/status

Needs team:update.

json
{ "isActive": false }

Returns the updated member. Disabling a member blocks their access without removing them - handy for someone who is away for a while. Their sessions stop working immediately, and so do any API keys they created.

You cannot change your own status, and the Owner cannot be disabled. Both return 400.

Remove a member ​

http
DELETE /api/team/:userId

Needs team:delete.

json
{ "message": "Team member removed" }

Removes them from the account and revokes any API keys they created. Key revocation happens first: if it fails, the removal is abandoned with a 500 rather than leaving a live key behind an account that no longer exists.

You cannot remove yourself, and the Owner cannot be removed. Both return 400.

Example ​

Invite a developer, then check what they can do:

bash
curl -X POST https://cloud-api.microapps.io/api/team/ \
  -H "Authorization: Bearer $MICROAPPS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "username": "bob",
    "firstName": "Bob",
    "lastName": "Lee",
    "email": "bob@example.com",
    "roleId": "6a4d5b83b7006ccae6391841"
  }'

curl -s https://cloud-api.microapps.io/api/roles/ \
  -H "Authorization: Bearer $MICROAPPS_TOKEN" \
  | jq '.[] | select(.id == "6a4d5b83b7006ccae6391841") | .permissions'

Built for the long tail.