Skip to content

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 production environment of project shop). 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 ​

RouteBearerBasic
Manager /api/machine/* (backend registration, heartbeat, tenant list, comms)yesno
Manager /esb/{applicationId}/... (ESB endpoint calls)yesno
Applications behind the proxy (the proxy validates through POST /api/validate-proxy-auth)yesyes

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 ​

  1. Open your project, then open a tenant. In the Tenant section of the sidebar, click Machine Users.
  2. 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.
  3. Click Create Machine User.
  4. 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.
  5. Click Save.
  6. 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 ​

graphql
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 ​

graphql
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:

graphql
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:

graphql
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:

graphql
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:

  1. Open the application, then Endpoints, then the endpoint.
  2. Click Add Machine User.
  3. 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:

graphql
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:

graphql
mutation {
  updateMachineUser2Endpoint(id: "relation-uuid", input: { enabled: false }) {
    id
    enabled
  }
}
graphql
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 ​

bash
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):

json
{
  "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_id must belong to the same project as the machine user's environment. Otherwise: 404 "Application not found or does not belong to your project".
  • callback_url must be a public URL. Private, loopback and cloud-metadata addresses are rejected with 400.
  • token is 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 200 with a new token; 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}/heartbeat

Returns 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
json
{
  "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
json
{
  "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/json
  • Authorization: Bearer <token from registration>
  • Body:
json
{
  "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:

graphql
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 for Retry-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 project on 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 ​

FeatureMachine usersPersonal Access Tokens
ScopeOne environment of a projectOne user
Auth methodsBearer on Manager routes; Bearer or Basic through the proxyBearer only
ExpiryNone; a sweep reports credentials past the 90-day rotation policyOptional expiry date
AccessEndpoint assignmentsEverything the user can do
Created byProject maintainer / owner, or a system administratorEach user, for themselves