Service accounts
A service account is a non-human principal in a workspace. It holds access keys and policies just like a user, but it doesn't have an email, can't log into the dashboard, and can't be invited — it only exists as an identity for machine workflows.
Use a service account whenever the actor is software:
- A CI pipeline that calls the Huudis admin API.
- A cron job that rotates secrets in another Forjio product on a schedule.
- A backend daemon that signs S3-style requests on every webhook delivery.
The alternative — pinning credentials to a real human's user — couples your automation to their employment status and tangles the audit log.
All endpoints require a bearer admin JWT — see Authentication.
Endpoints
| Method | Path | Purpose |
|---|---|---|
POST |
/v1/iam/service-accounts |
Create a service account |
GET |
/v1/iam/service-accounts |
List service accounts in the workspace |
GET |
/v1/iam/service-accounts/:id |
Retrieve one |
DELETE |
/v1/iam/service-accounts/:id |
Delete a service account |
Access-key management goes through Access keys with principalType: "service_account".
Create a service account
POST /v1/iam/service-accounts
Request body
| Field | Type | Required | Description |
|---|---|---|---|
name |
string (1–120) | yes | Display name. Unique per workspace. Conventionally describes the workload (Daily Backup Cron). |
description |
string (≤500) | no | Free-form context. |
Response — 201 Created
{
"data": {
"id": "svc_01KPG30TZK…",
"accountId": "acc_01KPG…",
"name": "Daily Backup Cron",
"description": "Runs nightly at 02:00 UTC.",
"createdAt": "2026-05-12T23:45:00.000Z"
}
}
The service account has no credentials at this point. Mint a key via POST /v1/iam/access-keys with principalType: "service_account" and the svc_… ID.
List service accounts
GET /v1/iam/service-accounts
Returns service accounts in the active workspace, newest first.
Retrieve a service account
GET /v1/iam/service-accounts/:id
Plain detail view. To see attached policies, query GET /v1/iam/policy-attachments?principalType=service_account&principalId=svc_….
Delete a service account
DELETE /v1/iam/service-accounts/:id
Removes the row. Cascades:
- All access keys for this principal are deleted (cannot exchange them anymore).
- Every assumed-role session originally issued to this principal is left to expire on its own timer — existing sessions remain valid until then. Revoke explicitly if you want immediate cutoff.
- Policy attachments are deleted.
204 No Content.
The service account object
| Field | Type | Description |
|---|---|---|
id |
string (svc_…) |
Stable ID. |
accountId |
string | Owning workspace. |
name |
string | Display name. Unique per workspace. |
description |
string | null | |
createdAt |
ISO 8601 |
There's no lastUsedAt on the service account itself — usage timestamps live on the individual access keys.
Naming patterns
A few conventions worth picking:
ci-${repo}— one service account per GitHub repo with a deploy/build CI.cron-${workload}— one per scheduled job.webhook-receiver-${product}— one per upstream product that signs incoming HMAC.
Don't share one service account across multiple workloads. Cheap to create, expensive to debug "which CI just rotated this key?" when 5 jobs share the same one.
Policy attachment patterns
Attach the minimum policy. For a daily-backup cron, that's something like:
{
"Version": "2026-01-01",
"Statement": [
{
"Effect": "Allow",
"Action": ["huudis:audit:read", "huudis:end_users:export"],
"Resource": "*"
},
{
"Effect": "Deny",
"Action": "huudis:*:write",
"Resource": "*"
}
]
}
Service accounts have no implicit permissions. Without attached policies they can authenticate (mint a JWT from their access key) but every authz check fails with decision: "Deny".
Service accounts vs. assumed roles
| Service account | Assumed role | |
|---|---|---|
| Credentials live | Long-lived (access key) | Short-lived (1–12 hours) |
| Created via | POST /v1/iam/service-accounts |
POST /v1/authz/assume-role |
| Holds policies | Yes (via attachments) | Yes (via attachments to the role) |
| Trust policy | n/a | Required — gates who can assume |
| Best for | Always-on workloads | Scoped one-off operations |
The typical advanced pattern: a long-lived service account is allowed to assume one or more roles. The service account's own policy contains only sts:AssumeRole permissions; the operational scope lives in the roles. Rotating the access key swaps credentials; rotating the roles swaps capabilities — the two concerns are kept independent.
Events
No events for service-account CRUD. Audit-log entries (iam.service_account.created, iam.service_account.deleted) cover the operations.
Next
- Access keys — the credentials a service account holds.
- Policies — what permissions a service account has.
- Roles — short-lived alternative.