Skip to content

Authentication

After this page you can choose the right credential for your integration, produce one that /v1 accepts, refresh it before it expires, and say precisely which operations your installation is allowed to perform and why a 403 happened.

Every /v1 call carries the same header:

Authorization: Bearer <credential>

That is the only accepted scheme, and these properties hold whichever credential you put in it:

  • Bearer only. A request to /v1 without an Authorization: Bearer header is 401 bearer_credential_required, even if it carries a valid browser session cookie. /v1 is a service API, not a browser backend.
  • Bound to one installation, therefore one organization. Sending X-Wamp-Organization alongside a bearer is 400 organization_header_not_allowed — the credential already names exactly one tenant, and letting a header override it would be a tenancy hole.
  • Not a user credential. Sessions it creates are attributed to createdBy: { kind: "app_installation" }. It is not safe in a browser, a mobile app, or anywhere a customer can read it.
  • Authority is live, never a snapshot. Capabilities are re-resolved from the installation record on every request, so revoking the grant stops the credential immediately.

What differs is how you get one, and there are two ways:

Installation API key Installation token
What it is An opaque secret, wamp_cloud_live_…, shown exactly once A JWT you mint from your own Ed25519 key
Who creates it An administrator of the organization, in WAMP Account Your backend, per exchange
Setup you build None — one environment variable Keypair, key registration, assertion signing, token cache
Lifetime Until revoked, or an optional expiry the administrator sets 600 seconds, then you mint another
Organizations per credential One One per token; one key registration serves all of them
Reach for it when Your backend serves one organization: an internal integration, a single customer, an on-premise deployment Your product is installed by many organizations, or policy forbids a long-lived secret at rest

Start with the API key. For the single-organization case it is the whole answer, and building the assertion flow first is work you did not need. A wamp_cloud_… key cannot authenticate as a user or call the AI proxy. A separate wamp_ai_… key can call inference with the wamp.ai.invoke grant.

The low-engineering path, and the one to use unless the table above sends you elsewhere. Everything happens once, in WAMP Account, and it is the administrator of the organization whose work you will run who does it — not you.

Send them these steps. They need the organization.integrations.manage permission in that organization:

  1. Open WAMP Account and go to Installed apps.
  2. On the WAMP Cloud product, choose Integrations, then New integration.
  3. Name the integration after the backend that will hold the key, and select the capabilities that backend needs — the same ids as in What your installation may do. Select only what it uses; the set can be changed later without a new key.
  4. Copy the key. It is shown exactly once, in that dialog, and cannot be retrieved afterwards — only replaced.

That flow creates the installation for you: there is no app to register, no keypair, and no installation intent. What you receive is one string:

Terminal window
export WAMP_CLOUD_API_KEY='wamp_cloud_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
curl -sS "$WAMP_API/v1/capabilities" -H "Authorization: Bearer $WAMP_CLOUD_API_KEY"

With the Node SDK it is the whole constructor:

import { WampCloud } from '@wamp/app-sdk';
function requiredEnv(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`Set ${name}`);
return value;
}
const cloud = new WampCloud({
baseUrl: requiredEnv('WAMP_API'),
apiKey: requiredEnv('WAMP_CLOUD_API_KEY'),
});

What to know about the key itself:

  • Treat the whole string as the secret, prefix included. The prefix names the resource (cloud) and the environment (live), and a key that does not match the deployment it is presented to fails as 401 invalid_or_expired_credential.
  • Store it in a secret manager, load it at boot, never log it, never ship it to a client. Unlike a token, this one is worth stealing for as long as it lives.
  • At most two keys are active at a time per integration, which is what makes rotation possible: the administrator creates the second, you deploy it, they revoke the first.
  • Losing it is recoverable, retrieving it is not. The administrator replaces the key from the same screen; the replacement revokes the old one in the same step.
  • There is no expiresIn to watch. Nothing on your side refreshes. A 401 means the key was revoked, expired, or belongs to another deployment — not that it aged out, so do not build a re-mint loop around it.

The rest of this page is the other credential. If the key is what you are using, skip to What your installation may do.

An administrator can create an AI key in WAMP Account: Installed apps → WAMP AI → Integrations → New integration. Select wamp.ai.invoke and copy the wamp_ai_live_… key shown once. Send it to Account’s AI gateway as a bearer; WAMP_ACCOUNT is the Account origin, which may differ from the Cloud origin.

