Service Account / Organization Tokens
Long-lived, organization-bound credentials for CI/CD, IaC, and automation with capability guardrails.
Service Account / Organization Tokens
Overview
Service account tokens are long-lived API credentials bound to a team organization, not to a person. Use them to give machine clients (CI pipelines, IaC tools, the Kubernetes operator) stable access to your organization without tying that access to any individual's account.
Key properties that distinguish them from personal API tokens:
| Property | Personal API token | Service account token |
|---|---|---|
| Bound to | The user who created it | A team organization |
| Org context | Resolves via standard rules | Pinned at creation — never changes |
| Capabilities | All capabilities the user holds | Restricted to automation-safe presets (see guardrail) |
| Lifecycle | Tied to the user's membership | Independent — survives member changes |
| Oversight | User's own settings | Owner-visible in Token Oversight |
When to Use Service Tokens
Prefer a service account token over a personal token whenever:
- A machine client (Terraform, Ansible, a Kubernetes operator, a CI job) needs access to the organization.
- You want access to survive team changes — a token tied to an individual breaks when that person leaves.
- You need a constrained credential: service tokens cannot gain SSO, member-management, billing, or token-oversight capabilities, no matter who creates them.
Personal tokens remain appropriate for interactive use (scripts you run locally, REST API exploration) where the action should be attributed to you.
Minting a Token
Via the UI
- Open your organization and go to Workspace settings → API Tokens.
- Under the Service tokens section, click Create service token.
- Give the token a descriptive name (e.g.
terraform-prod,kubediscovery-cluster-a). - Select a preset — see Presets below.
- Optionally set an expiry date.
- Click Create and copy the token immediately — it is shown only once.
Via the API
Requires service_token.manage (Owner or Admin).
curl -X POST https://app.uptimehunt.io/api/v1/orgs/acme/tokens \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{
"name": "terraform-prod",
"preset": "automation"
}'Response (201 Created):
{
"data": {
"id": 42,
"name": "terraform-prod",
"prefix": "uh_f3a2c1b4",
"token": "uh_f3a2c1b4xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"capabilities": [
"org.view",
"service.view", "service.create", "service.edit", "service.delete",
"project.view", "project.create", "project.edit", "project.delete",
"integration.view", "integration.create", "integration.edit", "integration.delete",
"alert_rule.view", "alert_rule.create", "alert_rule.edit", "alert_rule.delete",
"expectations.view", "expectations.edit",
"incident.view", "incident.ack"
],
"kind": "service",
"owner": null,
"created_by": {"id": 1, "email": "admin@acme.example"},
"status": "active",
"enabled": true,
"created": "2026-06-13T12:00:00Z",
"expires_at": null,
"revoked_at": null,
"revoked_by": null,
"last_used": null
}
}Copy the token now
The token field appears only in this creation response. Once you navigate away or the request completes, the raw value is gone — only its SHA-256 hash is stored. If you lose it, create a new token and revoke the old one.
Request fields:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Display name for the token |
preset | string | Yes | read-only or automation — see Presets |
expires_at | ISO 8601 | No | Optional expiry date; omit for a non-expiring token |
Token Presets
Presets are named, immutable bundles of capabilities. The available presets enforce the guardrail — no preset can grant administrative capabilities.
read-only
All *.view capabilities. Use for monitoring pipelines, dashboards, read-only reporting, and status checks.
| Capability | Description |
|---|---|
org.view | View organization profile |
service.view | List and read services |
project.view | List and read projects |
integration.view | List integrations (never credential values) |
alert_rule.view | List alert rules |
expectations.view | Read service expectations |
incident.view | Read incidents |
automation
Reads and full CRUD on the monitoring inventory. Use for Terraform, Ansible, and the Kubernetes auto-discovery operator when they need to create or update checks.
Adds to read-only:
| Capability | Description |
|---|---|
service.create, service.edit, service.delete | Full service management |
project.create, project.edit, project.delete | Full project management |
integration.create, integration.edit, integration.delete | Full integration management |
alert_rule.create, alert_rule.edit, alert_rule.delete | Full alert rule management |
expectations.edit | Write service expectations |
incident.ack | Acknowledge incidents |
What a Token Can Never Be Granted
The capability guardrail is enforced unconditionally — it applies regardless of the creating user's own role. Even an Owner cannot create a service token with these capabilities:
| Blocked capability | Why |
|---|---|
member.* | Adding, removing, or role-changing members must be a human action, attributed to a person |
sso.* | SSO configuration (including enforcing SSO, which revokes all member sessions) is Owner-only and human-gated |
token.inventory_view, token.revoke | A token that can view or revoke other tokens would be a privilege-escalation vector |
service_token.manage | A token cannot mint or revoke other service tokens — no privilege bootstrap |
audit.view | Audit log access is not grantable to service tokens — tokens are restricted to monitoring resources only |
org.billing | Plan and billing management requires human sign-off |
org.edit, org.delete, org.convert | Org lifecycle operations are human-controlled |
The complete never-grantable set (14 capabilities) is everything not in the 21-capability grantable allowlist. The API returns 400 if a request specifies a blocked capability directly; using a preset makes this enforcement invisible in practice.
No-escalation rule
Even for grantable capabilities, the minter cannot grant more than they hold in the target organization. An Admin who lacks integration.create (for example, because they are in a custom role) cannot mint a token with integration.create, even if the automation preset includes it. The backend resolves the intersection of the preset and the minter's own capabilities at mint time.
Organization Binding
Every service token is pinned to exactly one organization at creation time:
- Requests authenticated with the token automatically resolve to its bound organization.
- Sending
X-Org-Slugto the same org is redundant but accepted. - Sending
X-Org-Slugto a different org returns403— even if the token's owner is a member there. The token sees exactly one org. - Machine clients that use service tokens never need to send
X-Org-Slugand should not, for clarity.
This is why service tokens are the right credential for headless clients: the org context is stable, predictable, and cannot be accidentally or maliciously redirected.
Raw Value Shown Once — Rotation
UptimeHunt stores only the SHA-256 hash of each token value. The plaintext appears exactly once — in the creation response (API) or in the creation dialog (UI). After that it is unrecoverable.
Rotating via the UI
Click the Rotate button on the token row. A confirmation dialog warns that the current value stops working immediately. After confirmation, a new secret is displayed exactly once — copy it before closing the dialog.
Rotating via the API
POST /api/v1/orgs/{slug}/tokens/{id}/rotate performs an atomic swap: a new secret is minted with the same name, capabilities, and expiry as the original, and the old token is revoked in the same database transaction. There is no window during which both values are simultaneously active — the old secret is invalid the moment the response is returned.
Requires service_token.manage. Only active service tokens can be rotated; rotating a personal token or a revoked/expired/disabled service token returns 400.
curl -X POST https://app.uptimehunt.io/api/v1/orgs/acme/tokens/42/rotate \
-H "Authorization: Bearer {access_token}"Response (201 Created): same shape as the creation response, with token carrying the new plaintext uh_… value (shown once).
Update your consumer before the response is gone
The new token value in the rotate response is the only time the plaintext is available. Update your Kubernetes Secret, CI variable, Terraform state, or Ansible vault immediately — the endpoint does not re-serve it.
Recommended rotation workflow:
- Call the rotate endpoint and capture the new token value.
- Update your consumer (Kubernetes Secret, CI variable, Terraform state, Ansible vault) to use the new value.
- Verify the consumer is running cleanly.
Using Service Tokens
Kubernetes Auto-Discovery
The operator requires the automation preset regardless of discovery.mode. In annotation-only mode the reconciler still calls UpsertCheck for every Ingress that passes monitoring criteria — it only narrows which Ingresses qualify, it does not skip check creation. Use automation for both modes.
Mint a token:
- UI: Workspace settings → API Tokens → Service tokens section → Create service token → preset
automation - API:
POST /api/v1/orgs/acme/tokenswith"preset": "automation"
Store it as a Kubernetes Secret and reference it from the Helm values:
saas:
url: https://app.uptimehunt.io/api/v1
existingSecret: uptimehunt-token # Kubernetes Secret name
existingSecretKey: token # key inside the Secret
discovery:
mode: all
watch:
scope: cluster# Create the Secret first
kubectl create secret generic uptimehunt-token \
--namespace uptimehunt-kubediscovery \
--from-literal=token="uh_f3a2c1b4xxxx..."
helm install uptimehunt-kubediscovery \
oci://ohcr.io/uptimehunt/autodiscovery/kubernetes/charts/uptimehunt-kubediscovery \
--version 0.1.0 \
--namespace uptimehunt-kubediscovery --create-namespace \
-f values.yamlPrefer `existingSecret` over inlining the token
Using saas.token: in a plain values.yaml embeds the secret in a file that may end up in version control. existingSecret keeps the token out of Git and lets you rotate it by patching the Secret without touching the Helm release.
See Kubernetes Auto-Discovery for the full operator guide.
Terraform (uptimehunt_org_token)
Authorization header difference
The Terraform provider sends Authorization: Token uh_… (Django REST Framework's TokenAuthentication format), while the Kubernetes auto-discovery operator and Ansible collection send Authorization: Bearer uh_…. Both forms are accepted by the UptimeHunt API — the choice of prefix is provider-specific and does not affect the token's capabilities or behavior.
The uptimehunt_org_token resource mints a service account token from Terraform itself. This is useful when your Terraform configuration is your organization's source of truth for service tokens.
resource "uptimehunt_org_token" "ci_automation" {
organization = "acme"
name = "ci-automation"
preset = "automation"
}
# Use the token value — available only at creation, stored in state
output "ci_automation_token" {
value = uptimehunt_org_token.ci_automation.token
sensitive = true
}Token value in Terraform state
Terraform stores the token in its state file. Treat the state as a secret: use a remote backend with encryption at rest (S3 + SSE, Terraform Cloud, etc.) and restrict access to the state file.
The provider authenticates to UptimeHunt using a separate management credential (typically a personal token or another service token with automation preset). The uptimehunt_org_token resource then creates the service token and exposes its value for use by other resources or outputs.
For CI pipelines, pull the token from state or the Terraform output and pass it as a secret:
# Retrieve the token after apply
terraform output -raw ci_automation_tokenAnsible (organization_token module)
Use the uptimehunt.uptimehunt.organization_token module from the Ansible collection to manage service tokens declaratively.
- name: Ensure service account token exists
uptimehunt.uptimehunt.organization_token:
api_token: "{{ uptimehunt_api_token }}"
organization: acme
name: ansible-automation
preset: automation
state: present
register: org_token
# The secret value is returned only on the initial creation run.
# Store it immediately — subsequent runs return the token metadata
# without the secret (it was already hashed server-side).
- name: Write token to vault (first-run only)
when: org_token.changed and org_token.secret is not none
ansible.builtin.copy:
content: "{{ org_token.secret }}"
dest: /run/secrets/uptimehunt-token
mode: "0600"To revoke a token:
- name: Revoke service account token
uptimehunt.uptimehunt.organization_token:
api_token: "{{ uptimehunt_api_token }}"
organization: acme
name: ansible-automation
state: revokedThe module is idempotent: state: present on a token that already exists returns the metadata without the value (since the server no longer stores it) and sets changed: false.
Token Oversight
Organization Owners can see all service account tokens in the org — alongside members' personal tokens — via Token Oversight.
UI: Organization → Workspace settings → API Tokens.
API:
# List active service tokens
curl "https://app.uptimehunt.io/api/v1/orgs/acme/tokens?status=active" \
-H "Authorization: Bearer {access_token}"Each entry includes the token's name, prefix (for correlation with values.yaml or Terraform state), capabilities, created, and last_used. Revocation takes effect immediately and is recorded in the Audit Log as token.revoked.
Related Documentation
- Token Oversight — reviewing and revoking all org tokens as an Owner
- Roles & Permissions — the capability model service tokens draw from
- Audit Log — token creation and revocation events
- Kubernetes Auto-Discovery — operator installation and configuration
- API Authentication — Organization Context