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-keys404s, 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
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
GETparameter. URLs land in access logs, proxies, and browser history.
- Never pass it as a literal command-line argument — argv is visible via
psand 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/tokeninfoinstead.
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
expdefeats 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):
- Create the replacement key at
/account/api-keys.
- Update the secret in every consumer and confirm it works.
- 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.
- 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 |
Related
- Getting Started
- Device Authorization Flow — the right choice for interactive CLIs
- Client Credentials Flow — the right choice for service identities
- Tokens & Revocation — introspection, revocation, rate limits
- JWT Verification — for JWT access tokens (not keys)
- Errors
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.