Skip to content

Apps and credentials

This page is the credential reference. For each credential you can actually obtain: how you get it, the exact wire format, how long it lives, what authority it carries, and how to revoke it. Use the app assertion exchange for a third-party backend.

Three values come from the deployment, not from this page:

Placeholder What it is How to get it
$API The Account API origin, e.g. https://api.example.com From the operator
$ISSUER The OIDC issuer, ending in /oauth2 From the operator
$TOKEN_ISSUER The issuer of platform-signed non-OIDC tokens and the required aud of an app assertion https://api.vampikez.fun here; confirm the deployment’s configured JWT_ISSUER elsewhere. Never trust an unverified token to choose it

Every token WAMP signs uses EdDSA (Ed25519). There are no HMAC-signed tokens and no client secrets anywhere in the model.

Credential Represents Format
User access token One human, their whole account surface Authorization: Bearer <jwt>
User refresh token The right to mint the above JSON body field
OIDC access and ID token One human who consented to your client, in one organization Authorization: Bearer <jwt>
App assertion Your app’s publisher identity Request body field assertion
App installation access token Your app acting inside one customer organization Authorization: Bearer <jwt>
End-user AI token One end user your App backend authenticates Authorization: Bearer <jwt>
Terminal window
curl -X POST "$API/auth/login" -H 'Content-Type: application/json' \
-d '{"email":"person@example.com","password":"…"}'
# {"success":true,"user":{…},"accessToken":"…","refreshToken":"…","expiresIn":…}
Property Value
Header Authorization: Bearer <jwt>
JWT header {"alg": "EdDSA", "kid": "…", "typ": "at+jwt"}
Claims iss is $TOKEN_ISSUER, sub is the user id, sid is the opaque session id, plus userId and email
Lifetime 15 minutes by default, set by the operator
Authority User-authenticated management routes, subject to live membership and route-specific tenant-context requirements
Revocation POST /auth/logout deletes the exact refresh session identified by sid, including after access-token refresh. An already-issued access JWT remains valid until expiry

The refresh token is a separate JWT that travels in a JSON body, never a header:

Terminal window
curl -X POST "$API/auth/refresh" -H 'Content-Type: application/json' \
-d '{"refreshToken":"…"}'

It has two independent clocks. An absolute cap is baked into the token — 30 days by default — and a sliding idle window on the session row, 7 days by default, moves forward on every refresh. An active session dies at the absolute cap; an idle one dies one idle window after its last use.

This is the credential the developer-facing routes on $API want. It is not the same thing as an OIDC access token: those carry aud equal to your client_id and are meant for your own resource server, not for $API management routes. Presenting the wrong one gets you 401 {"error": "Invalid or expired token"}.

An app is the publisher-side identity: one row in a publisher organization, with a slug, and one or more public keys. There is no app secret. You generate an Ed25519 keypair, upload the public half, and keep the private half.

Terminal window
openssl genpkey -algorithm ed25519 -out app.key
openssl pkey -in app.key -pubout -out app.pub # SPKI PEM
curl -X POST "$API/api/apps" \
-H "Authorization: Bearer $USER_ACCESS_TOKEN" -H 'Content-Type: application/json' \
-d '{"orgId":"8c1b…","slug":"acme-bot","name":"Acme Bot"}'
curl -X POST "$API/api/apps/acme-bot/keys" \
-H "Authorization: Bearer $USER_ACCESS_TOKEN" -H 'Content-Type: application/json' \
-d '{"publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMCowBQYDK2Vw…\n-----END PUBLIC KEY-----\n"}'
# 201 {"success":true,"key":{"kid":"…","revokedAt":null,"createdAt":"…"}}

Creating an app and registering keys both require organization.applications.manage in the publisher organization. slug is 2–64 characters matching ^[a-z0-9][a-z0-9-]*[a-z0-9]$, and these slugs are reserved and can never be claimed: wamp, wamp-cloud, wamp-ai, wamp-core, wamp-engine, api, app, apps, auth, admin, marketplace.

