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.

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

  1. Mint a new key with expiresAt 90 days out.
  2. Update your CI / daemon's secret store with the new pair.
  3. Verify the new key works (a single GET /v1/account is enough).
  4. Revoke the old key (don't delete — you want the audit trail).
  5. 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