API Keys

The Client App Owner or Admin must enable Personal API Key in the app settings, and the app must be approved and active. New and upgraded apps default to disabled; CIMD apps are unavailable. If no apps are available, contact the Owner or open My Apps to enable your app, then retry. Turning the setting off immediately suspends both issuance and existing keys. Re-enabling restores only unexpired, unrevoked keys; suspended keys remain visible and can still be revoked.

A personal API key is an opaque credential (prefix sgk_) that a signed-in user creates for a caller that cannot complete an OAuth flow: a shell script, a CI job, a cron task, or a legacy system with no browser and no place to keep a client secret.

You send it exactly like a bearer token, but it is not a JWT — it carries no claims, cannot be refreshed, and cannot be verified offline. Resource servers ask Signet whether it is still good, which is precisely what makes revocation instant.

When to Use a Key — and When Not To

Situation Use
CI job, cron task, or one-off script acting as you Personal API key
Interactive CLI or headless device where a human can open a browser Device Flow
Backend service acting as itself, no user involved Client Credentials
Anything with a user and a browser Auth Code + PKCE

Reach for a key only when no flow fits. A flow gives you short-lived tokens, offline JWKS verification, and no long-lived secret on disk; a key gives up all three in exchange for working in a one-line curl. Also note a key is tied to your account — when you leave the team, everything using it stops. For anything owned by a team rather than a person, ask your admin for a Client Credentials client instead.

Your deployment may have the feature turned off entirely (PERSONAL_API_KEYS_ENABLED=false). If /account/api-keys 404s, that’s why — ask your admin.

Key Properties

Property Value
Format sgk_ + 52 lowercase base32 characters (56 total). Nothing is encoded in it — it is a random lookup handle.
Shown Once, on the page right after creation. Afterwards only a fragment like sgk_ab12…wxyz is ever shown.
Bound to Exactly one client app, chosen at creation
Scopes The client app’s scopes, read from the app when Signet fills its lookup cache — never a copy stored on the key
Expiry Mandatory, chosen at creation, capped by the deployment (default cap: 90 days). No non-expiring keys.
Quantity Capped per user (default: 10 live keys)
Revocation Immediate — the next verification fails
Verification Online only (/oauth/tokeninfo or /oauth/introspect) — no JWKS, no local validation

Scopes follow the app, not the key. For ordinary tokeninfo/introspect scopes, there is no per-key snapshot: the scopes a verification reports are the ones the client app had when Signet last filled its lookup cache for that key. If an admin widens the app’s scopes, every key bound to it widens too; if they narrow them, your key narrows — but not instantly, so never treat an app’s scope change as an immediate de-scoping of its keys. Pick the app whose scopes are the least privilege your script actually needs.

How It Works

sequenceDiagram participant User participant Signet participant Script participant API as Resource Server User->>Signet: /account/api-keys → create (name, client app, expiry) Signet-->>User: sgk_… (shown once) note over User,Script: Store in a secrets manager / CI secret Script->>API: GET /resource (Authorization: Bearer sgk_…) API->>Signet: GET /oauth/tokeninfo (Bearer sgk_…) Signet-->>API: {active, user_id, scope, token_type: personal_api_key} API-->>Script: 200 OK note over User,Signet: Revoke at /account/api-keys → next verification 401s

Step 1: Create a Key

Go to /account/api-keys → Create key. Three fields:

Field Notes
Name Up to 100 characters. Describe the consumer, e.g. CI deploy, nightly backup — this is what you’ll match against later.
Client App Type to search active client apps. The key belongs to this app and inherits its scopes.
Expiration 3 hours / 1 day / 7 days / 30 days / custom number of days. Every option is capped by the deployment (default 90 days), and presets above that cap are not offered.

The next page shows the full key once. Copy it straight into a secrets manager or CI secret store — the page is sent with Cache-Control: no-store, so you cannot recover it with the back button, and Signet only keeps a hash. Lost it? Revoke and create a new one; there is no “show again”.

Before you leave that page, run its Verify it now block. It hands you a ready-to-paste command carrying your real key, and prints the exact response your key should return — your user_id, your client_id, your scopes, your exp. Matching the two is the only proof you’ll get that the key made it out of the clipboard intact, and it costs one paste.

Choosing an expiry: pick the shortest lifetime you can automate around. A 3-hour key for a one-off migration is far cheaper to get wrong than a 90-day key on a CI runner. If you hit the per-user cap (the list page counts 3 of 10 keys in use), revoke a dead-weight key rather than asking for a higher cap.