kid is the RFC 7638 JWK thumbprint of the public key, so you can compute it locally and never have to read it back. publicKeyPem must contain -----BEGIN PUBLIC KEY----- and be ≤4096 characters.

Revoke a key with DELETE /api/apps/:slug/keys/:kid. Revocation is checked live on every exchange, so it bites immediately. Register the replacement key before revoking the old one — an app with no unrevoked key cannot authenticate at all, and a private_key_jwt OAuth client on that app stops working too.

The assertion is a short-lived JWT you sign yourself. It proves publisher identity only — never tenant authority — and it travels in a request body field named assertion, never in a header.

Part Required value
Header alg EdDSA
Header kid A registered, unrevoked kid for this app
iss Your app slug
sub Your app slug — it must equal iss
aud $TOKEN_ISSUER
iat, exp Both required, and exp - iat must be ≤ 600 seconds

Clock tolerance is 30 seconds. A missing or oversized lifetime is its own error (assertion_too_long_lived), distinct from a bad signature (invalid_assertion) and from an unknown app or key (unknown_app_or_key).

This is how your backend acts as itself inside one customer organization. You exchange an assertion plus one installationId plus one exact resource audience for a token scoped to precisely that.

Terminal window
curl -X POST "$API/auth/app-installation-token" \
-H 'Content-Type: application/json' \
-d '{
"assertion": "<jwt you just signed>",
"installationId": "5b7e…",
"resourceAudience": "wamp-ai"
}'
# {"success":true,"token":"<jwt>","expiresIn":600}

The request is strict — those three fields and nothing else. installationId must be a UUID; resourceAudience is 1–128 printable ASCII characters naming one resource server.

Property Value
Header Authorization: Bearer <jwt>
JWT header {"alg": "EdDSA", "kid": "…", "typ": "at+jwt"}
iss $TOKEN_ISSUER
aud Exactly the resourceAudience you asked for — one string, never an array
sub app-installation-access:<installationId>
Claims token_use: "app_installation_access", org_id, installation_id, app_id, resource_installation_id, resource_app_id, resource_server_id, authorization_version, capabilities (sorted, deduplicated), jti, and nbf equal to iat
Lifetime 600 seconds, not configurable
Authority The intersection of the signed capabilities and the live grant graph, re-resolved on every request. The signed array is a ceiling, not a grant
Revocation Revoke the installation, the capability grant, or the app key. Any of the three ends it at the next request

Because the ceiling is re-checked live, you do not need to shorten the TTL to make revocation prompt, and you should not cache authority decisions derived from capabilities.

Failures split by kind: 401 for invalid_assertion, unknown_app_or_key, and assertion_too_long_lived; 403 for app_suspended, installation_not_found, resource_server_not_found, and installation_not_authorized.

An installation is created when a customer organization approves your app. You open a consent handoff with your own assertion, send a human to it, then poll.

Terminal window
# 1. You: open the intent. Requires only your assertion.
curl -X POST "$API/api/apps/installation-intents" \
-H 'Content-Type: application/json' \
-d '{
"assertion": "<jwt>",
"resourceAudience": "wamp-ai",
"capabilityIds": ["wamp.ai.invoke"]
}'
# 201 {"success":true,"intent":{"id":"9a4d…","status":"pending",
# "expiresAt":"…","authorizeUrl":"https://account.example.com/install/9a4d…"}}

Send the customer to authorizeUrl. The unguessable intent id is the invitation, so treat the URL as a secret. A pending intent lives 15 minutes.

Terminal window
# 2. The customer's admin, in their own session, approves it for their org.
curl -X POST "$API/api/apps/installation-intents/9a4d…/authorize" \
-H "Authorization: Bearer $CUSTOMER_ACCESS_TOKEN" \
-H 'Content-Type: application/json' -d '{"orgId":"8c1b…"}'
# 3. You: poll. Reading an authorized intent does not consume it.
curl -X POST "$API/api/apps/installation-intents/9a4d…/status" \
-H 'Content-Type: application/json' -d '{"assertion":"<fresh jwt>"}'
# {"success":true,"intent":{"status":"authorized","installationId":"5b7e…", …}}

