Access keys
An access key is a long-lived credential pair (keyId + secret) attached to either a human user or a service account. CI pipelines, daemons, and SDK calls exchange the pair for short-lived bearer JWTs at POST /v1/auth/access-key/exchange — see API authentication.
This page covers minting, listing, revoking, and deleting access keys. For the signing recipe and how to use one to call the rest of the API, see Authentication.
The secret is returned exactly once, on create. Huudis stores only a bcrypt hash. If you lose it, revoke the key and mint a new one — there is no fetch-secret endpoint.
Endpoints
| Method | Path | Purpose |
|---|---|---|
POST |
/v1/iam/access-keys |
Mint a new key for a user or service account |
GET |
/v1/iam/access-keys?principalType=&principalId= |
List keys for a principal |
POST |
/v1/iam/access-keys/:id/revoke |
Disable without deleting (preserves audit trail) |
DELETE |
/v1/iam/access-keys/:id |
Hard delete |
All endpoints require role owner or admin. Members cannot mint keys, including for themselves.
Create an access key
POST /v1/iam/access-keys
Request body
| Field | Type | Required | Description |
|---|---|---|---|
principalType |
user | service_account |
yes | Which kind of identity the key authenticates as. |
principalId |
string | yes | The usr_… or svc_… to bind the key to. Must belong to the active workspace. |
label |
string (1–120) | no | Human-readable hint (Production CI, Local dev). |
expiresAt |
ISO 8601 datetime | no | Auto-expire timestamp. Omit for a non-expiring key. Recommended: set 90 days out for service-account keys; rotate via the list + delete-old pattern. |
Response — 201 Created
{
"data": {
"accessKeyId": "AKIA0123456789ABCDEF",
"secret": "sk_huudis_eX9...redacted...3kQ",
"principalType": "service_account",
"principalId": "svc_01KPG30…",
"label": "Production CI",
"createdAt": "2026-05-12T22:42:00.000Z",
"expiresAt": null
}
}
accessKeyId is the public identifier; secret is shown exactly once. Both go into the calling environment as HUUDIS_KEY_ID and HUUDIS_KEY_SECRET.
Errors
| Status | error.code |
When |
|---|---|---|
400 |
PRINCIPAL_NOT_FOUND |
principalId doesn't resolve to a user or service account in this workspace. |
400 |
WRONG_TYPE |
principalType doesn't match the actual principal type. |
403 |
FORBIDDEN |
Caller lacks the role to mint keys. |
List access keys
GET /v1/iam/access-keys?principalType={user|service_account}&principalId={id}
Returns the keys bound to one principal. Both query params are required — there is no instance-wide list to keep the surface narrow.
Response
{
"data": [
{
"id": "ak_01KPG…",
"accessKeyId": "AKIA0123456789ABCDEF",
"label": "Production CI",
"disabled": false,
"lastUsedAt": "2026-05-12T22:41:55.000Z",
"createdAt": "2026-04-30T11:00:00.000Z",
"expiresAt": "2026-07-30T11:00:00.000Z"
}
]
}
lastUsedAt is updated on every successful exchange. A null value means the key has never been used — useful for cleaning up "I minted this and forgot" entries.
Revoke an access key
POST /v1/iam/access-keys/:id/revoke
Soft-disables the key. The row is retained for audit purposes (disabled: true); subsequent /auth/access-key/exchange calls return INVALID_CREDENTIALS. Use this when you suspect a leak but want to keep history.
200 OK returns { id, disabled: true }.
Delete an access key
DELETE /v1/iam/access-keys/:id
Hard-deletes the row. Use sparingly — revocation usually suffices and preserves the audit trail. The audit log entry for the original create still exists; only the key row itself is gone.
204 No Content.
The access key object
| Field | Type | Description |
|---|---|---|
id |
string (ak_…) |
Internal row ID, used for revoke and delete. |
accessKeyId |
string (AKIA…) |
Public identifier sent on every signed request. 20 characters, base32-ish for readability. |
principalType |
string | user or service_account. |
principalId |
string | The bound usr_… or svc_…. |
label |
string | null | Operator-provided hint. |
disabled |
boolean | true after revoke. Disabled keys still exist; deleted ones don't. |
lastUsedAt |
ISO 8601 | null | Updated on every successful token exchange. |
createdAt |
ISO 8601 | Mint timestamp. |
expiresAt |
ISO 8601 | null | Auto-disable time. null means non-expiring. |
The secret field appears only on the create response.
Rotation playbook
Keys don't auto-rotate — Huudis leaves the cadence to you. The recommended pattern:
- Mint a new key with
expiresAt90 days out. - Update your CI / daemon's secret store with the new pair.
- Verify the new key works (a single
GET /v1/accountis enough). - Revoke the old key (don't delete — you want the audit trail).
- Delete after 30 days of confirmed non-use.
The SDKs (@forjio/huudis-node, huudis on PyPI, huudis-go) accept keyId + secret directly and do the JWT exchange transparently — no code change needed when rotating.
Events
| Event type | Fires on |
|---|---|
huudis.iam.access_key.created.v1 |
POST /v1/iam/access-keys succeeds. Reserved — not yet emitted in v1. |
huudis.iam.access_key.revoked.v1 |
A key is disabled. Reserved — not yet emitted. |
The audit log carries the same data — subscribe to /v1/account/audit until these events ship.
Next
- API authentication — how to exchange a key pair for a bearer token.
- Service accounts — non-human principals that hold keys.
- Policies — what the key's principal can actually do.