Service Accounts
A service account is a non-human principal owned by a human, for scripts, scheduled jobs, and machine-to-machine integrations. It is a rootcx_system.users row with kind = 'service': a random UUID, the email sa+{slug}@localhost, and no password or SSO identity.
A service account is governed by exactly the same RBAC, enforcement, delegation, and worker isolation as every other principal. The only net-new surface is its lifecycle, its credentials, a client-credentials token endpoint, and act-as grants.
Managing service accounts requires the admin:service_accounts.manage permission.
Create
POST /api/v1/service-accounts
{ "slug": "nightly-sync", "displayName": "Nightly Sync Job" }
The slug matches [a-z0-9_-], max 48 characters, and becomes the account's email (sa+nightly-sync@localhost). Returns 201 with the account id.
A service account is born with no permissions (deny-by-default). Grant it a least-privilege role through the standard roles API before it can do anything.
Credentials
A service account authenticates with one or more API keys.
POST /api/v1/service-accounts/{id}/credentials
{ "name": "ci-pipeline", "expiresInDays": 90 }
expiresInDays defaults to 90 and is clamped to 1-365. The response returns the key once:
{
"id": "9c1f...",
"key": "rcs_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"prefix": "rcs_xxxxxxxx",
"expiresAt": "2026-09-04T10:00:00+00:00",
"note": "store this key now; it is shown only once"
}
The key is a 256-bit secret prefixed with rcs_. Only its SHA-256 hash is stored; the plaintext is never persisted and never shown again. An indexed prefix (the first 12 characters) makes lookup fast without weakening the secret.
Revoke a key at any time:
DELETE /api/v1/service-accounts/{id}/credentials/{credId}
Getting a token
Exchange a credential for a short-lived access token using the OAuth 2.0 client-credentials grant (RFC 6749 §4.4):
curl -X POST http://localhost:9100/api/v1/auth/token \
-d grant_type=client_credentials \
-d client_id=<service-account-id> \
-d client_secret=rcs_...
Returns a Bearer token:
{ "access_token": "eyJ...", "token_type": "Bearer", "expires_in": 900 }
The exchange fails if the service account is disabled, has no owner of record, or the credential is wrong, revoked, or expired. Use the token like any other: Authorization: Bearer <access_token>.
Act-as: running work as a service account
A human can run an automation as a service account, so the work is owned and attributed to that account instead of the person. This is ownership, not impersonation. It passes a strict, double-locked gate, with exactly 1 way in and no bypass. See Delegation for the principle.
1. A standing act-as delegation must exist.
POST /api/v1/service-accounts/{id}/act-as
{ "userId": "<human-uuid>" }
This grants the human the right to act as the service account. Revoke it with DELETE on the same path.
2. Anti-escalation: the service account's permissions must be a subset of the human's.
Every time the human invokes act-as, the Core checks that the service account holds nothing the human does not already hold. You can never gain authority by routing through a service account, only narrow or match your own.
A human acting as itself is always allowed and skips both checks.
Lifecycle
| Action | Endpoint | Effect |
|---|---|---|
| Disable | POST /api/v1/service-accounts/{id}/disable |
Cuts access immediately; tokens are refused |
| Enable | POST /api/v1/service-accounts/{id}/enable |
Restores access |
| Transfer ownership | POST /api/v1/service-accounts/{id}/transfer-ownership |
Reassigns the owner; the new owner must be a human |
| Delete | DELETE /api/v1/service-accounts/{id} |
Revokes standing delegations, cascades credentials, removes the principal |
Disabling is immediate because the check runs live against the database on every request. There is no token to wait out.
API endpoints summary
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/service-accounts |
List service accounts |
| POST | /api/v1/service-accounts |
Create a service account |
| DELETE | /api/v1/service-accounts/{id} |
Delete a service account |
| POST | /api/v1/service-accounts/{id}/disable |
Disable |
| POST | /api/v1/service-accounts/{id}/enable |
Enable |
| POST | /api/v1/service-accounts/{id}/credentials |
Mint a credential (returned once) |
| DELETE | /api/v1/service-accounts/{id}/credentials/{credId} |
Revoke a credential |
| POST | /api/v1/service-accounts/{id}/transfer-ownership |
Transfer ownership to a human |
| POST | /api/v1/service-accounts/{id}/act-as |
Grant a human the right to act as the account |
| DELETE | /api/v1/service-accounts/{id}/act-as |
Revoke an act-as grant |
| POST | /api/v1/auth/token |
Client-credentials token exchange |
All management endpoints require admin:service_accounts.manage.