There's just one kind of user row (AppUser) for everyone -- Windows/domain, Azure AD, or a password-based "app account" alike, all provisioned into the same table the first time they're seen. What makes an account an "app account" in practice is simply that it has a password (see Set / Remove a User's Password) rather than relying on Windows or Azure AD to authenticate it; there's no separate table or type code.
Having trouble reaching the API, or logging in from another machine? See Network Access & Authentication Security -- by default the Api only answers
localhost, and PowerShell needs an extra flag once it does answer elsewhere.
Order of Operations
- Authenticate to the API.
- Create the account.
- Give it a password (see Set / Remove a User's Password) and a role (see Manage Role Membership).
The calls
GET /api/security/users
POST /api/security/users
DELETE /api/security/users/{userId}
GET /api/security/me
Permission needed: Security.Manage for list/create/delete. GET /api/security/me needs no permission at all -- any authenticated caller can look up their own account.
C# example
var jsonOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
// Step 1: Authenticate to the API.
using var handler = new HttpClientHandler { UseDefaultCredentials = true };
using var client = new HttpClient(handler) { BaseAddress = new Uri("http://your-minion-agent-server:5443") };
client.DefaultRequestHeaders.Add("X-App-Name", "MyIntegration");
// Step 2: Create the account.
var response = await client.PostAsJsonAsync("/api/security/users", new CreateUserRequest("svc-reporting"), jsonOptions);
response.EnsureSuccessStatusCode();
var created = await response.Content.ReadFromJsonAsync<UserSummary>(jsonOptions);
Console.WriteLine($"Created user id: {created!.UserId}");
// List every account.
var users = await client.GetFromJsonAsync<UserSummary[]>("/api/security/users", jsonOptions);
foreach (var u in users!)
Console.WriteLine($"{u.UserId}: {u.UserName}, has password = {u.HasPassword}, disabled = {u.IsDisabled}, roles = {string.Join(", ", u.Roles.Select(r => r.RoleName))}");
// Who am I?
var me = await client.GetFromJsonAsync<UserSummary>("/api/security/me", jsonOptions);
Console.WriteLine($"Signed in as {me!.UserName}");
// Delete it later.
var deleteResponse = await client.DeleteAsync($"/api/security/users/{created.UserId}");
deleteResponse.EnsureSuccessStatusCode();
record CreateUserRequest(string UserName);
record RoleSummary(int RoleId, string RoleName, bool IsBuiltIn, bool BypassesSecurity);
record UserSummary(int UserId, string UserName, RoleSummary[] Roles, bool HasPassword, bool IsDisabled);
Not on a domain machine? Swap in the app-account login from Calling the API From Your Own Code.
PowerShell example
# Step 1: Authenticate to the API.
$headers = @{ "X-App-Name" = "MyIntegration" }
# Step 2: Create the account. (-AllowUnencryptedAuthentication: PowerShell requires this for Windows auth over plain http to anything but localhost -- see "Network Access & Authentication Security".)
$body = @{ UserName = "svc-reporting" } | ConvertTo-Json
$created = Invoke-RestMethod -Uri "http://your-minion-agent-server:5443/api/security/users" `
-Method Post -Body $body -ContentType "application/json" -UseDefaultCredentials -AllowUnencryptedAuthentication -Headers $headers
"Created user id: $($created.userId)"
# List every account.
$users = Invoke-RestMethod -Uri "http://your-minion-agent-server:5443/api/security/users" -UseDefaultCredentials -AllowUnencryptedAuthentication -Headers $headers
$users | ForEach-Object { "$($_.userId): $($_.userName), has password = $($_.hasPassword), disabled = $($_.isDisabled)" }
# Who am I?
$me = Invoke-RestMethod -Uri "http://your-minion-agent-server:5443/api/security/me" -UseDefaultCredentials -AllowUnencryptedAuthentication -Headers $headers
"Signed in as $($me.userName)"
# Delete it later.
Invoke-RestMethod -Uri "http://your-minion-agent-server:5443/api/security/users/$($created.userId)" -Method Delete -UseDefaultCredentials -AllowUnencryptedAuthentication -Headers $headers
What you get back
POST -- 201 Created:
{ "userId": 41, "userName": "svc-reporting", "roles": [], "hasPassword": false, "isDisabled": false }
A brand-new account always comes back with no roles and no password yet -- add those separately.
GET /api/security/users -- 200 OK, an array:
[ { "userId": 41, "userName": "svc-reporting", "roles": [ { "roleId": 9, "roleName": "Report Viewers", "isBuiltIn": false, "bypassesSecurity": false } ], "hasPassword": true, "isDisabled": false } ]
GET /api/security/me -- 200 OK -- same shape, but roles always comes back empty (it's a lightweight self-check, not a full membership query -- use Check Effective Permissions if you need to know what you can actually do).
DELETE -- 204 No Content -- no body.
Codes this call can return
See API Response Codes for what each one means in general. For these calls:
- 201 -- created.
- 200 -- the list or self-lookup.
- 204 -- deleted.
- 400 -- (create) the name is blank, or Identity rejected it internally.
- 403 -- you don't have
Security.Manage(list/create/delete only --/menever returns this). - 404 -- (delete) no user exists with that id.
- 409 -- (create) a user with that name already exists.
What gets audited
Create is recorded as AppUser / Create User; delete as AppUser / Delete User, with the username in the before/after-state. The list and self-lookup GETs aren't audited.
See also: Set / Remove a User's Password, Enable / Disable a User, Manage Role Membership.