Machine Users
A machine user is a service account. A backend service uses it to call the Manager API and to call applications behind the proxy. Use a machine user when a program, not a person, needs to authenticate. For your own scripts, use a Personal Access Token instead.
What a machine user is
- It belongs to one environment of a project (for example the
productionenvironment of projectshop). It is not tenant-scoped: one backend registers once per (application, environment) and serves every tenant that has the application enabled. - It has one credential: either a Bearer token the Manager generates, or a username and password you choose (Basic authentication).
- It can be enabled or disabled. A disabled machine user cannot authenticate.
- It can be granted access to specific application endpoints.
- It has no expiry date, but see Rotating a credential.
Usernames must be unique within an environment.
Authentication methods
Bearer token
Recommended. The Manager generates a random 43-character token (32 random bytes, base64url). Only a hash is stored; the plaintext is shown once, at creation.
Authorization: Bearer <token>Basic authentication
For clients that can only send a username and password.
Authorization: Basic <base64(username:password)>The two schemes do not overlap. A credential's username decides which one it is: a bearer-shaped username (Bearer, or Bearer:<prefix>) marks a token credential, and the Basic path refuses it outright, so a token cannot be replayed as a Basic password. Send a token as Authorization: Bearer.
Where each method works
| Route | Bearer | Basic |
|---|---|---|
Manager /api/machine/* (backend registration, heartbeat, tenant list, comms) | yes | no |
Manager /esb/{applicationId}/... (ESB endpoint calls) | yes | no |
Applications behind the proxy (the proxy validates through POST /api/validate-proxy-auth) | yes | yes |
Application access needs an endpoint assignment
When a machine user calls an application through the proxy, the Manager accepts the credential only if that machine user has at least one enabled endpoint assignment on that application. Otherwise the proxy is told "Machine user does not have access to this application". See Endpoint access control.
Repeated failed authentications against /api/machine/* and /esb/* are rate-limited. After too many 401 responses the Manager answers 429 Too Many Requests with a Retry-After header.
How credentials are stored
Tokens and passwords are hashed with PBKDF2-SHA256: 600,000 iterations, a random 16-byte salt per credential, 32-byte output. For Bearer tokens the first 12 characters are also stored in plain text as the tokenPrefix, so the Manager can find the right record without hashing every candidate and so you can recognise a token in the UI.
Creating a machine user
In the UI
- Open your project, then open a tenant. In the Tenant section of the sidebar, click Machine Users.
- If the project has more than one environment, pick one in the drop-down at the top of the page. The list shows the machine users of that environment.
- Click Create Machine User.
- Fill in:
- Name - what the service is, for example
Payment Service. - Authentication Type - Bearer Token or Basic Authentication.
- Username and Password - only for Basic Authentication.
- Enabled - leave on.
- Name - what the service is, for example
- Click Save.
- Copy the token (or the username and password) from the confirmation dialog. They are shown only once. Click I've saved my credentials to close it.
Creating, editing (name, enabled) and deleting a machine user all require the project role maintainer or owner, or the system role administrator.
The mint writes an audit log entry (MachineUser / create) recording who minted it and in which environment, and never the token, its hash or its prefix. The write is not best-effort: if the audit row cannot be written the mint is rolled back. Edits write update, deletes write delete, and rotateMachineUserToken writes rotate with key_rotated: true in the metadata.
With the API: Bearer token
mutation {
createMachineUserWithCredentials(
environmentId: "environment-uuid"
input: {
name: "Payment Service"
username: "Bearer"
hashedKey: "__GENERATE_TOKEN__"
enabled: true
}
) {
generatedToken
machineUser {
id
username
tokenPrefix
}
}
}hashedKey: "__GENERATE_TOKEN__" tells the Manager to generate the token. generatedToken holds the plaintext and is returned only by this mutation. The Manager replaces username with an internal Bearer:<prefix> value, and ignores any tokenPrefix you send. Minting a token credential is the server's job alone: a create that carries a bearer-shaped username with a key of your own is refused, because the username identifies the scheme.
With the API: Basic authentication
mutation {
createMachineUserWithCredentials(
environmentId: "environment-uuid"
input: {
name: "Legacy Service"
username: "legacy-service"
hashedKey: "<plaintext-password>"
enabled: true
}
) {
plaintextPassword
machineUser {
id
username
}
}
}Pass the plaintext password in hashedKey. The Manager hashes it before storing it and echoes it back once in plaintextPassword.
Managing machine users
Listing
UI: the Machine Users page (see above). Columns: Name, Username, Enabled, Created at, Created by. A token credential has no login name, so its Username cell shows a Token auth chip instead of a value; Basic rows show the username the client sends.
API:
query {
machineUsers(
environment: "environment-uuid"
filters: []
order: { field: "name", direction: ASC }
pagination: { limit: 20, offset: 0 }
) {
id
name
username
enabled
tokenPrefix
createdAt
lastRotatedAt
}
}Filter fields: name, username, enabled, tokenPrefix. Sort fields: name, createdAt, updatedAt. Other names are rejected with "invalid order field".
Enabling and disabling
Disabling stops authentication immediately without deleting the record.
UI: click the pencil icon on the row, switch Enabled, click Save.
API:
mutation {
updateMachineUser(id: "machine-user-uuid", input: { enabled: false }) {
id
enabled
}
}Only name and enabled change after creation. The authentication type is fixed. updateMachineUser refuses hashedKey, tokenPrefix and clearTokenPrefix with an error naming rotateMachineUserToken, and only ever writes an update audit row. Changing a credential goes through Rotating a credential.
Deleting
UI: click the trash icon on the row and confirm.
API:
mutation {
deleteMachineUser(id: "machine-user-uuid")
}WARNING
Deleting a machine user immediately breaks every service that authenticates with it.
Deletion is refused once the credential has served ESB traffic
The ESB audit trail is append-only (every ESBAuditLog delete is refused, with none of the retention-sweep exemption AuditLog has) and an ESB row carries a NOT NULL reference to the machine user that served it. So a machine user that has handled even one logged ESB request cannot be deleted at all, and the refusal says so and names the count:
cannot delete machine user: it still has 3 recorded ESB request(s); the ESB
audit trail is append-only and cannot be cleared, so this machine user cannot
be deleted; take the credential out of service instead (updateMachineUser with
enabled: false)enabled: false is a genuine substitute rather than a consolation: the machine-user authentication middleware and both proxy authentication branches filter on enabled = true, so a disabled credential authenticates nowhere.
Endpoint access control
An endpoint assignment says "this machine user may call this endpoint". Each assignment has its own Enabled switch. Assignments are what let a machine user reach an application through the proxy and call /esb/{applicationId}/....
Granting access
UI:
- Open the application, then Endpoints, then the endpoint.
- Click Add Machine User.
- Pick the machine user, leave Enabled on, click Add Access.
The endpoint page lists assigned machine users with an Enabled toggle and a Remove Access button. Managing assignments requires an endpoint-management role on the application.
API:
mutation {
createMachineUser2Endpoint(
input: {
machineUserID: "machine-user-uuid"
endpointID: "endpoint-uuid"
enabled: true
}
) {
id
enabled
}
}Revoking access
UI: switch Enabled off, or click Remove Access.
API:
mutation {
updateMachineUser2Endpoint(id: "relation-uuid", input: { enabled: false }) {
id
enabled
}
}mutation {
deleteMachineUser2Endpoint(id: "relation-uuid")
}Backend registration and trigger callbacks
A backend registers itself so the Manager knows where to send trigger callbacks. Every route in this section takes Authorization: Bearer <machine-user-token>. Replace https://manager.example.com with your Manager URL.
Register
curl -X POST https://manager.example.com/api/machine/register-backend \
-H "Authorization: Bearer <machine-user-token>" \
-H "Content-Type: application/json" \
-d '{
"application_id": "550e8400-e29b-41d4-a716-446655440000",
"callback_url": "https://payment.example.com/triggers/callback",
"version": "1.0.0"
}'Response (201 Created):
{
"success": true,
"registered_id": "backend-uuid",
"token": "generated-bearer-token-for-callbacks",
"message": "Backend registered successfully",
"environment_id": "environment-uuid",
"application_id": "550e8400-e29b-41d4-a716-446655440000"
}application_idmust belong to the same project as the machine user's environment. Otherwise:404"Application not found or does not belong to your project".callback_urlmust be a public URL. Private, loopback and cloud-metadata addresses are rejected with400.tokenis the credential the Manager sends to you on every callback. Store it and check it on incoming requests.- Registering again for the same application returns
200with a newtoken; the previous one is no longer used. Re-register on start-up so a leaked token has a bounded lifetime.
Heartbeat
PUT /api/machine/backend/{registered_id}/heartbeatReturns 200 {"success": true, "registered_id": "...", "message": "Heartbeat recorded"}. A 404 means the registration is gone: register again. A 403 means it belongs to another machine user.
A backend that stops sending heartbeats is marked inactive by a periodic cleanup (cron.backend_heartbeat_timeout, default 1m). The next heartbeat or re-registration reactivates it.
Unregister
DELETE /api/machine/backend/{registered_id}Returns 200 {"success": true, "message": "Backend unregistered"} and deletes the registration. Call it on graceful shutdown.
Check authentication
GET /api/machine/check{
"authenticated": true,
"machine_user_id": "machine-user-uuid",
"environment_id": "environment-uuid"
}List the application's enabled tenants
Triggers fire once per application and carry no tenant. If your backend does per-tenant work, list the tenants and loop over them:
GET /api/machine/application/{application_id}/tenants{
"success": true,
"tenants": [{ "id": "tenant-uuid", "name": "Acme" }]
}The application must belong to your environment's project; otherwise 404.
Receiving trigger callbacks
When a trigger fires, the Manager sends a POST to your callback_url with:
Content-Type: application/jsonAuthorization: Bearer <token from registration>- Body:
{
"trigger_id": "trigger-uuid",
"trigger_name": "Hourly Payment Processing",
"cron_expression": "0 0 * * * *",
"run_key": "hourly-payment",
"executed_at": "2025-12-01T10:00:00Z"
}Check the Authorization header against the token you stored. Reply with any 2xx status within 5 seconds. The Manager ignores the response body and logs anything else as a failed dispatch. See Triggers.
Other /api/machine routes
POST /api/machine/comms/emit and POST /api/machine/comms/publish let a backend send notifications and real-time messages to an application's users. They use the same Bearer authentication.
Rotating a credential
Machine-user Bearer tokens and backend callback tokens do not expire. cron.credential_rotation_max_age (default 2160h, 90 days) is the age at which a credential must be rotated, and a maintenance sweep reports the credentials that reach it. The sweep changes nothing: a credential past the max age keeps authenticating until an operator rotates it.
Each pass of the sweep does two things.
It sets pfy_executor_stale_credentials{kind="machine_user"|"backend_token"}, the number of credentials past the policy. Alert on a sustained non-zero value; it falls back to zero on the pass after the rotation. cron.credential_rotation_max_age: 0 disables the sweep and leaves the gauge untouched rather than zeroing it, so a disabled policy does not read as an all-clear.
It writes one audit row the first time each credential passes the age, with action other, actorType system and metadata.kindcredential_staleness. The row is written once per lapse rather than once per tick, and a rotation makes the next lapse reportable again. The action is other and not expire because the credential still works, and expire is what the credential_expiries_in_period evidence counter reads as a credential that has stopped working.
lastRotatedAt in the machineUsers query tells you when a machine user's token last changed, and is null for one that has never been rotated.
Rotating a machine-user token
rotateMachineUserToken mints a fresh Bearer credential, stores its hash and prefix, stamps lastRotatedAt, writes the rotate audit row in the same transaction, and returns the plaintext once:
mutation {
rotateMachineUserToken(id: "machine-user-uuid") {
generatedToken
machineUser {
id
username
tokenPrefix
lastRotatedAt
}
}
}generatedToken is shown once and cannot be retrieved afterwards, and the old credential stops verifying the moment the mutation commits, so have the service ready to take the new value. The mutation requires the project role maintainer or owner, or the system role administrator, which is the same bar as editing and deleting a machine user. The UI has no button for it; call it through the API.
A token machine user has its username re-derived from the new prefix (Bearer:<prefix>), which also converts a row written under the older bare Bearer spelling. A Basic machine user keeps its username, and generatedToken is its new password.
Rotating a backend callback token
A backend's trigger-callback token rotates when the backend re-registers, which is the only moment the Manager can hand the backend the new value: POST /api/machine/register-backend for the same application and machine user answers 200 with a fresh token. Use the token from the latest registration response. The Go and Node libraries re-register when a heartbeat answers 404.
Troubleshooting
401 {"error": "Authorization header required"}or"Invalid authorization format. Expected: Bearer <token>"- the Manager's own routes accept Bearer only.401 {"error": "Invalid credentials"}- wrong or rotated token, or the machine user is disabled.429- too many failed attempts from this address or for this credential; wait forRetry-After.- The proxy reports "Machine user does not have access to this application" - add an enabled endpoint assignment on that application.
404 Application not found or does not belong to your projecton registration - the application is in a different project than the machine user's environment.400 Invalid callback_url- the URL points at a private or internal address.
Comparison with Personal Access Tokens
| Feature | Machine users | Personal Access Tokens |
|---|---|---|
| Scope | One environment of a project | One user |
| Auth methods | Bearer on Manager routes; Bearer or Basic through the proxy | Bearer only |
| Expiry | None; a sweep reports credentials past the 90-day rotation policy | Optional expiry date |
| Access | Endpoint assignments | Everything the user can do |
| Created by | Project maintainer / owner, or a system administrator | Each user, for themselves |