Skip to main content

Account API

Aspire name api-account. Everything about who exists and what they may do.

Organizations

MethodPathPermission
GET/organizationsOrganizationList (global)
GET/organizations/{id}OrganizationRead (global)
GET/organizations/{id}/signinAnonymous
POST/organizations/identityproviders/checkOrganizationRead + (OrganizationUpdate or OrganizationCreate)
POST/organizationsOrganizationCreate (global)
PUT/organizations/{id}OrganizationUpdate (global)
DELETE/organizations/{id}OrganizationDelete (global)

An organization record carries its name, active flag, identity providers, branding and account patterns.

Sign-in details

GET /organizations/{id}/signin
{
"id": "05cc0c65-021c-4515-850f-366667e26656",
"name": "Acme Ltd",
"logo": "…base64…",
"logoContentType": "image/png",
"customCss": ".login-card { border-radius: 16px; }",
"providers": [
{ "provider": "Entra", "clientId": "…", "tenantId": "…" }
]
}

Anonymous, because the login page renders before anyone is signed in. Only active organizations answer — an ID naming a disabled organization and an ID naming none both get 404, since in both cases there is nothing there to sign in to.

See the security note on what this endpoint exposes.

Checking provider credentials

See Identity providers.

Users

MethodPathPermission
GET/usersUserList
GET/users/namesAuditList
POST/users/names/lookupJWT only
GET/users/{id}/permissionsAnonymous
GET/users/meJWT only
POST/users/provisionAnonymous
GET/users/{id}UserRead
POST/usersUserCreate
PUT/users/{id}UserUpdate
DELETE/users/{id}UserDelete

The three ways to look a user up

They exist because three different callers need three different amounts, and asking for the largest permission each time would either over-grant or block the caller entirely.

GET /users — the full directory. UserList.

GET /users/names — names and email addresses only. Asks for AuditList, not UserList, because that is who needs it: whoever may read the audit log may see who wrote each entry, and a name is all of a user that takes. Anyone holding UserList reads /users instead.

POST /users/names/lookup — names for IDs the caller already has. No permission of its own, because there is nothing here to enumerate with: an ID is come by in the first place by reading a record you were allowed to read, and an ID nobody guessed is an ID nobody is told about. Scoped to the caller's organization all the same.

POST /users/names/lookup
{ "userIds": ["9c1e7a4d-…", "3f52a18b-…"] }
200 OK
[
{ "id": "9c1e7a4d-…", "name": "Neil Docherty" },
{ "id": "3f52a18b-…", "name": "Someone Else" }
]

This is how the library set API puts names to the IDs stamped on its records.

Provisioning

POST /users/provision
{
"provider": "Entra",
"providerUserId": "…",
"email": "neil@example.com",
"name": "Neil Docherty",
"organizationId": "05cc0c65-…",
"profileImage": null
}

The first call of every login. Anonymous, because it decides whether the login goes any further. It is also where an account is brought back into step with the provider — a rename, a new address, a new photo — before the token is minted, so the token reflects what is true now. See Auth overview.

Own profile

GET /users/me returns the caller's account with its linked identities and profile image. JWT only — it answers about whoever is asking, and the JWT decides who that is.

Groups

MethodPathPermission
GET/groupsGroupList
GET/groups/{id}GroupRead
POST/groupsGroupCreate + grant check
PUT/groups/{id}GroupUpdate + grant check
PUT/groups/{id}/membersGroupUpdate
DELETE/groups/{id}GroupDelete
POST /groups
{
"name": "Deployment",
"description": "Accounts behind deployment API keys",
"permissions": ["VariableList", "VariableRead", "VariableReadSecrets"]
}

Two checks run on save, beyond the endpoint permission:

  1. Grant check. Every permission being added must be one the caller may grant. Rejected ones are named in the response. Permissions already on the group that the caller cannot grant are preserved, not dropped.
  2. Dependency check. A group carrying AuditRead without AuditList is refused — the permission would do nothing, and a group that silently does nothing is worse than a rejected save.

See granting versus holding.

Membership is changed separately, through PUT /groups/{id}/members, which needs GroupUpdate and no grant check — adding somebody to a group is not the same act as deciding what the group may do.

Permissions catalogue

MethodPathPermission
GET/permissionsJWT only
200 OK
{
"permissions": [
{
"name": "VariableReadSecrets",
"value": 1795,
"globalPermission": false,
"dependsOn": ["VariableRead"]
}
]
}

Every signed-in user's UI resolves its own permissions against this, so it asks for none of its own. Read the catalogue from here rather than hard-coding it — the dependency graph in particular is what the management UI uses to work out which permissions to offer alongside one another.

See the permissions catalogue reference.

API keys

MethodPathPermission
POST/apikeys/lookupAnonymous
GET/users/{id}/apikeysUserRead
GET/users/me/apikeysJWT only
POST/users/me/apikeysProfileCreateApiKey (+ ProfileNeverExpiresApiKey for NoExpiry)
DELETE/users/me/apikeys/{id}JWT only
DELETE/users/{id}/apikeys/{apiKeyId}UserDelete

POST /apikeys/lookup is a POST rather than a GET because the hash identifies a credential, and a GET would put it in the request line where every access log and proxy along the way would keep a copy.

"me" is not a GUID, so the route constraint keeps /users/me/apikeys/{id} and /users/{id}/apikeys/{apiKeyId} apart even though a caller could name their own ID in the second. Doing so is allowed and does the same thing — it just asks more of them.

Full detail in API keys.