Skip to content

API keys

Service-account API keys are the credential type for server-to-server integrations. Each service account (SA) carries a fixed role + scope and owns exactly one API key at a time (D83). Issuing, rotating, and disabling SAs is an admin- or owner-only operation — operators cannot manage key material. If you only need to mint a key, the admin UI flow is documented at Rotate API keys (frontend route: /settings/service-accounts per Phase 6); this page covers what you need to know as the developer consuming the key.

What the SA key looks like

When an SA is created (POST /api/v1/service-accounts), the response contains the full key exactly once. After that response, only the key_prefix is readable; the full key is irretrievable.

{
"service_account": {
"id": "a1b2c3d4-1234-5678-9abc-def012345678",
"name": "Slack SOC bridge",
"role": "viewer",
"created_at": "2026-05-22T10:00:00Z"
},
"api_key": {
"full_key": "sa_<one-time secret>",
"key_prefix": "sa_...",
"warning": "Store this key immediately. It will never be shown again."
}
}

Use the full key in Authorization: Bearer sa_... on every API call. The sa_ prefix is what the cloud’s middleware matches to route the bearer token through APIKeyAuth (one-by-one bcrypt verify) instead of the standard JWT path; without sa_ the gateway treats the token as a JWT.

List service accounts and inspect prefixes

GET /api/v1/service-accounts (admin+) returns the org’s SAs with their key prefix only:

Terminal window
curl -H "Authorization: Bearer sa_<one-time secret>" \
https://novavms.novalien.com/api/v1/service-accounts

Expected response:

{
"data": [
{
"id": "a1b2c3d4-1234-5678-9abc-def012345678",
"name": "Slack SOC bridge",
"role": "viewer",
"key_prefix": "sa_...",
"created_at": "2026-05-22T10:00:00Z",
"last_used_at": "2026-05-22T13:59:42Z",
"disabled": false
}
]
}

The single-SA shape is at GET /api/v1/service-accounts/{id}.

Rotate a key (admin+)

POST /api/v1/service-accounts/{id}/rotate-key invalidates the current key and returns a new one in the response body (again, shown exactly once). Use the rolling-cutover pattern: the old key keeps working until the rotate call commits — there is no overlap window beyond the moment of rotation, so push the new key to the integration in the same deploy that consumes the rotated value, or stage a brief blue/green.

Terminal window
curl -X POST -H "Authorization: Bearer sa_<one-time secret>" \
https://novavms.novalien.com/api/v1/service-accounts/a1b2c3d4-1234-5678-9abc-def012345678/rotate-key

Response:

{
"service_account": { "id": "a1b2c3d4-...", "name": "Slack SOC bridge" },
"api_key": {
"full_key": "sa_<new one-time secret>",
"key_prefix": "sa_...",
"warning": "Store this key immediately."
}
}

Disable / re-enable a service account (admin+)

DELETE /api/v1/service-accounts/{id} soft-disables the SA. The key returns 401 UNAUTHORIZED on every subsequent request; the row remains in audit_log for compliance.

Terminal window
curl -X DELETE -H "Authorization: Bearer sa_<one-time secret>" \
https://novavms.novalien.com/api/v1/service-accounts/a1b2c3d4-1234-5678-9abc-def012345678

To re-enable an SA, use the admin UI at /settings/service-accounts; the API does not expose an explicit enable endpoint in v1.

Site access (site-scoped SAs)

If the SA’s role is viewer, you can constrain it to a subset of sites:

  • GET /api/v1/service-accounts/{id}/site-access returns the list.
  • PUT /api/v1/service-accounts/{id}/site-access (admin+) sets the list. Empty array means “no sites” (the SA can authenticate but every site-scoped read returns 404).
Terminal window
curl -X PUT -H "Authorization: Bearer sa_<one-time secret>" \
-H "Content-Type: application/json" \
-d '{"site_ids":["b5e9f3a1-2c4d-4e6f-8a1b-3c5d7e9f1a2b"]}' \
https://novavms.novalien.com/api/v1/service-accounts/a1b2c3d4-1234-5678-9abc-def012345678/site-access

For non-viewer roles the site-access list is ignored at evaluation time (admin, operator, owner are not site-scoped per D78 / D81).

Per-SA rate limit

SAs share an org-wide bucket of 600 requests per minute (set in cloud/cmd/server/main.go via authmw.SARateLimit(600)). Human (JWT) callers are not subject to this bucket. There is no per-key override in v1 — see Rate limits for the full quota story and how to handle 429 RATE_LIMITED.