Terminal window
curl -sS "$WAMP_ACCOUNT/v1/models" \
-H "Authorization: Bearer $WAMP_AI_API_KEY"

This key is for inference only. It cannot call WAMP Cloud’s /v1 resource API or exchange for /v1/runtime-credentials; revoking it stops the next inference request. A wamp_cloud_… key cannot call the AI gateway.

The installation token, for a product many organizations install

Section titled “The installation token, for a product many organizations install”

Reach for this when one backend serves many customer organizations, or when a long-lived secret at rest is not acceptable. You register one Ed25519 public key against your app, and from then on you can mint a credential for any organization that installs you, without asking anyone to create a key per tenant.

Ed25519 keypair you generate it; the private half never leaves your process
│
▼ register the public key
your App (a slug + one or more key ids)
│
▼ a human administrator approves capabilities for their organization
an Installation (a UUID you store per customer)
│
▼ sign a ≤600s assertion, POST it to /auth/app-installation-token
an installation token (600s, audience wamp-cloud)
│
▼
/v1/...

Two origins are involved. The token exchange is served by WAMP Account; /v1 is served by WAMP Cloud. In a single-origin deployment they are the same host; in a split deployment they are not. Keep them as two configuration values from the start — your operator tells you both. Every call in the four steps below goes to Account:

