Account API
Aspire name api-account. Everything about who exists and what they may do.
Organizations
| Method | Path | Permission |
|---|---|---|
GET | /organizations | GlobalOrganizationList (global) |
GET | /organizations/{id} | GlobalOrganizationRead (global) |
GET | /organizations/{id}/signin | Anonymous |
POST | /organizations/identityproviders/check | GlobalOrganizationRead + (GlobalOrganizationUpdate or GlobalOrganizationCreate) |
POST | /organizations | GlobalOrganizationCreate (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
| Method | Path | Permission |
|---|---|---|
GET | /organizations/me/credits | JWT only |
{ "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
{
"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
| Method | Path | Permission |
|---|---|---|
GET | /users | UserList |
GET | /users/names | AuditList |
POST | /users/names/lookup | JWT only |
GET | /users/{id}/permissions | Anonymous |
GET | /users/me | JWT only |
POST | /users/provision | Anonymous |
GET | /users/{id} | UserRead |
POST | /users | UserCreate |
PUT | /users/{id} | UserUpdate |
PUT | /users/{id}/groups | 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.
{ "userIds": ["9c1e7a4d-…", "3f52a18b-…"] }
[
{ "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
{
"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.
| Method | Path | Permission |
|---|---|---|
GET | /groups | UserList |
GET | /groups/{id} | UserRead |
POST | /groups | UserCreate + grant check |
PUT | /groups/{id} | UserUpdate + grant check |
PUT | /groups/{id}/members | UserUpdate |
DELETE | /groups/{id} | UserDelete |
{
"name": "Deployment",
"description": "Accounts behind deployment API keys",
"permissions": ["VariableList", "VariableRead", "VariableReadSecrets"]
}
Two checks run on save, beyond the endpoint permission:
- 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.
- Dependency check. A group carrying
AuditReadwithoutAuditListis refused — the permission would do nothing, and a group that silently does nothing is worse than a rejected save.
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
| Method | Path | Permission |
|---|---|---|
GET | /permissions | JWT only |
{
"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
| Method | Path | Permission |
|---|---|---|
POST | /apikeys/lookup | Anonymous |
GET | /users/{id}/apikeys | UserRead |
GET | /users/me/apikeys | JWT only |
POST | /users/me/apikeys | ProfileCreateApiKey (+ ProfileNeverExpiresApiKey for NoExpiry) |
POST | /users/{id}/apikeys | UserCreateServiceAccountApiKey (+ 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.