API KEY AUTHENTICATION
API keys authenticate CLI commands and programmatic API requests. There are two types: personal keys and collective tokens.
Key formats
| Type | Prefix | Body | Example |
|---|---|---|---|
| Personal key | swamp_ |
64 letters (A–Z, a–z) |
swamp_aBcDeFgHiJkL... |
| Collective token | swamp_org_ |
64 lowercase hex characters (32 bytes) | swamp_org_3f9ac1e07b2d... |
Authentication headers
API requests accept either header:
x-api-key: swamp_a1b2c3...Authorization: Bearer swamp_a1b2c3...The CLI sends Authorization: Bearer when the user is logged in. The
x-api-key header remains accepted for backwards compatibility and is still
used internally on specific per-call paths (e.g. extension push, yank). When
both are present, Authorization takes precedence.
Device identity
In addition to the auth header, the CLI sends Swamp-Distinct-Id on every
request to /api/*:
Swamp-Distinct-Id: c2f3eae0-d758-4174-9e8d-7b3f9a000001The value is a per-device UUID lazy-created and stored by the CLI at
~/.config/swamp/identity.json. It enables stable per-device attribution for
telemetry on requests that carry no API key — without it the server falls back
to a per-request anon_* hash that changes on every call from the same machine.
The header is untrusted:
- It never authenticates or authorizes a request.
- It never resolves to a user record server-side.
- A session or API key always wins over the header when computing the
distinct_idfor an event. - The server validates the value as a canonical UUID and falls back to the
anon_*IP+UA hash on any other input.
Credential storage
The CLI stores credentials in auth.json in the
Swamp config directory:
$SWAMP_CONFIG_DIR, $SWAMP_HOME/config, $XDG_CONFIG_HOME/swamp, or
~/.config/swamp, the first that applies. File permissions are set to 0o600.
Writes are atomic (temporary file, then rename) to prevent corruption.
The stored structure contains:
| Field | Description |
|---|---|
serverUrl |
Server the key authenticates against |
apiKey |
The raw API key (swamp_ prefix) |
apiKeyId |
Key ID for revocation tracking |
username |
Authenticated username |
collectives |
Cached collective membership slugs |
apiKeyFingerprint |
Fingerprint of the SWAMP_API_KEY value the cached identity belongs to |
When SWAMP_API_KEY is set, the CLI caches the identity it resolved in
auth.json and keys the cache on apiKeyFingerprint. A different key, or a
file written by an older CLI that stored the key's first 12 characters instead,
reads as stale and the identity is fetched again.
Verification state
The CLI also keeps the state of its
account check in the config
directory. Each file has permissions 0o600. swamp auth logout deletes all of
them.
| File | Contents |
|---|---|
auth_verified.json |
The signed proof from swamp-club's last verified answer |
auth_fail_open.json |
When swamp-club began returning server errors, if it is |
auth_token_check.json |
When the SWAMP_SIGNIN_TOKEN key was last checked live |
auth_refresh_attempt.json |
When the CLI last tried to refresh auth_verified.json |
Credential precedence
--club-api-key-file(swamp serve,swamp serve check-configandswamp serve daemon enableonly)SWAMP_API_KEY_FILEenvironment variableSWAMP_API_KEYenvironment variableauth.jsonin the Swamp config directory
SWAMP_API_KEY_FILE and SWAMP_API_KEY are mutually exclusive: with both set
and no --club-api-key-file, every command fails with
SWAMP_API_KEY and SWAMP_API_KEY_FILE are mutually exclusive — unset one of them.
A key file is read on every command and trimmed of whitespace; a missing,
unreadable or empty file is an error.
The server is SWAMP_CLUB_URL if set, otherwise the serverUrl in auth.json,
otherwise https://swamp-club.com.
When a collective key is set from any of the first three sources,
swamp auth login and swamp auth logout are disabled.
Environment variables
| Variable | Purpose |
|---|---|
SWAMP_API_KEY |
API key for authentication (overrides auth.json) |
SWAMP_API_KEY_FILE |
Path to a file holding the API key; mutually exclusive with SWAMP_API_KEY |
SWAMP_SIGNIN_TOKEN |
Signin token issued with the collective token in SWAMP_API_KEY |
SWAMP_CLUB_URL |
Server URL override |
SWAMP_CONFIG_DIR |
Directory holding auth.json and the verification state |
SWAMP_SIGNIN_TOKEN is used only when it was issued for the active API key; a
signin token for any other key is ignored. It has no file variant.
Scope
Personal keys create a full user session. The authenticated user's identity, roles, and collective memberships are all available. Most CLI commands that require authentication work with personal keys.
Collective tokens create a collective context only. They do not create a
user session. The middleware sets collectiveTokenContext with the collective
ID, slug, and token ID. Actions performed with a collective token are attributed
to the collective, not to any individual user. Some commands — such as
swamp extension push — accept collective tokens for CI/CD use cases where no
individual operative is involved.
Personal keys grant access to all collectives the user belongs to. Collective tokens are scoped to a single collective.
Collective token scopes
| Scope | Grants access to |
|---|---|
extensions:push |
Push extensions on behalf of the collective, and make the collective's extensions public |
extensions:promote |
Promote extension versions |
extensions:read |
Read extension metadata |
extensions:lifecycle |
Yank, unyank, deprecate, and undeprecate extensions |
extensions:* |
All extension operations |
lab:read |
Read lab issues |
lab:write |
Create and update lab issues |
lab:* |
All lab operations |
profile:read |
Read operative profiles |
profile:write |
Update operative profiles |
profile:* |
All profile operations |
collective:read |
Read collective metadata |
collective:write |
Update collective settings |
collective:* |
All collective operations |
billing:manage |
Manage billing and subscriptions |
feed:write |
Submit feed posts |
notifications:read |
Read notifications |
notifications:write |
Mark notifications as read |
notifications:* |
All notification operations |
oauth:manage |
Manage OAuth applications and consents. When set on SWAMP_API_KEY, enables headless OAuth client registration for swamp serve --auth-mode oauth — no browser interaction needed on first boot — and swamp auth oauth-client register. Looks up the names in --admins, --allowed-users and grant files that have no stored record. Also lists and deletes the collective's OAuth clients. See Deploy with Headless OAuth |
serve:* |
Start swamp serve with --auth-mode token or --auth-mode oauth |
datastore:* |
Set up external datastores (S3, GCS, extension-provided backends) |
vault:* |
Create and manage non-local vault configurations |
tokens:mint |
Mint expiring collective tokens through the API. See Minting |
A personal key carries all scopes implicitly. A collective token must include the required scope explicitly; without it the server returns HTTP 403.
Role requirements
| Operation | Required role |
|---|---|
| Create personal key | Any authenticated user |
| Create collective token | Owner or admin |
| Revoke collective token | Owner or admin |
| Delete collective token | Owner or admin |
| Mint collective token | A tokens:mint token |
Limits
Each collective can have a maximum of 25 active API tokens, whoever created them: tokens a person creates and tokens another token mints share the one limit. Revoked and expired tokens do not count toward it. The list shows every active token, plus the 25 revoked or expired tokens used most recently; older inactive tokens are no longer listed.
Minting
A collective token with the tokens:mint scope can create collective tokens
through POST /api/v1/collectives/{slug}/api-tokens, so a service identity can
rotate its own credentials without a person signing in. tokens:mint is never
granted by a wildcard, and only an owner or admin can create a token that holds
it. The server enforces these rules on every minted token:
- Each scope must be one the minting token holds. A request for any other scope
answers
403. - It cannot hold
tokens:mint, so a minted token cannot mint. expiresAtis required, at most 90 days ahead, and no later than the minting token's own expiry. A missing or out-of-range expiry answers422.- It records the token that minted it, shown as Minted by token in the collective's token list. The person who created the minting token is listed as its creator.
A minted token's record is deleted 30 days after it expires, so rotation does not accumulate records without bound.
A minting token can revoke the tokens it minted with
PATCH /api/v1/collectives/{slug}/api-tokens/{tokenId}; any other token answers
404. It cannot list or delete tokens. Revoking or deleting a minting token
also revokes every token it minted.
Expiration
Collective tokens support an optional expiration date, set at creation time in
the Expires field of the collective's token form or as expiresAt in the
API. A token without one does not expire. Expiration is enforced at verification
— an expired token is rejected even if it is still enabled. Minted tokens always
expire; see Minting.
A personal key minted by swamp auth login expires after 90 days without use.
Each request the key authenticates moves its expiry to 90 days later, so a key
in regular use does not expire. Logging in again mints a fresh key and leaves
the previous one to expire once it is no longer used. An expired login key is
deleted. The Expires column of the Access Tokens panel on /u/<username>
shows when each key expires.
Personal keys created from the web UI do not expire. Use one, or a collective token, for automation that may go more than 90 days between requests.
Key lifecycle
- Created — key is generated, raw value shown once, hash stored
- Enabled — key authenticates requests normally
- Revoked — key is disabled and can no longer authenticate; remains visible in the UI
- Deleted — key is permanently removed
Revoked keys cannot be re-enabled.
Security
Keys are hashed with SHA-256 (base64url encoding) before storage. The raw key is returned only at creation time. The full hash is never exposed in API responses or the web UI.
Fingerprints
A key's fingerprint is the first 16 characters of the lowercase hex SHA-256 of the full key. It names a key without revealing it and cannot be used to authenticate. The same value appears in:
- the Fingerprint column of the Access Tokens panel on
/u/<username> - the Fingerprint column of a collective's token list in its settings
swamp auth whoami,swamp auth token createandswamp auth token list- the
fingerprintfield of the collective token API responses
To match a key you hold to its row, see Identify a key.
API endpoints
Personal keys (BetterAuth)
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/auth/api-key/create |
Create a key |
| GET | /api/auth/api-key/list |
List keys |
| POST | /api/auth/api-key/update |
Update a key |
| POST | /api/auth/api-key/delete |
Delete a key |
These endpoints require an active session (cookie or session token).
A personal key can also revoke itself, which is how a CLI discards its own credential:
| Method | Endpoint | Description |
|---|---|---|
| DELETE | /api/v1/me/api-key |
Delete the presenting key |
It authenticates with the key being deleted (x-api-key or
Authorization: Bearer) and takes no body — the key id comes from the
credential, so a key can only delete itself. It answers
200 {"revoked": true, "id": "<keyId>"}, 403 for a collective token or a
browser session, and 401 for a key that no longer exists. Another replica may
keep accepting a revoked key for up to 60 seconds.
Collective tokens
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/collectives/{slug}/api-tokens |
List tokens |
| POST | /api/v1/collectives/{slug}/api-tokens |
Create a token |
| PATCH | /api/v1/collectives/{slug}/api-tokens/{tokenId} |
Revoke a token |
| DELETE | /api/v1/collectives/{slug}/api-tokens/{tokenId} |
Delete a token |
These endpoints require an active session with owner or admin role on the
collective, except that a tokens:mint token may create and revoke tokens as
described under Minting. List and create responses carry each
token's keyPrefix and its fingerprint as fingerprint. The
list returns every active token and the 25 most recently used revoked or expired
ones, each with an expired flag; moreInactive is true when older inactive
tokens were left out. The create response also carries signinToken unless the
server could not sign one; it is never returned again.
Revoke and delete answer 200 {"ok": true}, 403 for a session below owner or
admin, 404 {"error":"Not found"} for an unknown collective, and
404 {"error":"Token not found"} for an unknown token. A token that belongs to
another collective answers as an unknown one.
Collective management
The collective:read and collective:write scopes are honoured by the
collective REST API — reading a collective, its members, its pending
invitations, and its published extensions, plus updating its settings. See
Collective API for the endpoint table, and
/api/v1/openapi.json for the machine-readable description.
Membership writes (invite, remove, change role) accept a session only and cannot be driven by any bearer credential; the reason is on that page.
CLI Commands
swamp auth login
Authenticate with a Swamp server.
| Flag | Description |
|---|---|
--server |
Server URL (env: SWAMP_CLUB_URL) |
--username |
Username or email |
--password |
Password (omit to prompt) |
--no-browser |
Disable browser login, use username/password instead |
Default behavior opens a browser for device-flow authentication.
swamp auth logout
Remove stored authentication credentials. No flags.
swamp auth whoami
Show current authenticated identity. Aliased as swamp auth status. No flags.
Use --json for structured output.
Personal key output:
stack72 (paul@swamp-club.com) on https://swamp-club.com
Fingerprint: 3afc78556734e019
Plan: Team
Collectives:
swamp Team active
system-initiative
stack72 covered
newcomers trial: 13 days left (ends 2026-08-19)
ended-trial trial endedPlan is a roll-up: the strongest plan across every collective you belong to.
It answers "am I paying for anything?" but not "which collective needs
upgrading?" — that is what the per-collective list is for. Billing attaches to a
collective, never to a user, so a private extension push is judged on the
namespace's collective, not on you. The exception is your personal namespace: it
may hold private extensions while any collective you belong to is on a paid plan
or an active trial.
The second column shows a collective's plan, and its billing status when it is paying. A free collective shows no plan; it shows where it stands instead:
| Shown | When |
|---|---|
trial: <n> days left (ends <date>) |
Its trial is running |
covered |
No trial is running and the registry allows it private extensions: a personal namespace covered by another collective you belong to |
trial ended |
Its trial has ended |
| Nothing | None of these |
Three details worth knowing:
- Billing status is shown to owners and admins only. Plain members see the plan and its name but not the raw status, matching the access rules on the collective's billing page. When triaging a report from a member, expect the field to be missing rather than null.
- A paying collective never shows a trial, even if its 30-day clock is still
running. In
--jsonitstrialisnull. - An active trial allows private extensions; an ended one does not. A collective that has ever paid is judged on its plan alone. Cancelling inside the 30-day window does not fall back to the trial.
Fingerprint is the fingerprint of the key in use, whether it
comes from auth.json or SWAMP_API_KEY. --json carries it as fingerprint.
The key id is not shown.
With --json, each collectiveEntitlements entry carries the registry's
privateExtensions verdict for that collective, passed through as the registry
sent it, unknown values included:
{
"collectiveEntitlements": [
{
"slug": "skunk-ape",
"plan": "free",
"planName": "Free",
"subscriptionStatus": null,
"trial": null,
"privateExtensions": "allowed"
}
]
}privateExtensions |
Meaning |
|---|---|
allowed |
The collective may hold private extensions, including a personal namespace covered by another collective |
trial-ended |
Its free trial has ended and it has no paid plan |
paywalled |
It needs a paid plan |
The field is absent when the registry sends none, as an older or self-hosted one
does. swamp extension push --visibility private decides on this verdict; see
Publishing § Private entitlement.
If the server sends no entitlement — a self-hosted or older swamp-club — whoami
falls back to the single Collectives: a, b, c line.
Collective token output:
Collective token: myorg on https://swamp-club.com
Scopes: extensions:push, extensions:read
Fingerprint: a40f6d9d7459f9aa
Collectives: myorgCollective token JSON output (--json):
{
"authenticated": true,
"serverUrl": "https://swamp-club.com",
"collectiveToken": true,
"collectiveSlug": "myorg",
"scopes": ["extensions:push", "extensions:read"],
"fingerprint": "a40f6d9d7459f9aa",
"collectives": ["myorg"]
}The collective token response omits the id, username, email, and name
fields that appear in the personal key response.
swamp auth token create
Create a scoped API token for a collective from the CLI.
| Flag | Description |
|---|---|
--collective <slug> |
Collective to create the token for (required) |
--scopes <comma-separated> |
Scopes to grant, e.g. extensions:push,serve:* (required) |
--name <label> |
Token label; defaults to cli-<hostname>-<timestamp> (optional) |
--ttl <duration> |
Lifetime, sent as expiresAt (optional; required when minting) |
--json |
Output in JSON format |
--ttl takes a positive integer followed by m (minutes), h, d, w, mo
(30 days) or y, e.g. 7d. Without it, a token created from a personal login
never expires.
Runs from a personal login, where only collective owners and admins can create
tokens, or from a collective token holding tokens:mint set as SWAMP_API_KEY.
A token created the second way is a minted token: --ttl is required, and the
server applies the rules under Minting, including the 90-day limit
and the minting token's own expiry. A refused request prints the server's 403
or 422 message unchanged.
--ttl, and creating a token from a collective token, require Swamp
v20261103.162057.0-sha.1ac19410 or later.
The token key is shown once at creation time and cannot be retrieved later. A
Fingerprint: line above it gives the token's fingerprint,
computed locally from the new key; --json carries it as fingerprint. The
output and --json also carry expiresAt and mintedByTokenId. Maximum 25
active tokens per collective.
When swamp-club issues a signin token with the new key, it is printed below the
key under Signin token:, and --json carries it as signinToken. It is shown
once and cannot be retrieved later. Set it as SWAMP_SIGNIN_TOKEN beside
SWAMP_API_KEY.
swamp auth oauth-client register
Register an OAuth client for swamp serve and print its id and secret once, so
recreated instances can reuse it. Requires Swamp
v20261103.225037.0-sha.5911dc9c or later.
| Flag | Description |
|---|---|
--name <name> |
Client name, as --oauth-client-name gives it in swamp serve (required) |
--club-api-key-file <path> |
Path to a file holding the collective API key; overrides SWAMP_API_KEY_FILE and SWAMP_API_KEY |
--oauth-provider <url> |
OAuth authorization server URL; it must be the server that issued the key (default: SWAMP_CLUB_URL, else swamp-club.com) |
--json |
Output in JSON format |
Runs only from a collective token that holds oauth:manage, because a personal
login would register a client no collective lists. The client belongs to that
token's collective. Registration counts against the collective's client limit,
if it has one.
The output looks like this:
OAuth client: prod-api-server
Provider: https://swamp-club.com
Client id: <client-id>
Client secret:
<client-secret>
Start swamp serve with --oauth-client-id (or SWAMP_OAUTH_CLIENT_ID) and the secret in
--oauth-client-secret-file, SWAMP_OAUTH_CLIENT_SECRET_FILE or SWAMP_OAUTH_CLIENT_SECRET.
The secret is shown once and will not be displayed again — store it now.Against a server other than swamp-club.com, the output also says to pass
--oauth-provider to swamp serve, which does not read SWAMP_CLUB_URL.
--json carries clientId, clientName, clientSecret and provider. See
Recreate an instance without registering a new client.
swamp auth token list
List all API tokens for a collective.
| Flag | Description |
|---|---|
--collective <slug> |
Collective to list tokens for (required) |
--json |
Output in JSON format |
Requires personal authentication (swamp auth login). Only collective owners
and admins can list tokens.
The table output shows each token's name, id, status (active, revoked or
expired), fingerprint, scopes, creation date, and last-used
date. Against a server that does not report fingerprints, the key prefix appears
in the fingerprint's place. JSON output includes id, keyPrefix,
fingerprint, status and mintedByTokenId for each token, and a revoked
token appears with enabled: false. The list holds every active token and the
25 most recently used inactive ones; a note says when older inactive tokens are
hidden.
swamp auth token revoke
Disable an API token. Its key stops working, and the token stays in
swamp auth token list as revoked. Requires Swamp
v20261103.162057.0-sha.1ac19410 or later. Older versions delete the token on
revoke, as swamp auth token delete now does.
| Argument | Description |
|---|---|
<token-id> |
UUID of the token to revoke |
| Flag | Description |
|---|---|
--collective <slug> |
Collective the token belongs to (required) |
--json |
Output in JSON format |
Runs from a personal login, where only collective owners and admins can revoke
tokens, or from a collective token holding tokens:mint, which can revoke only
the tokens it minted; any other id answers not found. Revoking a minting token
also revokes every token it minted.
Use swamp auth token list --collective <slug> to find the token id.
swamp auth token delete
Permanently remove an API token. The token disappears from
swamp auth token list and its key stops working. A deleted token cannot be
restored. Requires Swamp v20261103.162057.0-sha.1ac19410 or later.
| Argument | Description |
|---|---|
<token-id> |
UUID of the token to delete |
| Flag | Description |
|---|---|
--collective <slug> |
Collective the token belongs to (required) |
-y, --yes |
Skip the confirmation prompt; -f, --force also work |
--json |
Output in JSON format |
Requires personal authentication (swamp auth login). Only collective owners
and admins can delete tokens. Without a terminal to confirm in, the command
fails unless --yes or --json is given. Deleting a minting token also revokes
every token it minted.
swamp auth server-login
Store a server token for a swamp serve instance — subsequent --server
commands use it automatically.
| Flag | Description |
|---|---|
--server |
Server URL to associate the token with (required unless SWAMP_SERVE_URL is set) |
--token |
Server token in <name>.<secret> format (required for token auth; omit for OAuth/device flow) |
CLI commands with key-type requirements
Every CLI command needs a credential except those listed in Swamp Account Requirement. The commands below additionally require a particular key type, or a scope when run with a collective token.
| Command | Key type |
|---|---|
swamp auth whoami |
Personal or Collective |
swamp auth token create |
Personal or Collective (tokens:mint) |
swamp auth token list |
Personal |
swamp auth token revoke |
Personal or Collective (tokens:mint) |
swamp auth token delete |
Personal |
swamp auth oauth-client register |
Collective (oauth:manage) |
swamp extension push |
Personal or Collective |
swamp extension yank |
Personal |
swamp extension unyank |
Personal |
swamp extension deprecate |
Personal |
swamp extension undeprecate |
Personal |
swamp issue submit |
Personal |
swamp issue ripple |
Personal |
swamp issue get |
Personal |
swamp datastore setup extension |
Personal or Collective (datastore:*) |
swamp vault create <type> |
Personal or Collective (vault:*); local_encryption needs no scope |
swamp serve --auth-mode token |
Personal or Collective (serve:*) |
swamp serve --auth-mode oauth |
Personal or Collective (serve:*, plus oauth:manage to look up a name with no stored record, such as every name on an empty datastore) |
When a collective token is used with a command that requires a specific scope, the server returns HTTP 403 with:
{ "error": "This token requires the <scope> scope" }