What counts against the cap: only live keys — active and not yet expired. Revoked and expired keys stay in your list as a record of what existed, but they release their slot immediately, which is why the row count and the number in the heading routinely disagree. Letting a key expire frees a slot just as surely as revoking it.

Step 2: Use the Key

Send it as a bearer token:

curl -H "Authorization: Bearer $SIGNET_API_KEY" https://api.example.com/resource

In CI, keep it in the secret store and inject it as an environment variable:

# GitHub Actions
- name: Deploy
  env:
    SIGNET_API_KEY: ${{ secrets.SIGNET_API_KEY }}
  run: ./deploy.sh

Handling rules — a key is a long-lived password:

  • Never put it in a URL query string, a redirect, or a GET parameter. URLs land in access logs, proxies, and browser history.
  • Never pass it as a literal command-line argument — argv is visible via ps and lands in shell history. Read it from an env var or a file.
  • Don’t commit it, don’t paste it into an issue, and scrub it from CI logs (mask the variable).
  • One key per consumer. Sharing one key across three scripts means revoking it breaks all three, and Last used tells you nothing about who used it.

Step 3: Verify the Key (Resource Servers)

There is nothing to verify offline. Both endpoints below return token_type: "personal_api_key", which lets you apply distinct policy — e.g. accept keys for a deploy API but reject them on anything OIDC-sensitive.

GET /oauth/tokeninfo

The simplest option: the key authenticates the call, so you need no client credentials.

curl -H "Authorization: Bearer sgk_..." https://your-signet/oauth/tokeninfo
{
  "active": true,
  "user_id": "5f6e...",
  "client_id": "d4c3...",
  "scope": "deploy",
  "exp": 1769472000,
  "iss": "https://your-signet",
  "subject_type": "user",
  "token_type": "personal_api_key"
}

user_id is the human who created the key — treat the call as being made as that user, and check scope before allowing the operation. Note there is no aud field for a key: audience binding (RFC 8707) is a JWT feature, so a key cannot be scoped to one resource server. If your API relies on aud to keep tokens from being replayed against a sibling service, keys are the wrong credential for it.

Every failure — unknown, malformed, revoked, expired, bound to a disabled app, or the feature being switched off — returns the same response:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", resource_metadata="…/.well-known/oauth-protected-resource"

{"error": "invalid_token", "error_description": "Token is invalid or expired"}

That uniformity is deliberate: a scanner must not be able to learn why a key failed. It also means you can’t tell from the response either — see Why did my key stop working below.

POST /oauth/introspect (RFC 7662)

Use this when your resource server is itself a registered Signet client and you want RFC-shaped output:

curl -X POST https://your-signet/oauth/introspect \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=sgk_..."
{
  "active": true,
  "scope": "deploy",
  "client_id": "d4c3...",
  "token_type": "personal_api_key",
  "exp": 1769472000,
  "iat": 1769385600,
  "sub": "5f6e...",
  "username": "alice",
  "iss": "https://your-signet",
  "jti": "key-uuid"
}

An inactive key is simply {"active": false} (never a 4xx), per RFC 7662.

Ownership gate applies here too. By default (INTROSPECTION_REQUIRE_OWNERSHIP=true) you get the full metadata above only when you authenticate as the client app the key is bound to. Any other client sees a bare {"active": true} with everything stripped. So a shared API gateway introspecting keys bound to many different apps will see nothing useful — in that topology use /oauth/tokeninfo instead.

Caching Verdicts

Don’t call Signet once per inbound request if you can help it. A key is the only credential that genuinely needs a round-trip — a JWT access token must be verified offline via JWKS, never by calling these endpoints per request. Both endpoints are rate-limited: /oauth/tokeninfo per IP (600 req/min by default, shared by everything behind your egress IP), /oauth/introspect per authenticated client app (600 req/min each, with a 1200 req/min per-IP ceiling behind it); see Tokens & Revocation. Signet’s own caches protect Signet’s database, not your request budget — a 429 is the signal that a verdict cache is missing on your side.

// Go pseudo-code — cache the verdict, not the key
if v, ok := cache.Get(sha256(key)); ok {
    return v
}
v := callTokenInfo(key)          // 401 → cache a negative verdict too
cache.Set(sha256(key), v, 60*time.Second)
  • Key the cache by a hash of the key, never the key itself, and never log the key.
  • Keep the TTL short — your cache TTL is your revocation delay. 30–60 seconds is a reasonable trade; caching until exp defeats the entire point of an online-verified credential.
  • Cache negative verdicts briefly too, so a misconfigured client’s retry loop doesn’t hammer both you and Signet.

