Skip to main content

Account API

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

Organizations​

MethodPathPermission
GET/organizationsGlobalOrganizationList (global)
GET/organizations/{id}GlobalOrganizationRead (global)
GET/organizations/{id}/signinAnonymous
POST/organizations/identityproviders/checkGlobalOrganizationRead + (GlobalOrganizationUpdate or GlobalOrganizationCreate)
POST/organizationsGlobalOrganizationCreate (global)
PUT/organizations/{id}GlobalOrganizationUpdate (global)
DELETE/organizations/{id}GlobalOrganizationDelete (global)

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

Credits​

MethodPathPermission
GET/organizations/me/creditsJWT only
200 OK
{ "usedCredits": 42, "totalCredits": 1000 }

The caller's own organization's credit balance. totalCredits is the pricing band's included credits plus the organization's additional credits. The library set API reads this before every billable call, forwarding the caller's JWT.

The account API also consumes the credits-spent Service Bus queue, adding each message's amount to the organization's used credits, so the balance can lag slightly behind real use. Used credits are reset to 0 when a subscription payment is applied.

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
PUT/users/{id}/groupsUserUpdate
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​

Managing groups uses the same permissions as managing users — see the note below — so there is no permission family of its own for groups.

MethodPathPermission
GET/groupsUserList
GET/groups/{id}UserRead
POST/groupsUserCreate + grant check
PUT/groups/{id}UserUpdate + grant check
PUT/groups/{id}/membersUserUpdate
DELETE/groups/{id}UserDelete
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 UserUpdate and no grant check — adding somebody to a group is not the same act as deciding what the group may do. The same membership can also be set from the other side, through PUT /users/{id}/groups.

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)
POST/users/{id}/apikeysUserCreateServiceAccountApiKey (+ ProfileNeverExpiresApiKey for NoExpiry); service accounts only
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.