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

  1. Authenticate to the API.
  2. Create the account.
  3. 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 -- /me never 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.