Which Endpoints Accept a Key

Endpoint sgk_ key Behaviour
GET /oauth/tokeninfo Accepted Verification. token_type: personal_api_key
POST /oauth/introspect Accepted Verification, RFC 7662 shape. Requires client auth; ownership gate applies
POST /oauth/revoke Accepted Self-service revocation — holding the key is the authorization. Always 200
POST /oauth/token (any of the four grants) Rejected A key is not a code, a refresh token, or a device code → invalid_grant (access_denied on device_code)
GET /api/v1/me Conditional Management API enabled; explicit per-key account:read grant and current client/resource permission required
GET /oauth/userinfo Rejected OIDC surface, JWT access tokens only → 401 invalid_token
Local JWKS verification Rejected Not a JWT — nothing to verify. Call one of the two endpoints above

Revoking and Rotating

From the UI: /account/api-keys → Revoke. Effective immediately and it cannot be undone; on a multi-node deployment without a shared cache, allow up to a minute for every node to catch up.

From a script — /oauth/revoke accepts a key with no client credentials, because possession of the key is the authorization. Handy for tearing down a short-lived key at the end of a job:

curl -X POST https://your-signet/oauth/revoke \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "token=$SIGNET_API_KEY"

Per RFC 7009 this returns 200 whether or not the key existed, so the response confirms nothing. There is one case where it does not revoke: if the deployment has personal API keys switched off (PERSONAL_API_KEYS_ENABLED=false) the call still answers 200, but the key stays on file and starts working again the moment an operator switches the feature back on. When it matters — a leak, say — confirm at /account/api-keys, and if that page 404s ask your admin to revoke it for you.

Rotation, in this order (never the reverse — revoking first means downtime):

  1. Create the replacement key at /account/api-keys.
  2. Update the secret in every consumer and confirm it works.
  3. Check the old key’s Last used column; if it is still advancing, you missed a consumer. Do this before revoking — a revoked key never updates Last used again, so afterwards the column is frozen and tells you nothing.
  4. Revoke the old key.

If a key leaks, revoke it first and ask questions second. Then create a fresh one — never “un-leak” a key by rotating the client app’s secret, which does nothing for keys.

Why Did My Key Stop Working?

Every one of these produces the same 401 invalid_token, so work down the list at /account/api-keys:

Cause Where you see it Recovery
Past its expiry Status Expired Create a new key
You (or an admin) revoked it Status Revoked Create a new key — revocation is permanent
Your account was disabled You can’t sign in All your keys were revoked. Re-enabling the account does not bring them back
The client app was disabled Key still listed, neither expired nor revoked Nothing to do — keys resume automatically when an admin re-activates the app (allow up to ~1 minute to propagate)
The client app was deleted Key shows as Revoked Permanent. Create a new key against another app
Feature switched off deployment-wide /account/api-keys 404s Ask your admin
Truncated / mangled key Key looks healthy in the list, still 401s Re-copy it — a key is exactly 56 characters, and the displayed hint (sgk_ab12…wxyz) is not usable as a credential
Scope no longer sufficient 403 from the resource server, not 401 The client app’s scopes changed. Ask the app owner

Who else can see your keys: the owner of the client app you bound the key to, and admins, can see that the key exists — its name, hint, expiry, and last-used time, alongside your username and email. Neither can see the key itself. App owners cannot revoke your key; admins can force-revoke it.

Security Checklist

Requirement Details
Prefer a flow Use a key only when no OAuth flow fits — see the table at the top
Shortest workable expiry Days, not months. The deployment cap is a ceiling, not a target
One key per consumer Independent revocation and a meaningful Last used signal
Secrets manager / CI secret Never in source control, argv, URLs, or logs
Least-privilege client app The key inherits the app’s scopes — bind it to the narrowest app that works
Verify online, cache briefly 30–60 s verdict cache; your TTL is your revocation delay
Handle 429 tokeninfo is limited per IP, introspect per client app — honor Retry-After if present, back off with jitter
Revoke on leak, then rotate Immediately, then create → deploy → revoke old
Clean up Revoke keys for retired scripts; don’t let them sit until expiry

Management API permissions

When enabled, /api/v1/me accepts a personal key with the explicit account:read management grant. The creation page offers optional management permissions, saved independently on each key. Existing keys have none. Management scopes are intersected with live client permissions, and administrator operations also check the live administrator role. Management requests bypass ordinary validation caches; revocation, disablement and narrowing apply on the next request. The server binds the management grant to its exact configured resource; this does not add JWT claims or change tokeninfo/introspect scopes. An approved client alone never grants management access.