Terminal window
export WAMP_ACCOUNT=https://api.vampikez.fun
  1. One Ed25519 keypair per app, generated once. WAMP only ever sees the public half, so there is no shared secret to leak and nothing to rotate by support ticket.

    import { generateKeyPair, exportPKCS8, exportSPKI, exportJWK, calculateJwkThumbprint } from 'jose';
    const { publicKey, privateKey } = await generateKeyPair('EdDSA', { extractable: true });
    const privateKeyPem = await exportPKCS8(privateKey); // secret — your key manager
    const publicKeyPem = await exportSPKI(publicKey); // register this with WAMP
    const kid = await calculateJwkThumbprint(await exportJWK(publicKey));
    • privateKeyPem is PKCS8 PEM. Put it in a secret manager and load it into memory at boot. It must never ship in a distributed client.
    • publicKeyPem is SPKI PEM (-----BEGIN PUBLIC KEY-----). This is what you register.
    • kid is the RFC 7638 JWK thumbprint of the public key — the identical value WAMP derives when you register it, and the kid your signatures must carry.

    Register the public key against your app slug as a signed-in publisher with organization.applications.manage, through Account or its app-management API. An App assertion cannot register its own key. Rotation is additive: register a second public key, switch the kid you sign with, then revoke the first. A revoked key stops verifying at the next exchange.

    The Node SDK ships this as generateAppKeypair(); see SDK for whether the package is installable for you yet. The snippet above uses jose directly and does not depend on it.

  2. The assertion is a JWT you sign with your private key. It proves your app’s identity to WAMP Account, and it is what authenticates every call in the two steps below.

    Part Value
    Header alg EdDSA — Ed25519. Nothing else is accepted
    Header kid Your registered key id (the JWK thumbprint)
    iss Your app slug
    sub Your app slug — the same value
    aud Account’s configured platform JWT issuer (JWT_ISSUER), supplied by the operator
    iat Issue time, in seconds
    exp Expiry. exp - iat must be ≤ 600 seconds, or the exchange fails
    assertion.mjs
    import { SignJWT, importPKCS8 } from 'jose';
    const key = await importPKCS8(process.env.WAMP_APP_PRIVATE_KEY, 'EdDSA');
    export async function assertion() {
    const now = Math.floor(Date.now() / 1000);
    return new SignJWT({})
    .setProtectedHeader({ alg: 'EdDSA', kid: process.env.WAMP_APP_KID })
    .setIssuer(process.env.WAMP_APP_SLUG)
    .setSubject(process.env.WAMP_APP_SLUG)
    .setAudience(process.env.WAMP_AUD)
    .setIssuedAt(now)
    .setExpirationTime(now + 300)
    .sign(key);
    }
    // Running the file prints one assertion, which is what the shell tabs read.
    if (process.argv[1]?.endsWith('assertion.mjs')) process.stdout.write(await assertion());

    The payload is otherwise empty, and the file reads four values from the environment: WAMP_APP_PRIVATE_KEY (the PKCS8 PEM from step 1), WAMP_APP_KID, WAMP_APP_SLUG, and WAMP_AUD — the aud value the aside below explains how to obtain. Signing a fresh assertion per exchange costs nothing, so the TypeScript tabs below call assertion() again for every request. To follow the shell tabs, export one:

    Terminal window
    export ASSERTION=$(node assertion.mjs)

    Re-run that when a command answers 401 invalid_assertion — an assertion past its exp fails exactly like a bad signature.

  3. An installation is one customer organization’s approval of your app for one exact resource server. Nothing exists — and no token can be minted — until a human approves it.

    If the administrator installs your app themselves, they hand you the installationId and you are done. To drive the approval from your own product, create an installation intent, which is a signed request for a consent URL:

    Terminal window
    INTENT=$(curl -sS -X POST "$WAMP_ACCOUNT/api/apps/installation-intents" \
    -H 'Content-Type: application/json' \
    -d "$(jq -n --arg a "$ASSERTION" '{
    assertion: $a,
    resourceAudience: "wamp-cloud",
    capabilityIds: [
    "wamp.cloud.access",
    "wamp.cloud.sessions:create",
    "wamp.cloud.sessions:read",
    "wamp.cloud.turns:submit"
    ]
    }')")
    INTENT_ID=$(printf '%s' "$INTENT" | jq -r '.intent.id')
    {
    "success": true,
    "intent": {
    "id": "c1e4f9a2-5d3b-4a7e-8f10-2b6c9d4e7a15",
    "status": "pending",
    "expiresAt": "2026-08-11T09:15:00.000Z",
    "authorizeUrl": "https://accounts.example.com/install/c1e4f9a2-5d3b-4a7e-8f10-2b6c9d4e7a15"
    }
    }

    Send the administrator to authorizeUrl. They choose the organization and approve the exact capability list you asked for. The intent is valid for 15 minutes.

    Then poll for the result. Polling is a POST because it is authenticated by the same assertion, and reading an authorized result does not consume it, so a lost response is safe to retry:

    Terminal window
    INSTALLATION=$(curl -sS -X POST \
    "$WAMP_ACCOUNT/api/apps/installation-intents/$INTENT_ID/status" \
    -H 'Content-Type: application/json' \
    -d "$(jq -n --arg a "$ASSERTION" '{assertion: $a}')")
    # Empty until a human has approved; the exchange below needs it.
    export WAMP_INSTALLATION_ID=$(printf '%s' "$INSTALLATION" \
    | jq -r '.intent.installationId // empty')
    {
    "success": true,
    "intent": {
    "id": "c1e4f9a2-5d3b-4a7e-8f10-2b6c9d4e7a15",
    "status": "authorized",
    "expiresAt": "2026-08-11T09:15:00.000Z",
    "authorizeUrl": "https://accounts.example.com/install/c1e4f9a2-5d3b-4a7e-8f10-2b6c9d4e7a15",
    "installationId": "8a0f1c6b-92d4-4e73-b5a1-0d3e7f2c9b48"
    }
    }

    status is pending, authorized, or expired; installationId appears only on authorized. Store it against your customer — it is the tenancy key for everything that follows.

    Every id in capabilityIds must be a capability that an app installation is allowed to hold. Asking for a human-only capability fails the whole intent with installation_not_authorized, so request from the list below, and request only what you use.

  4. The assertion travels in a body field named assertion — not in a header. The body is validated strictly: three required fields, one optional fourth, and nothing else.

    Terminal window
    curl -sS -X POST "$WAMP_ACCOUNT/auth/app-installation-token" \
    -H 'Content-Type: application/json' \
    -d "$(jq -n --arg a "$ASSERTION" --arg i "$WAMP_INSTALLATION_ID" \
    '{assertion: $a, installationId: $i, resourceAudience: "wamp-cloud"}')"
    { "success": true, "token": "eyJhbGciOiJFZERTQSIsImtpZCI6…", "expiresIn": 600 }
    Field Required Meaning
    assertion yes The signed JWT from the previous step, 20–4096 characters
    installationId yes UUID of the installation this token acts for
    resourceAudience yes The resource server the token is for. For the Cloud API it is always wamp-cloud
    setupAuthorityId no UUID. The opaque setup authority an authorized installation intent handed you. Send it only when completing that handshake; Account binds the live authorizing membership into the token itself, so you cannot supply a membership id

    Failures on this endpoint use Account’s envelope, { "success": false, "error": "<code>" }, rather than the /v1 error shape:

    Status error Cause
    400 invalid_request A missing, extra, or malformed field. This body also carries an issues array naming what failed validation
    401 invalid_assertion Signature, claims, aud, or expiry did not verify
    401 unknown_app_or_key No app matches iss, or no live key matches kid
    401 assertion_too_long_lived exp - iat exceeds 600 seconds, or iat/exp is missing
    403 app_suspended The app is suspended
    403 installation_not_found Not an active installation of this app
    403 installation_revoked The installation existed and was revoked. Nothing to retry; the organization must install the app again
    403 resource_server_not_found No active resource server for that resourceAudience
    403 installation_not_authorized The organization has not approved this app for that resource server
    429 rate limited Token issuance is rate limited per IP. Cache tokens; do not mint per request
    500 exchange_failed The exchange itself failed unexpectedly. Retry with backoff