After authorization the record is readable for 60 minutes, which is your window to collect the installationId — store it, it is permanent. Polling is idempotent, so a lost response cannot strand you. A second authorization by a different user or organization answers 409 installation_intent_already_authorized; an expired intent answers 410.

capabilityIds is 0–100 ids, each 3–128 characters. An empty list is accepted only for an identity-only resource audience owned by the App signing the request; it cannot install another publisher’s App without explicit capability grants. For a non-empty list, you may only request capabilities whose definition allows an app installation to hold them — some are human-only, and asking for one answers installation_not_authorized. Before a customer sees the screen, check which capabilities the resource server you target actually offers to app installations: wamp.ai.invoke for the AI proxy, and the wamp.cloud.* set documented at docs.cloud.vampikez.fun.

These exist in the platform and you may notice them in logs or token dumps. None of them are obtainable by a third-party developer, and none are a contract you should build against.

Credential Why it is not yours
Browser session cookies (__Host-wamp_access, __Secure-wamp_refresh) Issued only to same-origin requests from Account Center and first-party product web hosts
Extension token (aud: ext:<extensionId>) Minted by the desktop host for its own extensions; the trust boundary is the Electron main process
Engine connection token (aud: wamp-engine) The desktop host’s least-privilege credential for connecting to an engine core
Installation proxy token (sub: app-installation-proxy:…) Mintable only through the private internal delegation plane. Third parties use the installation access token above
Workload assertions to /internal/v1/* The private trust plane between first-party resource servers and the Account process

Bodies are {"success": false, "error": "<code>", "message": "…"}.

Code Status Meaning
invalid_request 400 The body failed validation
slug_reserved 400 The slug is in the platform-reserved list
invalid_public_key 400 Not an acceptable SPKI PEM Ed25519 public key
invalid_assertion 401 Bad signature, wrong iss/sub/aud, or a malformed header
unknown_app_or_key 401 No app with that slug, or no unrevoked key with that kid
assertion_too_long_lived 401 exp - iat exceeded 600 seconds, or iat/exp were missing
permission_denied 403 You lack organization.applications.manage on the publisher organization
app_suspended 403 The app is suspended; nothing it signs is accepted
app_not_found 404 Unknown slug, or one you do not administer
key_not_found 404 Unknown kid
installation_not_found 404 Unknown installation for this app
installation_intent_not_found 404 Unknown intent id
slug_taken 409 Another app already has that slug
key_already_registered 409 That public key is already on the app
installation_intent_already_authorized 409 Someone already authorized this intent
installation_intent_expired 410 Past the intent TTL

Limits are per client IP, token-bucket, and the limiter fails open if it errors — so treat them as a floor, not a guarantee.

Bucket Burst Sustained Applies to
Authentication 20 10 per minute POST /auth/login, POST /auth/change-password
Token issuance 60 60 per minute POST /auth/refresh, POST /auth/app-installation-token, app key, OAuth client, installation intents

A rejection is 429 with a Retry-After header and {"success": false, "error": "Too many requests. Please try again later.", "retryAfter": <seconds>}.

Two read-only endpoints report metered AI usage for your app, both requiring organization.applications.manage:

Terminal window
curl -s "$API/api/apps/acme-bot/usage?limit=100" -H "Authorization: Bearer $USER_ACCESS_TOKEN"
curl -s "$API/api/apps/acme-bot/usage/summary?from=2026-08-01" -H "Authorization: Bearer $USER_ACCESS_TOKEN"

The detail endpoint takes limit (≤100), before, from, to, and endUserId, and pages with before. Attribution by endUserId only appears for signed end-user subjects, or when an app-level credential sends user or safety_identifier on the HTTP AI request.