The token lives 600 seconds and many runs take longer, so refreshing is part of normal operation rather than an error path. None of this applies to an installation API key, which has nothing to refresh.

  • Cache per installation. Key the cache by installationId. A multi-tenant backend holds one token per active customer, never one globally.
  • Refresh on a margin. Re-mint when less than a minute of life remains, using the expiresIn from the response rather than a hardcoded number.
  • Re-mint once on 401. If a /v1 call returns 401 invalid_or_expired_credential, discard the cached token, mint a new one, and retry the request exactly once. Never loop.
  • Never re-mint on 403. 403 insufficient_scope and 403 cloud_access_denied are authorization outcomes; a fresh token has identical authority. Surface them to whoever can fix the grant.
  • Never persist a token. Not in a database, not in a log, not in a trace. It is cheaper to mint another one.

Capabilities are the authorization model, and they are the same set whichever credential you hold: an administrator chooses them when creating an integration, or approves the list your installation intent asked for. Each one gates specific operations.

wamp.cloud.access is the gate on the credential itself: every /v1 request is checked against it before routing, and a credential without it gets 403 cloud_access_denied on everything. The rest gate individual operations:

Capability What it gates
wamp.cloud.access Using WAMP Cloud at all. Checked on every request
wamp.cloud.sessions:create PUT and PATCH a session — and GET /v1/capabilities
wamp.cloud.sessions:read Reading sessions, turns, runs, events, interactions, artifacts, publications, and the repository review
wamp.cloud.sessions:archive DELETE (archive) a session
wamp.cloud.turns:submit PUT a turn, which is how all work is submitted, and POST /v1/transcriptions
wamp.cloud.runs:cancel Requesting cancellation of a run
wamp.cloud.approvals:respond Answering a live approval with Session access; sensitive, grantable to humans and App installations
wamp.cloud.repositories:read Discovering usable repositories (GET /v1/repositories) and the grants behind them
wamp.cloud.publications:create Opening a pull request from a session’s reviewed tree. Sensitive
wamp.cloud.publications:merge Merging a publication at its exact reviewed head. Sensitive
wamp.cloud.runtime-releases:probe Activating or deactivating a bounded runtime-release probe for this installation’s new placements. Sensitive, APP_INSTALLATION only

When a call needs one you do not hold, the response names it:

{ "error": "insufficient_scope", "requiredScope": "wamp.cloud.turns:submit" }

The field is requiredScope, singular, and its value is a capability id — which makes it the string to put in the message you show an administrator.

The contract declares fourteen capabilities in total. The ten above are the ones an app installation can be granted. The other four — wamp.cloud.sessions:manage (opening a session an installed app created, by taking operator access on it), wamp.cloud.accounts:manage (enrolling runtime accounts), wamp.cloud.repositories:manage (connecting organization installations and changing application grants), and wamp.cloud.compute:manage (adding, draining and removing the machines the organization runs its work on) — can only be held by a human signed in to WAMP Cloud. They are administration surfaces, not part of an integration’s request, and asking for one in an installation intent fails the intent.

wamp.cloud.publications:create, wamp.cloud.publications:merge, and wamp.cloud.runtime-releases:probe are marked sensitive. The first two can write to a customer’s repository; the third is a release-authority override, not a session input. Expect them to receive more scrutiny during approval, and request them only if you need them. Most integrations never ask for the probe.